From 0e423b806cb502bf62cbbc820bdd912957342f25 Mon Sep 17 00:00:00 2001 From: Sasanka20 Date: Tue, 20 Oct 2026 10:45:03 +0530 Subject: [PATCH 1/3] Sms & Email --- .../Controllers/OwnerReportController.cs | 77 + .../ERPCore/Domain/Entities/OwnerReport.cs | 79 + .../ERPCore/Domain/Enums/OwnerReportEnums.cs | 16 + .../ERPCore/Dtos/Config/OwnerReportDtos.cs | 52 + .../ERPCore/Dtos/Warehouses/WarehouseDtos.cs | 6 +- .../Infra/Auth/AuthHex/AuthHexClient.cs | 61 + .../Infra/Auth/AuthHex/IAuthHexClient.cs | 8 + .../ERPCore/Infra/Messaging/MessageSenders.cs | 55 + .../OwnerReportConfiguration.cs | 75 + .../ERPCore/Infra/Persistence/DataSeeder.cs | 10 + .../ERPCore/Infra/Persistence/ErpDbContext.cs | 3 + ...90000_FixCheckConstraintQuotingForMySql.cs | 25 +- .../20261019053150_AddOwnerReport.Designer.cs | 11546 ++++++++++++++++ .../20261019053150_AddOwnerReport.cs | 131 + .../Migrations/ErpDbContextModelSnapshot.cs | 172 + Backend/ERPCore/Program.cs | 20 +- .../OwnerReport/OwnerReportContentBuilder.cs | 193 + .../Services/OwnerReport/OwnerReportJob.cs | 49 + .../OwnerReport/OwnerReportService.cs | 360 + Backend/PROGRESS.md | 9 + Frontend/PROGRESS.md | 5 + .../dashboard/settings/configuration/page.tsx | 8 +- .../dashboard/settings/owner-report/page.tsx | 524 + .../components/Layouts/Breadcrumbs.tsx | 1 + .../erp-system/components/Layouts/Header.tsx | 1 + Frontend/erp-system/lib/api/owner-report.ts | 37 + Frontend/erp-system/lib/validations/phone.ts | 28 +- Frontend/erp-system/types/owner-report.ts | 69 + docs/DAY_END_OWNER_REPORT_PLAN.md | 251 + 29 files changed, 13845 insertions(+), 26 deletions(-) create mode 100644 Backend/ERPCore/Controllers/OwnerReportController.cs create mode 100644 Backend/ERPCore/Domain/Entities/OwnerReport.cs create mode 100644 Backend/ERPCore/Domain/Enums/OwnerReportEnums.cs create mode 100644 Backend/ERPCore/Dtos/Config/OwnerReportDtos.cs create mode 100644 Backend/ERPCore/Infra/Messaging/MessageSenders.cs create mode 100644 Backend/ERPCore/Infra/Persistence/Configurations/OwnerReportConfiguration.cs create mode 100644 Backend/ERPCore/Migrations/20261019053150_AddOwnerReport.Designer.cs create mode 100644 Backend/ERPCore/Migrations/20261019053150_AddOwnerReport.cs create mode 100644 Backend/ERPCore/Services/OwnerReport/OwnerReportContentBuilder.cs create mode 100644 Backend/ERPCore/Services/OwnerReport/OwnerReportJob.cs create mode 100644 Backend/ERPCore/Services/OwnerReport/OwnerReportService.cs create mode 100644 Frontend/erp-system/app/dashboard/settings/owner-report/page.tsx create mode 100644 Frontend/erp-system/lib/api/owner-report.ts create mode 100644 Frontend/erp-system/types/owner-report.ts create mode 100644 docs/DAY_END_OWNER_REPORT_PLAN.md diff --git a/Backend/ERPCore/Controllers/OwnerReportController.cs b/Backend/ERPCore/Controllers/OwnerReportController.cs new file mode 100644 index 0000000..efb3a6f --- /dev/null +++ b/Backend/ERPCore/Controllers/OwnerReportController.cs @@ -0,0 +1,77 @@ +using ERPCore.Dtos.Common; +using ERPCore.Dtos.Config; +using ERPCore.Infra.Auth; +using ERPCore.Services.OwnerReport; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace ERPCore.Controllers; + +/// +/// Day-end owner report settings (docs/DAY_END_OWNER_REPORT_PLAN.md §3.5) — recipients, schedule, +/// preview, "send test now" and the send log. Gated on the Settings nav policy like the other +/// configuration screens (BranchConfigController, GlAccountConfigController). +/// +[Route("api/v1/settings/owner-report")] +[Authorize(Policy = NavPolicies.Prefix + "settings")] +public sealed class OwnerReportController : ApiControllerBase +{ + // One test send per minute (whole deployment) so the button can't be used to spam the owner. + private static readonly SemaphoreSlim TestGate = new(1, 1); + private static DateTime _lastTestUtc = DateTime.MinValue; + + private readonly IOwnerReportService _service; + + public OwnerReportController(IOwnerReportService service) => _service = service; + + [HttpGet] + [ProducesResponseType(typeof(OwnerReportConfigDto), StatusCodes.Status200OK)] + public async Task> Get(CancellationToken ct) + { + var result = await _service.GetAsync(ct); + SetETag(result.RowVersion); + return Ok(result.Value); + } + + [HttpPut] + [ProducesResponseType(typeof(OwnerReportConfigDto), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status412PreconditionFailed)] + [ProducesResponseType(StatusCodes.Status422UnprocessableEntity)] + public async Task> Update([FromBody] UpdateOwnerReportConfigRequest request, CancellationToken ct) + { + var result = await _service.UpdateAsync(request, RequireIfMatch(), ct); + SetETag(result.RowVersion); + return Ok(result.Value); + } + + [HttpGet("preview")] + [ProducesResponseType(typeof(OwnerReportPreviewDto), StatusCodes.Status200OK)] + public async Task> Preview([FromQuery] DateOnly? date, CancellationToken ct) + => Ok(await _service.PreviewAsync(date, ct)); + + [HttpPost("test")] + [ProducesResponseType(typeof(IReadOnlyList), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status429TooManyRequests)] + public async Task>> SendTest([FromBody] OwnerReportTestRequest? request, CancellationToken ct) + { + await TestGate.WaitAsync(ct); + try + { + if (DateTime.UtcNow - _lastTestUtc < TimeSpan.FromMinutes(1)) + return StatusCode(StatusCodes.Status429TooManyRequests, + new { title = "A test was sent less than a minute ago — wait a moment before sending another." }); + _lastTestUtc = DateTime.UtcNow; + } + finally + { + TestGate.Release(); + } + + return Ok(await _service.SendTestAsync(request?.Date, ct)); + } + + [HttpGet("dispatches")] + [ProducesResponseType(typeof(PagedResponse), StatusCodes.Status200OK)] + public async Task>> Dispatches([FromQuery] PageQuery query, CancellationToken ct) + => Ok(await _service.ListDispatchesAsync(query, ct)); +} diff --git a/Backend/ERPCore/Domain/Entities/OwnerReport.cs b/Backend/ERPCore/Domain/Entities/OwnerReport.cs new file mode 100644 index 0000000..166d178 --- /dev/null +++ b/Backend/ERPCore/Domain/Entities/OwnerReport.cs @@ -0,0 +1,79 @@ +using ERPCore.Domain.Enums; + +namespace ERPCore.Domain.Entities; + +/// +/// Day-end owner report settings — a singleton row, same shape as +/// (docs/DAY_END_OWNER_REPORT_PLAN.md §3.1). When , OwnerReportJob sends the +/// day's figures to every active at . +/// +public class OwnerReportConfig +{ + /// Always 1 - the singleton row's id. + public const int SingletonId = 1; + + public int ConfigId { get; set; } + + /// Master switch — nothing is sent automatically while off ("Send test now" still works). + public bool Enabled { get; set; } + + public bool SmsEnabled { get; set; } = true; + public bool EmailEnabled { get; set; } = true; + + /// Local time of day (DayEndReport:TimeZone) after which the day's report goes out. + public TimeOnly SendAtLocalTime { get; set; } = new(21, 0); + + /// Adds one summary row per active branch when branching is switched on. + public bool IncludeBranchBreakdown { get; set; } = true; + + public DateTime? UpdatedAt { get; set; } + public int? UpdatedBy { get; set; } + public User? UpdatedByUser { get; set; } + + public uint RowVersion { get; set; } +} + +/// Someone who receives the day-end report — by SMS, email or both. +public class OwnerReportRecipient +{ + public int RecipientId { get; set; } + public string Name { get; set; } = string.Empty; + + /// Normalised to +94XXXXXXXXX (Sri Lankan rule, see PhoneRules). + public string? Mobile { get; set; } + public string? Email { get; set; } + public bool IsActive { get; set; } = true; +} + +/// +/// One send of one day's report on one channel to one recipient — the send log, and the guard +/// against sending twice: (BusinessDate, Channel, RecipientId, IsTest) is unique for automatic +/// sends, so a restart or a second server instance can't send the same day again. +/// +public class OwnerReportDispatch +{ + public long DispatchId { get; set; } + public DateOnly BusinessDate { get; set; } + public OwnerReportChannel Channel { get; set; } + + public int? RecipientId { get; set; } + public OwnerReportRecipient? Recipient { get; set; } + + /// Number / email address at send time — kept even if the recipient is edited later. + public string Destination { get; set; } = string.Empty; + public string RecipientName { get; set; } = string.Empty; + + public OwnerReportDispatchStatus Status { get; set; } + public int Attempts { get; set; } + public string? Error { get; set; } + + /// Test sends ("Send test now") — never block or count as the day's automatic send. + public bool IsTest { get; set; } + + /// For tests: a unique token so repeated tests don't collide on the unique index. + public string DedupeKey { get; set; } = string.Empty; + + public DateTime CreatedAt { get; set; } + public DateTime? SentAt { get; set; } + public DateTime? NextAttemptAt { get; set; } +} diff --git a/Backend/ERPCore/Domain/Enums/OwnerReportEnums.cs b/Backend/ERPCore/Domain/Enums/OwnerReportEnums.cs new file mode 100644 index 0000000..925fcc9 --- /dev/null +++ b/Backend/ERPCore/Domain/Enums/OwnerReportEnums.cs @@ -0,0 +1,16 @@ +namespace ERPCore.Domain.Enums; + +/// How the day-end owner report is delivered (docs/DAY_END_OWNER_REPORT_PLAN.md). +public enum OwnerReportChannel +{ + Sms, + Email, +} + +/// State of one owner-report send. +public enum OwnerReportDispatchStatus +{ + Pending, + Sent, + Failed, +} diff --git a/Backend/ERPCore/Dtos/Config/OwnerReportDtos.cs b/Backend/ERPCore/Dtos/Config/OwnerReportDtos.cs new file mode 100644 index 0000000..75b39e6 --- /dev/null +++ b/Backend/ERPCore/Dtos/Config/OwnerReportDtos.cs @@ -0,0 +1,52 @@ +using System.ComponentModel.DataAnnotations; + +namespace ERPCore.Dtos.Config; + +/// Day-end owner report settings (docs/DAY_END_OWNER_REPORT_PLAN.md §3.5). +/// "HH:mm" in . +public sealed record OwnerReportConfigDto( + bool Enabled, bool SmsEnabled, bool EmailEnabled, string SendAtLocalTime, bool IncludeBranchBreakdown, + IReadOnlyList Recipients, string TimeZone, DateTime? UpdatedAt, int? UpdatedBy); + +public sealed record OwnerReportRecipientDto(int RecipientId, string Name, string? Mobile, string? Email, bool IsActive); + +public sealed class UpdateOwnerReportConfigRequest +{ + [Required] public bool? Enabled { get; set; } + [Required] public bool? SmsEnabled { get; set; } + [Required] public bool? EmailEnabled { get; set; } + [Required] public bool? IncludeBranchBreakdown { get; set; } + + /// "HH:mm", 24-hour. + [Required, RegularExpression(@"^([01]\d|2[0-3]):[0-5]\d$", ErrorMessage = "Send time must be HH:mm (24-hour).")] + public string SendAtLocalTime { get; set; } = "21:00"; + + /// The full list — recipients not in it are removed. At most 5. + [Required, MaxLength(5, ErrorMessage = "At most 5 recipients.")] + public List Recipients { get; set; } = new(); +} + +public sealed class OwnerReportRecipientInput +{ + /// Existing recipient to update; null adds a new one. + public int? RecipientId { get; set; } + [Required, StringLength(100, MinimumLength = 1)] public string Name { get; set; } = string.Empty; + [StringLength(20)] public string? Mobile { get; set; } + [StringLength(200), EmailAddress] public string? Email { get; set; } + public bool IsActive { get; set; } = true; +} + +/// The report as it would be sent for . +public sealed record OwnerReportPreviewDto(DateOnly Date, string SmsText, string EmailSubject, string EmailHtml); + +public sealed class OwnerReportTestRequest +{ + /// Day to report; null = today in the report's time zone. + public DateOnly? Date { get; set; } +} + +public sealed record OwnerReportSendResultDto(string Channel, string RecipientName, string Destination, bool Ok, string? Error); + +public sealed record OwnerReportDispatchDto( + long DispatchId, DateOnly BusinessDate, string Channel, string RecipientName, string Destination, + string Status, int Attempts, string? Error, bool IsTest, DateTime CreatedAt, DateTime? SentAt); diff --git a/Backend/ERPCore/Dtos/Warehouses/WarehouseDtos.cs b/Backend/ERPCore/Dtos/Warehouses/WarehouseDtos.cs index b22d624..f9e37e0 100644 --- a/Backend/ERPCore/Dtos/Warehouses/WarehouseDtos.cs +++ b/Backend/ERPCore/Dtos/Warehouses/WarehouseDtos.cs @@ -11,11 +11,11 @@ public sealed record WarehouseDto(int WarehouseId, string Code, string Name, str public sealed record BinDto(int BinId, int WarehouseId, string Code, string? BinType); /// Sri Lankan phone rule for master data — mirrors the frontend's `lib/validations/phone.ts`: -/// 10 digits starting with 0, or +94 followed by 9 digits; spaces and hyphens allowed between digits. +/// 10 digits starting with 0, or 94 followed by 9 digits with or without a leading +; spaces and hyphens allowed between digits. public static class PhoneRules { - public const string Pattern = @"^(?:0(?:[\s-]*\d){9}|\+94(?:[\s-]*\d){9})[\s-]*$"; - public const string Message = "Enter a valid Sri Lankan phone number: 10 digits starting with 0 (0771234567) or +94 followed by 9 digits (+94771234567)."; + public const string Pattern = @"^(?:0(?:[\s-]*\d){9}|\+?94(?:[\s-]*\d){9})[\s-]*$"; + public const string Message = "Enter a valid Sri Lankan phone number: 10 digits starting with 0 (0771234567), or 94 followed by 9 digits (94771234567 or +94771234567)."; } public sealed class CreateWarehouseRequest diff --git a/Backend/ERPCore/Infra/Auth/AuthHex/AuthHexClient.cs b/Backend/ERPCore/Infra/Auth/AuthHex/AuthHexClient.cs index 9f500a4..982797d 100644 --- a/Backend/ERPCore/Infra/Auth/AuthHex/AuthHexClient.cs +++ b/Backend/ERPCore/Infra/Auth/AuthHex/AuthHexClient.cs @@ -154,6 +154,67 @@ public sealed class AuthHexClient : IAuthHexClient public Task VerifyAltOtpAsync(VerifyAltOtpRequest request, CancellationToken ct) => CallAsync("alt", "VerifyOTP", request, null, ct); + // ---- Notify ------------------------------------------------------------- + + public Task> SendEmailAsync(IReadOnlyList to, string subject, string htmlBody, string? textBody, CancellationToken ct) + => NotifyAsync("email", new { to, subject, htmlBody, textBody }, ct); + + public Task> SendSmsAsync(IReadOnlyList to, string text, CancellationToken ct) + => NotifyAsync("sms", new { to, text }, ct); + + /// + /// /api/notify/* is plain JSON ({ results: [{ to, ok, error }] }), not the function + /// envelope the manager routes use, so it bypasses . + /// + private async Task> NotifyAsync(string channel, object body, CancellationToken ct) + { + using var httpRequest = new HttpRequestMessage(HttpMethod.Post, $"/api/notify/{channel}"); + foreach (var (key, value) in AdminHeaders()) + httpRequest.Headers.Add(key, value); + httpRequest.Content = JsonContent.Create(body, options: JsonOptions); + + HttpResponseMessage httpResponse; + try + { + httpResponse = await _http.SendAsync(httpRequest, ct); + } + catch (HttpRequestException) + { + throw new DomainException(ErrorCodes.AuthServiceUnavailable, "The authentication service is unreachable.", 503); + } + catch (TaskCanceledException) when (!ct.IsCancellationRequested) + { + throw new DomainException(ErrorCodes.AuthServiceUnavailable, "The authentication service timed out.", 503); + } + + NotifyResponse? payload = null; + try + { + payload = await httpResponse.Content.ReadFromJsonAsync(JsonOptions, ct); + } + catch (JsonException) + { + // Fall through — reported below with the HTTP status. + } + + if (!httpResponse.IsSuccessStatusCode || payload?.Results is null) + { + var message = payload?.Message + ?? (httpResponse.StatusCode == global::System.Net.HttpStatusCode.NotFound + ? "The authentication service does not support sending messages yet (no /api/notify)." + : $"The authentication service could not send the {channel} (HTTP {(int)httpResponse.StatusCode})."); + throw new DomainException(ErrorCodes.AuthUpstreamError, message, 502); + } + + return payload.Results; + } + + private sealed class NotifyResponse + { + public List? Results { get; set; } + public string? Message { get; set; } + } + // ---- Transport -------------------------------------------------------- private async Task CallVoidAsync(string routeGroup, string functionName, object payload, string? bearerToken, CancellationToken ct) diff --git a/Backend/ERPCore/Infra/Auth/AuthHex/IAuthHexClient.cs b/Backend/ERPCore/Infra/Auth/AuthHex/IAuthHexClient.cs index 54efe32..f83e19d 100644 --- a/Backend/ERPCore/Infra/Auth/AuthHex/IAuthHexClient.cs +++ b/Backend/ERPCore/Infra/Auth/AuthHex/IAuthHexClient.cs @@ -57,4 +57,12 @@ public interface IAuthHexClient Task IsAvailableAsync(IsAvailableRequest request, CancellationToken ct); Task SendOtpAsync(SendOtpRequest request, CancellationToken ct); Task VerifyAltOtpAsync(VerifyAltOtpRequest request, CancellationToken ct); + + // Notify (POST /api/notify/email|sms) — service-to-service messaging through AuthHex's own + // SMTP / SMS gateway settings, gated by the admin API key (docs/DAY_END_OWNER_REPORT_PLAN.md §3.2). + Task> SendEmailAsync(IReadOnlyList to, string subject, string htmlBody, string? textBody, CancellationToken ct); + Task> SendSmsAsync(IReadOnlyList to, string text, CancellationToken ct); } + +/// Per-recipient outcome of an AuthHex notify call. +public sealed record AuthHexNotifyResult(string To, bool Ok, string? Error); diff --git a/Backend/ERPCore/Infra/Messaging/MessageSenders.cs b/Backend/ERPCore/Infra/Messaging/MessageSenders.cs new file mode 100644 index 0000000..c405cf2 --- /dev/null +++ b/Backend/ERPCore/Infra/Messaging/MessageSenders.cs @@ -0,0 +1,55 @@ +using ERPCore.Infra.Auth.AuthHex; + +namespace ERPCore.Infra.Messaging; + +/// Per-recipient outcome of a send. +public sealed record SendResult(string To, bool Ok, string? Error); + +/// +/// Sends email and SMS. ERPCore holds no SMTP or SMS credentials — the real implementation asks +/// AuthHex to send with its own settings (docs/DAY_END_OWNER_REPORT_PLAN.md §3.2). +/// Implementations report per-recipient failures in the result and only throw when the whole +/// call failed (e.g. AuthHex unreachable). +/// +public interface IMessageSender +{ + Task> SendEmailAsync(IReadOnlyList to, string subject, string htmlBody, string? textBody, CancellationToken ct); + Task> SendSmsAsync(IReadOnlyList to, string text, CancellationToken ct); +} + +/// Default: sends through AuthHex's /api/notify endpoints (Messaging:Provider = AuthHex). +public sealed class AuthHexMessageSender : IMessageSender +{ + private readonly IAuthHexClient _authHex; + + public AuthHexMessageSender(IAuthHexClient authHex) => _authHex = authHex; + + public async Task> SendEmailAsync(IReadOnlyList to, string subject, string htmlBody, string? textBody, CancellationToken ct) + => (await _authHex.SendEmailAsync(to, subject, htmlBody, textBody, ct)).Select(r => new SendResult(r.To, r.Ok, r.Error)).ToList(); + + public async Task> SendSmsAsync(IReadOnlyList to, string text, CancellationToken ct) + => (await _authHex.SendSmsAsync(to, text, ct)).Select(r => new SendResult(r.To, r.Ok, r.Error)).ToList(); +} + +/// +/// Writes messages to the log instead of sending (Messaging:Provider = Log) — for local +/// development and tests, so nothing reaches real phones or inboxes. +/// +public sealed class LogOnlyMessageSender : IMessageSender +{ + private readonly ILogger _logger; + + public LogOnlyMessageSender(ILogger logger) => _logger = logger; + + public Task> SendEmailAsync(IReadOnlyList to, string subject, string htmlBody, string? textBody, CancellationToken ct) + { + _logger.LogInformation("[LogOnly] Email to {To}: {Subject}\n{Body}", string.Join(", ", to), subject, textBody ?? htmlBody); + return Task.FromResult>(to.Select(t => new SendResult(t, true, null)).ToList()); + } + + public Task> SendSmsAsync(IReadOnlyList to, string text, CancellationToken ct) + { + _logger.LogInformation("[LogOnly] SMS to {To}:\n{Text}", string.Join(", ", to), text); + return Task.FromResult>(to.Select(t => new SendResult(t, true, null)).ToList()); + } +} diff --git a/Backend/ERPCore/Infra/Persistence/Configurations/OwnerReportConfiguration.cs b/Backend/ERPCore/Infra/Persistence/Configurations/OwnerReportConfiguration.cs new file mode 100644 index 0000000..8198a06 --- /dev/null +++ b/Backend/ERPCore/Infra/Persistence/Configurations/OwnerReportConfiguration.cs @@ -0,0 +1,75 @@ +using ERPCore.Domain.Entities; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace ERPCore.Infra.Persistence.Configurations; + +/// Singleton owner-report settings row (mirrors SalesConfigConfiguration). +public sealed class OwnerReportConfigConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("owner_report_config", t => + t.HasCheckConstraint("ck_owner_report_config_singleton", $"`ConfigId` = {OwnerReportConfig.SingletonId}")); + + builder.HasKey(c => c.ConfigId); + builder.Property(c => c.ConfigId).ValueGeneratedNever(); + + builder.Property(c => c.Enabled).IsRequired().HasDefaultValue(false); + // Sentinel = the CLR initializer (true), so an explicit false is still written on insert. + builder.Property(c => c.SmsEnabled).IsRequired().HasDefaultValue(true).HasSentinel(true); + builder.Property(c => c.EmailEnabled).IsRequired().HasDefaultValue(true).HasSentinel(true); + builder.Property(c => c.IncludeBranchBreakdown).IsRequired().HasDefaultValue(true).HasSentinel(true); + builder.Property(c => c.SendAtLocalTime).IsRequired(); + + builder.Property(c => c.RowVersion).IsRowVersion(); + + builder.HasOne(c => c.UpdatedByUser) + .WithMany() + .HasForeignKey(c => c.UpdatedBy) + .OnDelete(DeleteBehavior.Restrict); + } +} + +public sealed class OwnerReportRecipientConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("owner_report_recipients"); + builder.HasKey(r => r.RecipientId); + builder.Property(r => r.Name).HasMaxLength(100).IsRequired(); + builder.Property(r => r.Mobile).HasMaxLength(20); + builder.Property(r => r.Email).HasMaxLength(200); + builder.Property(r => r.IsActive).IsRequired().HasDefaultValue(true).HasSentinel(true); + } +} + +public sealed class OwnerReportDispatchConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("owner_report_dispatches"); + builder.HasKey(d => d.DispatchId); + + builder.Property(d => d.Channel).HasConversion().HasMaxLength(10).IsRequired(); + builder.Property(d => d.Status).HasConversion().HasMaxLength(10).IsRequired(); + builder.Property(d => d.Destination).HasMaxLength(200).IsRequired(); + builder.Property(d => d.RecipientName).HasMaxLength(100).IsRequired(); + builder.Property(d => d.Error).HasMaxLength(1000); + builder.Property(d => d.DedupeKey).HasMaxLength(40).IsRequired(); + + // The "send once" guard: one automatic send per day, channel and recipient. Test sends get + // a unique DedupeKey so any number of them can coexist. + builder.HasIndex(d => new { d.BusinessDate, d.Channel, d.RecipientId, d.IsTest, d.DedupeKey }) + .IsUnique() + .HasDatabaseName("ux_owner_report_dispatch_once"); + builder.HasIndex(d => new { d.Status, d.NextAttemptAt }); + builder.HasIndex(d => d.CreatedAt); + + // Recipients can be removed; their past sends stay in the log (Destination/RecipientName keep the detail). + builder.HasOne(d => d.Recipient) + .WithMany() + .HasForeignKey(d => d.RecipientId) + .OnDelete(DeleteBehavior.SetNull); + } +} diff --git a/Backend/ERPCore/Infra/Persistence/DataSeeder.cs b/Backend/ERPCore/Infra/Persistence/DataSeeder.cs index 8bcc85e..13275d9 100644 --- a/Backend/ERPCore/Infra/Persistence/DataSeeder.cs +++ b/Backend/ERPCore/Infra/Persistence/DataSeeder.cs @@ -64,6 +64,7 @@ public static class DataSeeder dirty |= await SeedProcurementConfigAsync(db, ct); dirty |= await SeedGlAccountConfigAsync(db, configuration, ct); dirty |= await SeedNotificationConfigAsync(db, ct); + dirty |= await SeedOwnerReportConfigAsync(db, ct); if (dirty) await db.SaveChangesAsync(ct); @@ -309,6 +310,15 @@ public static class DataSeeder }); return true; } + // Off by default: the owner report only starts sending once someone adds recipients and switches it on. + private static async Task SeedOwnerReportConfigAsync(ErpDbContext db, CancellationToken ct) + { + if (await db.OwnerReportConfig.AnyAsync(c => c.ConfigId == OwnerReportConfig.SingletonId, ct)) return false; + + db.OwnerReportConfig.Add(new OwnerReportConfig { ConfigId = OwnerReportConfig.SingletonId, Enabled = false }); + return true; + } + private static async Task SeedNotificationConfigAsync(ErpDbContext db, CancellationToken ct) { diff --git a/Backend/ERPCore/Infra/Persistence/ErpDbContext.cs b/Backend/ERPCore/Infra/Persistence/ErpDbContext.cs index b9bba97..8403c4a 100644 --- a/Backend/ERPCore/Infra/Persistence/ErpDbContext.cs +++ b/Backend/ERPCore/Infra/Persistence/ErpDbContext.cs @@ -55,6 +55,9 @@ public class ErpDbContext : DbContext public DbSet GlAccountConfig => Set(); /// Singleton row for notification delivery preferences. public DbSet NotificationConfig => Set(); + public DbSet OwnerReportConfig => Set(); + public DbSet OwnerReportRecipients => Set(); + public DbSet OwnerReportDispatches => Set(); public DbSet NotificationConfigModules => Set(); public DbSet Notifications => Set(); public DbSet NotificationRecipients => Set(); diff --git a/Backend/ERPCore/Migrations/20261005190000_FixCheckConstraintQuotingForMySql.cs b/Backend/ERPCore/Migrations/20261005190000_FixCheckConstraintQuotingForMySql.cs index d187ec9..f81051a 100644 --- a/Backend/ERPCore/Migrations/20261005190000_FixCheckConstraintQuotingForMySql.cs +++ b/Backend/ERPCore/Migrations/20261005190000_FixCheckConstraintQuotingForMySql.cs @@ -17,8 +17,8 @@ namespace ERPCore.Migrations /// /// Written by hand rather than scaffolded, and the drop/add is emitted as raw SQL instead of /// DropCheckConstraint: MariaDB spells the removal DROP CONSTRAINT (MySQL's - /// DROP CHECK is not a safe assumption here), and IF EXISTS keeps this - /// re-runnable on a database where the broken constraint was already removed by hand. + /// DROP CHECK is not a safe assumption here). It is a plain drop, not IF EXISTS: + /// that form is MariaDB-only and a syntax error on MySQL 8, which broke fresh local databases. /// /// public partial class FixCheckConstraintQuotingForMySql : Migration @@ -26,13 +26,13 @@ namespace ERPCore.Migrations /// protected override void Up(MigrationBuilder migrationBuilder) { - migrationBuilder.Sql("ALTER TABLE `grn_config` DROP CONSTRAINT IF EXISTS `ck_grn_config_singleton`;"); + DropCheck(migrationBuilder, "grn_config", "ck_grn_config_singleton"); migrationBuilder.Sql("ALTER TABLE `grn_config` ADD CONSTRAINT `ck_grn_config_singleton` CHECK (`ConfigId` = 1);"); - migrationBuilder.Sql("ALTER TABLE `sales_config` DROP CONSTRAINT IF EXISTS `ck_sales_config_singleton`;"); + DropCheck(migrationBuilder, "sales_config", "ck_sales_config_singleton"); migrationBuilder.Sql("ALTER TABLE `sales_config` ADD CONSTRAINT `ck_sales_config_singleton` CHECK (`ConfigId` = 1);"); - migrationBuilder.Sql("ALTER TABLE `warranty_periods` DROP CONSTRAINT IF EXISTS `ck_warranty_periods_months`;"); + DropCheck(migrationBuilder, "warranty_periods", "ck_warranty_periods_months"); migrationBuilder.Sql("ALTER TABLE `warranty_periods` ADD CONSTRAINT `ck_warranty_periods_months` CHECK (`Months` BETWEEN 1 AND 600);"); } @@ -40,14 +40,23 @@ namespace ERPCore.Migrations protected override void Down(MigrationBuilder migrationBuilder) { // Restores the original (broken) expressions so the migration is a faithful inverse. - migrationBuilder.Sql("ALTER TABLE `grn_config` DROP CONSTRAINT IF EXISTS `ck_grn_config_singleton`;"); + DropCheck(migrationBuilder, "grn_config", "ck_grn_config_singleton"); migrationBuilder.Sql("ALTER TABLE `grn_config` ADD CONSTRAINT `ck_grn_config_singleton` CHECK (\"ConfigId\" = 1);"); - migrationBuilder.Sql("ALTER TABLE `sales_config` DROP CONSTRAINT IF EXISTS `ck_sales_config_singleton`;"); + DropCheck(migrationBuilder, "sales_config", "ck_sales_config_singleton"); migrationBuilder.Sql("ALTER TABLE `sales_config` ADD CONSTRAINT `ck_sales_config_singleton` CHECK (\"ConfigId\" = 1);"); - migrationBuilder.Sql("ALTER TABLE `warranty_periods` DROP CONSTRAINT IF EXISTS `ck_warranty_periods_months`;"); + DropCheck(migrationBuilder, "warranty_periods", "ck_warranty_periods_months"); migrationBuilder.Sql("ALTER TABLE `warranty_periods` ADD CONSTRAINT `ck_warranty_periods_months` CHECK (\"Months\" BETWEEN 1 AND 600);"); } + + /// + /// Plain DROP CONSTRAINT — valid on MariaDB and on MySQL 8.0.19+. Deliberately not + /// DROP CONSTRAINT IF EXISTS, which is MariaDB-only and is a syntax error on MySQL 8, + /// breaking fresh local databases. The constraints always exist at this point: the earlier + /// migrations create them. + /// + private static void DropCheck(MigrationBuilder migrationBuilder, string table, string constraint) + => migrationBuilder.Sql($"ALTER TABLE `{table}` DROP CONSTRAINT `{constraint}`;"); } } diff --git a/Backend/ERPCore/Migrations/20261019053150_AddOwnerReport.Designer.cs b/Backend/ERPCore/Migrations/20261019053150_AddOwnerReport.Designer.cs new file mode 100644 index 0000000..aac0958 --- /dev/null +++ b/Backend/ERPCore/Migrations/20261019053150_AddOwnerReport.Designer.cs @@ -0,0 +1,11546 @@ +// +using System; +using ERPCore.Infra.Persistence; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace ERPCore.Migrations +{ + [DbContext(typeof(ErpDbContext))] + [Migration("20261019053150_AddOwnerReport")] + partial class AddOwnerReport + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .UseCollation("utf8mb4_unicode_ci") + .HasAnnotation("ProductVersion", "9.0.20") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder); + + modelBuilder.Entity("ERPCore.Domain.Entities.AccessConfig", b => + { + b.Property("ConfigId") + .HasColumnType("int"); + + b.Property("DefaultRoleId") + .HasColumnType("int"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("int unsigned"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.Property("UpdatedBy") + .HasColumnType("int"); + + b.HasKey("ConfigId"); + + b.HasIndex("DefaultRoleId"); + + b.HasIndex("UpdatedBy"); + + b.ToTable("access_config", null, t => + { + t.HasCheckConstraint("ck_access_config_singleton", "`ConfigId` = 1"); + }); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.ApprovalConfig", b => + { + b.Property("ApprovalConfigId") + .HasColumnType("int"); + + b.Property("Level2RoleIds") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("PriceAdjustmentLevel2ThresholdPercent") + .HasPrecision(9, 4) + .HasColumnType("decimal(9,4)"); + + b.Property("PriceAdjustmentLevels") + .HasColumnType("int"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("int unsigned"); + + b.Property("StockAdjustmentLevel2ThresholdValue") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("StockAdjustmentLevels") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.Property("UpdatedBy") + .HasColumnType("int"); + + b.Property("WastageLevels") + .HasColumnType("int"); + + b.HasKey("ApprovalConfigId"); + + b.ToTable("approval_config", null, t => + { + t.HasCheckConstraint("ck_approval_config_singleton", "`ApprovalConfigId` = 1"); + }); + + b.HasData( + new + { + ApprovalConfigId = 1, + PriceAdjustmentLevel2ThresholdPercent = 0m, + PriceAdjustmentLevels = 2, + RowVersion = 1u, + StockAdjustmentLevel2ThresholdValue = 0m, + StockAdjustmentLevels = 2, + WastageLevels = 0 + }); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.AttendanceRecord", b => + { + b.Property("AttendanceRecordId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("AttendanceRecordId")); + + b.Property("AttendanceDate") + .HasColumnType("datetime(6)"); + + b.Property("AttendanceStatus") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("varchar(20)"); + + b.Property("AttendanceUploadBatchId") + .HasColumnType("int"); + + b.Property("CheckIn") + .HasColumnType("time(6)"); + + b.Property("CheckOut") + .HasColumnType("time(6)"); + + b.Property("DuplicateOfAttendanceRecordId") + .HasColumnType("int"); + + b.Property("EarlyLeaveMinutes") + .HasColumnType("int"); + + b.Property("EditedAt") + .HasColumnType("datetime(6)"); + + b.Property("EditedBy") + .HasColumnType("int"); + + b.Property("EmployeeId") + .HasColumnType("int"); + + b.Property("IsManualOverride") + .HasColumnType("tinyint(1)"); + + b.Property("LateMinutes") + .HasColumnType("int"); + + b.Property("Notes") + .HasMaxLength(1000) + .HasColumnType("varchar(1000)"); + + b.Property("OvertimeMinutes") + .HasColumnType("int"); + + b.Property("RowValidationStatus") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("varchar(20)"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("int unsigned"); + + b.Property("WorkShiftId") + .HasColumnType("int"); + + b.Property("WorkingMinutes") + .HasColumnType("int"); + + b.HasKey("AttendanceRecordId"); + + b.HasIndex("AttendanceUploadBatchId"); + + b.HasIndex("WorkShiftId"); + + b.HasIndex("EmployeeId", "AttendanceDate"); + + b.ToTable("hr_attendance_records", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.AttendanceUploadBatch", b => + { + b.Property("AttendanceUploadBatchId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("AttendanceUploadBatchId")); + + b.Property("ConfirmedAt") + .HasColumnType("datetime(6)"); + + b.Property("ConfirmedBy") + .HasColumnType("int"); + + b.Property("DocNo") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("varchar(30)"); + + b.Property("OriginalFileName") + .HasMaxLength(260) + .HasColumnType("varchar(260)"); + + b.Property("PeriodEnd") + .HasColumnType("datetime(6)"); + + b.Property("PeriodStart") + .HasColumnType("datetime(6)"); + + b.Property("RowCountDuplicate") + .HasColumnType("int"); + + b.Property("RowCountError") + .HasColumnType("int"); + + b.Property("RowCountTotal") + .HasColumnType("int"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("int unsigned"); + + b.Property("SourceType") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("varchar(20)"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("varchar(20)"); + + b.Property("UploadedAt") + .HasColumnType("datetime(6)"); + + b.Property("UploadedBy") + .HasColumnType("int"); + + b.HasKey("AttendanceUploadBatchId"); + + b.HasIndex("DocNo") + .IsUnique(); + + b.HasIndex("Status"); + + b.HasIndex("PeriodStart", "PeriodEnd"); + + b.ToTable("hr_attendance_upload_batches", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.AuditLog", b => + { + b.Property("AuditId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("AuditId")); + + b.Property("Action") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("varchar(10)"); + + b.Property("ChangeSet") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("EntityId") + .HasColumnType("int"); + + b.Property("EntityType") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("varchar(80)"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("AuditId"); + + b.HasIndex("CreatedAt"); + + b.HasIndex("UserId"); + + b.HasIndex("EntityType", "EntityId"); + + b.ToTable("audit_logs", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.BankDeposit", b => + { + b.Property("BankDepositId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("BankDepositId")); + + b.Property("Amount") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("BranchId") + .HasColumnType("int"); + + b.Property("BusinessDate") + .HasColumnType("date"); + + b.Property("CancelReason") + .HasMaxLength(250) + .HasColumnType("varchar(250)"); + + b.Property("CancelledAt") + .HasColumnType("datetime(6)"); + + b.Property("CancelledBy") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("CreatedBy") + .HasColumnType("int"); + + b.Property("DepositDate") + .HasColumnType("date"); + + b.Property("DepositSlipNo") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("DepositedBy") + .HasMaxLength(150) + .HasColumnType("varchar(150)"); + + b.Property("DocNo") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("varchar(30)"); + + b.Property("FromCashAccountCode") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("varchar(20)"); + + b.Property("FromCashAccountName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("FromGlCashAccountId") + .HasColumnType("bigint"); + + b.Property("GlJournalNo") + .HasMaxLength(30) + .HasColumnType("varchar(30)"); + + b.Property("GlPostedAt") + .HasColumnType("datetime(6)"); + + b.Property("GlReversalJournalNo") + .HasMaxLength(30) + .HasColumnType("varchar(30)"); + + b.Property("Remarks") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("int unsigned"); + + b.Property("Status") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(20) + .HasColumnType("varchar(20)") + .HasDefaultValue("Posted"); + + b.Property("ToAccountNumber") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("ToBankAccountCode") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("varchar(20)"); + + b.Property("ToBankAccountName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("ToBankName") + .HasMaxLength(150) + .HasColumnType("varchar(150)"); + + b.Property("ToGlBankAccountId") + .HasColumnType("bigint"); + + b.HasKey("BankDepositId"); + + b.HasIndex("CreatedBy"); + + b.HasIndex("DocNo") + .IsUnique(); + + b.HasIndex("ToGlBankAccountId"); + + b.HasIndex("BranchId", "BusinessDate", "Status"); + + b.ToTable("bank_deposits", null, t => + { + t.HasCheckConstraint("ck_bank_deposits_amount", "`Amount` > 0"); + }); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.Batch", b => + { + b.Property("BatchId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("BatchId")); + + b.Property("Barcode") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("BatchNo") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("ExpiryDate") + .HasColumnType("date"); + + b.Property("ItemId") + .HasColumnType("int"); + + b.HasKey("BatchId"); + + b.HasIndex("Barcode") + .IsUnique(); + + b.HasIndex("ItemId", "BatchNo") + .IsUnique(); + + b.ToTable("batches", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.Bin", b => + { + b.Property("BinId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("BinId")); + + b.Property("BinType") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("WarehouseId") + .HasColumnType("int"); + + b.HasKey("BinId"); + + b.HasIndex("WarehouseId", "Code") + .IsUnique(); + + b.ToTable("bins", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.Branch", b => + { + b.Property("BranchId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("BranchId")); + + b.Property("Address") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("varchar(20)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("PhoneNumber") + .HasMaxLength(30) + .HasColumnType("varchar(30)"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("int unsigned"); + + b.Property("Status") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(20) + .HasColumnType("varchar(20)") + .HasDefaultValue("Active"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("BranchId"); + + b.HasIndex("Code") + .IsUnique(); + + b.HasIndex("Status"); + + b.ToTable("hr_branches", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.BranchConfig", b => + { + b.Property("ConfigId") + .HasColumnType("int"); + + b.Property("BranchingEnabled") + .ValueGeneratedOnAdd() + .HasColumnType("tinyint(1)") + .HasDefaultValue(true); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("int unsigned"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.Property("UpdatedBy") + .HasColumnType("int"); + + b.Property("WarehouseBranchingEnabled") + .HasColumnType("tinyint(1)"); + + b.HasKey("ConfigId"); + + b.HasIndex("UpdatedBy"); + + b.ToTable("branch_config", null, t => + { + t.HasCheckConstraint("ck_branch_config_singleton", "`ConfigId` = 1"); + }); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.Brand", b => + { + b.Property("BrandId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("BrandId")); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("int unsigned"); + + b.Property("Status") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(20) + .HasColumnType("varchar(20)") + .HasDefaultValue("Active"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("BrandId"); + + b.HasIndex("Name") + .IsUnique(); + + b.HasIndex("Status"); + + b.ToTable("brands", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.BundleSale", b => + { + b.Property("BundleSaleId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("BundleSaleId")); + + b.Property("BundleCode") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("BundleDate") + .HasColumnType("datetime(6)"); + + b.Property("BundleName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("BundleNo") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("BundlePrice") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("BundleSaleTemplateId") + .HasColumnType("int"); + + b.Property("CardAmount") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("CashAmount") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("CashierUserId") + .HasColumnType("int"); + + b.Property("ComponentSubtotal") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("CustomerId") + .HasColumnType("int"); + + b.Property("CustomerSnapshotName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("DayEndId") + .HasColumnType("int"); + + b.Property("DiscountTotal") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("GrandTotal") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("MarginAmount") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("int unsigned"); + + b.Property("Status") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(20) + .HasColumnType("varchar(20)") + .HasDefaultValue("Draft"); + + b.Property("TaxTotal") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.Property("WarehouseId") + .HasColumnType("int"); + + b.HasKey("BundleSaleId"); + + b.HasIndex("BundleNo") + .IsUnique(); + + b.HasIndex("BundleSaleTemplateId"); + + b.HasIndex("CashierUserId"); + + b.HasIndex("CustomerId"); + + b.HasIndex("DayEndId"); + + b.HasIndex("WarehouseId"); + + b.ToTable("bundle_sales", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.BundleSaleLine", b => + { + b.Property("BundleSaleLineId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("BundleSaleLineId")); + + b.Property("BundleSaleId") + .HasColumnType("int"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("IncludeInBundle") + .ValueGeneratedOnAdd() + .HasColumnType("tinyint(1)") + .HasDefaultValue(true); + + b.Property("IsComponent") + .ValueGeneratedOnAdd() + .HasColumnType("tinyint(1)") + .HasDefaultValue(true); + + b.Property("ItemId") + .HasColumnType("int"); + + b.Property("LineTotal") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("ParentLineId") + .HasColumnType("int"); + + b.Property("Qty") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("UnitPrice") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("WarehouseId") + .HasColumnType("int"); + + b.HasKey("BundleSaleLineId"); + + b.HasIndex("BundleSaleId"); + + b.HasIndex("ItemId"); + + b.HasIndex("WarehouseId"); + + b.ToTable("bundle_sale_lines", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.BundleSaleTemplate", b => + { + b.Property("BundleSaleTemplateId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("BundleSaleTemplateId")); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Description") + .HasMaxLength(1000) + .HasColumnType("varchar(1000)"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("int unsigned"); + + b.Property("Status") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(20) + .HasColumnType("varchar(20)") + .HasDefaultValue("Active"); + + b.Property("TemplateCode") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("TemplateName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("BundleSaleTemplateId"); + + b.HasIndex("TemplateCode") + .IsUnique(); + + b.ToTable("bundle_sale_templates", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.BundleSaleTemplateLine", b => + { + b.Property("BundleSaleTemplateLineId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("BundleSaleTemplateLineId")); + + b.Property("BundleSaleTemplateId") + .HasColumnType("int"); + + b.Property("IncludeInBundle") + .HasColumnType("tinyint(1)"); + + b.Property("ItemId") + .HasColumnType("int"); + + b.Property("Qty") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("SortOrder") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasDefaultValue(0); + + b.Property("UnitPrice") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("WarehouseId") + .HasColumnType("int"); + + b.HasKey("BundleSaleTemplateLineId"); + + b.HasIndex("BundleSaleTemplateId"); + + b.HasIndex("ItemId"); + + b.HasIndex("WarehouseId"); + + b.ToTable("bundle_sale_template_lines", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.Category", b => + { + b.Property("CategoryId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("CategoryId")); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("int unsigned"); + + b.Property("Status") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(20) + .HasColumnType("varchar(20)") + .HasDefaultValue("Active"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("CategoryId"); + + b.HasIndex("Name") + .IsUnique(); + + b.HasIndex("Status"); + + b.ToTable("categories", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.Customer", b => + { + b.Property("CustomerId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("CustomerId")); + + b.Property("AddressLine1") + .HasMaxLength(250) + .HasColumnType("varchar(250)"); + + b.Property("AddressLine2") + .HasMaxLength(250) + .HasColumnType("varchar(250)"); + + b.Property("BranchId") + .HasColumnType("int"); + + b.Property("City") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("Country") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("CreditDays") + .HasColumnType("int"); + + b.Property("CreditLimit") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("CustomerCode") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("CustomerType") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(20) + .HasColumnType("varchar(20)") + .HasDefaultValue("B2C"); + + b.Property("DefaultWarehouseId") + .HasColumnType("int"); + + b.Property("DisplayName") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("Email") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("Phone") + .HasMaxLength(30) + .HasColumnType("varchar(30)"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("int unsigned"); + + b.Property("Status") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(20) + .HasColumnType("varchar(20)") + .HasDefaultValue("Active"); + + b.Property("TaxRegistrationNo") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("CustomerId"); + + b.HasIndex("BranchId"); + + b.HasIndex("CustomerCode") + .IsUnique(); + + b.HasIndex("CustomerType"); + + b.HasIndex("DefaultWarehouseId"); + + b.HasIndex("Status"); + + b.ToTable("customers", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.Department", b => + { + b.Property("DepartmentId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("DepartmentId")); + + b.Property("BranchId") + .HasColumnType("int"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("varchar(20)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("HeadEmployeeId") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("ParentDepartmentId") + .HasColumnType("int"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("int unsigned"); + + b.Property("Status") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(20) + .HasColumnType("varchar(20)") + .HasDefaultValue("Active"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("DepartmentId"); + + b.HasIndex("BranchId"); + + b.HasIndex("Code") + .IsUnique(); + + b.HasIndex("HeadEmployeeId"); + + b.HasIndex("ParentDepartmentId"); + + b.HasIndex("Status"); + + b.ToTable("hr_departments", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.Designation", b => + { + b.Property("DesignationId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("DesignationId")); + + b.Property("Code") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("varchar(20)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("int unsigned"); + + b.Property("Status") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(20) + .HasColumnType("varchar(20)") + .HasDefaultValue("Active"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("DesignationId"); + + b.HasIndex("Code") + .IsUnique(); + + b.HasIndex("Status"); + + b.ToTable("hr_designations", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.DocumentApproval", b => + { + b.Property("DocumentApprovalId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("DocumentApprovalId")); + + b.Property("Comment") + .HasMaxLength(250) + .HasColumnType("varchar(250)"); + + b.Property("DecidedAt") + .HasColumnType("datetime(6)"); + + b.Property("DecidedBy") + .HasColumnType("int"); + + b.Property("Decision") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("varchar(20)"); + + b.Property("DocId") + .HasColumnType("int"); + + b.Property("DocType") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("varchar(10)"); + + b.Property("Level") + .HasColumnType("int"); + + b.HasKey("DocumentApprovalId"); + + b.HasIndex("DecidedBy"); + + b.HasIndex("DocType", "DocId"); + + b.ToTable("document_approvals", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.Employee", b => + { + b.Property("EmployeeId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("EmployeeId")); + + b.Property("AddressLine1") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("AddressLine2") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("BranchId") + .HasColumnType("int"); + + b.Property("City") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("ConfirmationDate") + .HasColumnType("datetime(6)"); + + b.Property("Country") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("CreatedBy") + .HasColumnType("int"); + + b.Property("DateOfBirth") + .HasColumnType("datetime(6)"); + + b.Property("DepartmentId") + .HasColumnType("int"); + + b.Property("DesignationId") + .HasColumnType("int"); + + b.Property("Email") + .HasMaxLength(320) + .HasColumnType("varchar(320)"); + + b.Property("EmergencyContactName") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("EmergencyContactPhone") + .HasMaxLength(30) + .HasColumnType("varchar(30)"); + + b.Property("EmergencyContactRelationship") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("EmployeeCode") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("varchar(30)"); + + b.Property("EmploymentTypeId") + .HasColumnType("int"); + + b.Property("EpfNumber") + .HasMaxLength(30) + .HasColumnType("varchar(30)"); + + b.Property("EtfNumber") + .HasMaxLength(30) + .HasColumnType("varchar(30)"); + + b.Property("FullName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("Gender") + .HasMaxLength(20) + .HasColumnType("varchar(20)"); + + b.Property("HireDate") + .HasColumnType("datetime(6)"); + + b.Property("LastWorkingDate") + .HasColumnType("datetime(6)"); + + b.Property("Nationality") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("Nic") + .HasMaxLength(30) + .HasColumnType("varchar(30)"); + + b.Property("PersonalMobile") + .HasMaxLength(30) + .HasColumnType("varchar(30)"); + + b.Property("PostalCode") + .HasMaxLength(20) + .HasColumnType("varchar(20)"); + + b.Property("ProfilePhotoPath") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("ReportingManagerId") + .HasColumnType("int"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("int unsigned"); + + b.Property("Status") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(20) + .HasColumnType("varchar(20)") + .HasDefaultValue("Active"); + + b.Property("TaxIdentificationNumber") + .HasMaxLength(30) + .HasColumnType("varchar(30)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.Property("UpdatedBy") + .HasColumnType("int"); + + b.Property("UserId") + .HasColumnType("int"); + + b.Property("WorkShiftId") + .HasColumnType("int"); + + b.HasKey("EmployeeId"); + + b.HasIndex("BranchId"); + + b.HasIndex("DepartmentId"); + + b.HasIndex("DesignationId"); + + b.HasIndex("Email"); + + b.HasIndex("EmployeeCode") + .IsUnique(); + + b.HasIndex("EmploymentTypeId"); + + b.HasIndex("ReportingManagerId"); + + b.HasIndex("Status"); + + b.HasIndex("UserId") + .IsUnique(); + + b.HasIndex("WorkShiftId"); + + b.ToTable("hr_employees", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.EmployeeBankDetail", b => + { + b.Property("EmployeeBankDetailId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("EmployeeBankDetailId")); + + b.Property("AccountHolderName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("AccountNumber") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("BankName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("BranchName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("EmployeeId") + .HasColumnType("int"); + + b.Property("IsPrimary") + .HasColumnType("tinyint(1)"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("int unsigned"); + + b.Property("Status") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(20) + .HasColumnType("varchar(20)") + .HasDefaultValue("Active"); + + b.Property("SwiftCode") + .HasMaxLength(20) + .HasColumnType("varchar(20)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("EmployeeBankDetailId"); + + b.HasIndex("EmployeeId"); + + b.ToTable("hr_employee_bank_details", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.EmployeeDocument", b => + { + b.Property("EmployeeDocumentId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("EmployeeDocumentId")); + + b.Property("ContentType") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("EmployeeId") + .HasColumnType("int"); + + b.Property("ExpiryDate") + .HasColumnType("datetime(6)"); + + b.Property("HrDocumentTypeId") + .HasColumnType("int"); + + b.Property("IssueDate") + .HasColumnType("datetime(6)"); + + b.Property("Notes") + .HasMaxLength(1000) + .HasColumnType("varchar(1000)"); + + b.Property("OriginalFileName") + .IsRequired() + .HasMaxLength(260) + .HasColumnType("varchar(260)"); + + b.Property("RelativePath") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("int unsigned"); + + b.Property("SizeBytes") + .HasColumnType("bigint"); + + b.Property("Status") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(20) + .HasColumnType("varchar(20)") + .HasDefaultValue("Active"); + + b.Property("StoredFileName") + .IsRequired() + .HasMaxLength(260) + .HasColumnType("varchar(260)"); + + b.Property("UploadedAt") + .HasColumnType("datetime(6)"); + + b.Property("UploadedBy") + .HasColumnType("int"); + + b.Property("VerifiedAt") + .HasColumnType("datetime(6)"); + + b.Property("VerifiedBy") + .HasColumnType("int"); + + b.HasKey("EmployeeDocumentId"); + + b.HasIndex("EmployeeId"); + + b.HasIndex("ExpiryDate"); + + b.HasIndex("HrDocumentTypeId"); + + b.ToTable("hr_employee_documents", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.EmployeeLoan", b => + { + b.Property("EmployeeLoanId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("EmployeeLoanId")); + + b.Property("ApprovedAt") + .HasColumnType("datetime(6)"); + + b.Property("ApprovedBy") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("CreatedBy") + .HasColumnType("int"); + + b.Property("DocNo") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("varchar(30)"); + + b.Property("EmployeeId") + .HasColumnType("int"); + + b.Property("InstallmentAmount") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("InterestRate") + .HasPrecision(6, 4) + .HasColumnType("decimal(6,4)"); + + b.Property("LoanKind") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("varchar(20)"); + + b.Property("NumberOfInstallments") + .HasColumnType("int"); + + b.Property("OutstandingBalance") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("PrincipalAmount") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("int unsigned"); + + b.Property("StartMonth") + .HasColumnType("int"); + + b.Property("StartYear") + .HasColumnType("int"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("varchar(20)"); + + b.HasKey("EmployeeLoanId"); + + b.HasIndex("DocNo") + .IsUnique(); + + b.HasIndex("EmployeeId"); + + b.HasIndex("Status"); + + b.ToTable("hr_employee_loans", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.EmployeeSalaryStructure", b => + { + b.Property("EmployeeSalaryStructureId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("EmployeeSalaryStructureId")); + + b.Property("ApprovedAt") + .HasColumnType("datetime(6)"); + + b.Property("ApprovedBy") + .HasColumnType("int"); + + b.Property("BasicSalary") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("CreatedBy") + .HasColumnType("int"); + + b.Property("Currency") + .IsRequired() + .HasMaxLength(3) + .HasColumnType("varchar(3)"); + + b.Property("EffectiveFrom") + .HasColumnType("datetime(6)"); + + b.Property("EffectiveTo") + .HasColumnType("datetime(6)"); + + b.Property("EmployeeId") + .HasColumnType("int"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("int unsigned"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("varchar(20)"); + + b.HasKey("EmployeeSalaryStructureId"); + + b.HasIndex("EmployeeId", "EffectiveTo"); + + b.ToTable("hr_employee_salary_structures", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.EmployeeSalaryStructureLine", b => + { + b.Property("EmployeeSalaryStructureLineId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("EmployeeSalaryStructureLineId")); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("EmployeeSalaryStructureId") + .HasColumnType("int"); + + b.Property("SalaryComponentId") + .HasColumnType("int"); + + b.HasKey("EmployeeSalaryStructureLineId"); + + b.HasIndex("EmployeeSalaryStructureId"); + + b.HasIndex("SalaryComponentId"); + + b.ToTable("hr_employee_salary_structure_lines", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.EmploymentType", b => + { + b.Property("EmploymentTypeId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("EmploymentTypeId")); + + b.Property("Code") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("varchar(20)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("int unsigned"); + + b.Property("Status") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(20) + .HasColumnType("varchar(20)") + .HasDefaultValue("Active"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("EmploymentTypeId"); + + b.HasIndex("Code") + .IsUnique(); + + b.HasIndex("Status"); + + b.ToTable("hr_employment_types", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.Expense", b => + { + b.Property("ExpenseId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("ExpenseId")); + + b.Property("Amount") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("BalanceAmount") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("BillReference") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("BranchId") + .HasColumnType("int"); + + b.Property("CancelReason") + .HasMaxLength(250) + .HasColumnType("varchar(250)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("CreatedBy") + .HasColumnType("int"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("DocNo") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("varchar(30)"); + + b.Property("DueDate") + .HasColumnType("date"); + + b.Property("ExpenseCategoryId") + .HasColumnType("int"); + + b.Property("ExpenseDate") + .HasColumnType("date"); + + b.Property("ExpenseTitleId") + .HasColumnType("int"); + + b.Property("GlExpenseAccountCode") + .HasMaxLength(20) + .HasColumnType("varchar(20)"); + + b.Property("GlJournalNo") + .HasMaxLength(30) + .HasColumnType("varchar(30)"); + + b.Property("GlPostedAt") + .HasColumnType("datetime(6)"); + + b.Property("GlReversalJournalNo") + .HasMaxLength(30) + .HasColumnType("varchar(30)"); + + b.Property("PaidAmount") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("Payee") + .HasMaxLength(150) + .HasColumnType("varchar(150)"); + + b.Property("PaymentStatus") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(20) + .HasColumnType("varchar(20)") + .HasDefaultValue("Unpaid"); + + b.Property("PeriodMonth") + .HasMaxLength(7) + .HasColumnType("varchar(7)"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("int unsigned"); + + b.Property("Status") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(20) + .HasColumnType("varchar(20)") + .HasDefaultValue("Draft"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.Property("VendorId") + .HasColumnType("int"); + + b.HasKey("ExpenseId"); + + b.HasIndex("BranchId"); + + b.HasIndex("CreatedBy"); + + b.HasIndex("DocNo") + .IsUnique(); + + b.HasIndex("ExpenseCategoryId"); + + b.HasIndex("ExpenseDate"); + + b.HasIndex("ExpenseTitleId"); + + b.HasIndex("PaymentStatus"); + + b.HasIndex("Status"); + + b.HasIndex("VendorId"); + + b.ToTable("expenses", null, t => + { + t.HasCheckConstraint("ck_expenses_amount", "`Amount` > 0"); + }); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.ExpenseCategory", b => + { + b.Property("ExpenseCategoryId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("ExpenseCategoryId")); + + b.Property("Code") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("varchar(30)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("IsSystem") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("int unsigned"); + + b.Property("SortOrder") + .HasColumnType("int"); + + b.Property("Status") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(20) + .HasColumnType("varchar(20)") + .HasDefaultValue("Active"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("ExpenseCategoryId"); + + b.HasIndex("Code") + .IsUnique(); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("expense_categories", (string)null); + + b.HasData( + new + { + ExpenseCategoryId = 1, + Code = "UTILITY", + CreatedAt = new DateTime(2026, 10, 7, 0, 0, 0, 0, DateTimeKind.Utc), + IsSystem = true, + Name = "Utility", + RowVersion = 1u, + SortOrder = 1, + Status = "Active" + }, + new + { + ExpenseCategoryId = 2, + Code = "RENTAL", + CreatedAt = new DateTime(2026, 10, 7, 0, 0, 0, 0, DateTimeKind.Utc), + IsSystem = true, + Name = "Rental", + RowVersion = 1u, + SortOrder = 2, + Status = "Active" + }, + new + { + ExpenseCategoryId = 3, + Code = "SALARIES", + CreatedAt = new DateTime(2026, 10, 7, 0, 0, 0, 0, DateTimeKind.Utc), + IsSystem = true, + Name = "Salaries", + RowVersion = 1u, + SortOrder = 3, + Status = "Active" + }, + new + { + ExpenseCategoryId = 4, + Code = "OTHER", + CreatedAt = new DateTime(2026, 10, 7, 0, 0, 0, 0, DateTimeKind.Utc), + IsSystem = true, + Name = "Other", + RowVersion = 1u, + SortOrder = 4, + Status = "Active" + }); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.ExpensePayment", b => + { + b.Property("ExpensePaymentId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("ExpensePaymentId")); + + b.Property("Amount") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("BankAccountName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("ChequeNo") + .HasMaxLength(30) + .HasColumnType("varchar(30)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("CreatedBy") + .HasColumnType("int"); + + b.Property("ExpenseId") + .HasColumnType("int"); + + b.Property("GlBankAccountId") + .HasColumnType("bigint"); + + b.Property("GlJournalNo") + .HasMaxLength(30) + .HasColumnType("varchar(30)"); + + b.Property("PaymentDate") + .HasColumnType("datetime(6)"); + + b.Property("PaymentMethod") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("varchar(20)"); + + b.Property("Reference") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.HasKey("ExpensePaymentId"); + + b.HasIndex("CreatedBy"); + + b.HasIndex("ExpenseId"); + + b.ToTable("expense_payments", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.ExpenseTitle", b => + { + b.Property("ExpenseTitleId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("ExpenseTitleId")); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("ExpenseCategoryId") + .HasColumnType("int"); + + b.Property("GlExpenseAccountCode") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("varchar(20)"); + + b.Property("IsSystem") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("int unsigned"); + + b.Property("SortOrder") + .HasColumnType("int"); + + b.Property("Status") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(20) + .HasColumnType("varchar(20)") + .HasDefaultValue("Active"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("ExpenseTitleId"); + + b.HasIndex("ExpenseCategoryId", "Name") + .IsUnique(); + + b.ToTable("expense_titles", (string)null); + + b.HasData( + new + { + ExpenseTitleId = 1, + CreatedAt = new DateTime(2026, 10, 7, 0, 0, 0, 0, DateTimeKind.Utc), + ExpenseCategoryId = 1, + GlExpenseAccountCode = "5200-01", + IsSystem = true, + Name = "Electricity", + RowVersion = 1u, + SortOrder = 1, + Status = "Active" + }, + new + { + ExpenseTitleId = 2, + CreatedAt = new DateTime(2026, 10, 7, 0, 0, 0, 0, DateTimeKind.Utc), + ExpenseCategoryId = 1, + GlExpenseAccountCode = "5200-01", + IsSystem = true, + Name = "Water", + RowVersion = 1u, + SortOrder = 2, + Status = "Active" + }, + new + { + ExpenseTitleId = 3, + CreatedAt = new DateTime(2026, 10, 7, 0, 0, 0, 0, DateTimeKind.Utc), + ExpenseCategoryId = 1, + GlExpenseAccountCode = "5200-01", + IsSystem = true, + Name = "Telephone", + RowVersion = 1u, + SortOrder = 3, + Status = "Active" + }, + new + { + ExpenseTitleId = 4, + CreatedAt = new DateTime(2026, 10, 7, 0, 0, 0, 0, DateTimeKind.Utc), + ExpenseCategoryId = 1, + GlExpenseAccountCode = "5200-01", + IsSystem = true, + Name = "Internet", + RowVersion = 1u, + SortOrder = 4, + Status = "Active" + }, + new + { + ExpenseTitleId = 5, + CreatedAt = new DateTime(2026, 10, 7, 0, 0, 0, 0, DateTimeKind.Utc), + ExpenseCategoryId = 1, + GlExpenseAccountCode = "5200-01", + IsSystem = true, + Name = "Gas", + RowVersion = 1u, + SortOrder = 5, + Status = "Active" + }, + new + { + ExpenseTitleId = 6, + CreatedAt = new DateTime(2026, 10, 7, 0, 0, 0, 0, DateTimeKind.Utc), + ExpenseCategoryId = 2, + GlExpenseAccountCode = "5200-02", + IsSystem = true, + Name = "Building Rent", + RowVersion = 1u, + SortOrder = 1, + Status = "Active" + }, + new + { + ExpenseTitleId = 7, + CreatedAt = new DateTime(2026, 10, 7, 0, 0, 0, 0, DateTimeKind.Utc), + ExpenseCategoryId = 2, + GlExpenseAccountCode = "5200-02", + IsSystem = true, + Name = "Vehicle Rent", + RowVersion = 1u, + SortOrder = 2, + Status = "Active" + }, + new + { + ExpenseTitleId = 8, + CreatedAt = new DateTime(2026, 10, 7, 0, 0, 0, 0, DateTimeKind.Utc), + ExpenseCategoryId = 2, + GlExpenseAccountCode = "5200-02", + IsSystem = true, + Name = "Equipment Rent", + RowVersion = 1u, + SortOrder = 3, + Status = "Active" + }, + new + { + ExpenseTitleId = 9, + CreatedAt = new DateTime(2026, 10, 7, 0, 0, 0, 0, DateTimeKind.Utc), + ExpenseCategoryId = 3, + GlExpenseAccountCode = "5200-03", + IsSystem = true, + Name = "Staff Salaries", + RowVersion = 1u, + SortOrder = 1, + Status = "Active" + }, + new + { + ExpenseTitleId = 10, + CreatedAt = new DateTime(2026, 10, 7, 0, 0, 0, 0, DateTimeKind.Utc), + ExpenseCategoryId = 3, + GlExpenseAccountCode = "5200-03", + IsSystem = true, + Name = "Wages", + RowVersion = 1u, + SortOrder = 2, + Status = "Active" + }, + new + { + ExpenseTitleId = 11, + CreatedAt = new DateTime(2026, 10, 7, 0, 0, 0, 0, DateTimeKind.Utc), + ExpenseCategoryId = 3, + GlExpenseAccountCode = "5200-03", + IsSystem = true, + Name = "Overtime", + RowVersion = 1u, + SortOrder = 3, + Status = "Active" + }, + new + { + ExpenseTitleId = 12, + CreatedAt = new DateTime(2026, 10, 7, 0, 0, 0, 0, DateTimeKind.Utc), + ExpenseCategoryId = 3, + GlExpenseAccountCode = "5200-03", + IsSystem = true, + Name = "Bonus", + RowVersion = 1u, + SortOrder = 4, + Status = "Active" + }, + new + { + ExpenseTitleId = 13, + CreatedAt = new DateTime(2026, 10, 7, 0, 0, 0, 0, DateTimeKind.Utc), + ExpenseCategoryId = 3, + GlExpenseAccountCode = "5200-03", + IsSystem = true, + Name = "EPF / ETF Contribution", + RowVersion = 1u, + SortOrder = 5, + Status = "Active" + }, + new + { + ExpenseTitleId = 14, + CreatedAt = new DateTime(2026, 10, 7, 0, 0, 0, 0, DateTimeKind.Utc), + ExpenseCategoryId = 4, + GlExpenseAccountCode = "5200-04", + IsSystem = true, + Name = "Stationery", + RowVersion = 1u, + SortOrder = 1, + Status = "Active" + }, + new + { + ExpenseTitleId = 15, + CreatedAt = new DateTime(2026, 10, 7, 0, 0, 0, 0, DateTimeKind.Utc), + ExpenseCategoryId = 4, + GlExpenseAccountCode = "5200-04", + IsSystem = true, + Name = "Transport", + RowVersion = 1u, + SortOrder = 2, + Status = "Active" + }, + new + { + ExpenseTitleId = 16, + CreatedAt = new DateTime(2026, 10, 7, 0, 0, 0, 0, DateTimeKind.Utc), + ExpenseCategoryId = 4, + GlExpenseAccountCode = "5200-04", + IsSystem = true, + Name = "Repairs & Maintenance", + RowVersion = 1u, + SortOrder = 3, + Status = "Active" + }, + new + { + ExpenseTitleId = 17, + CreatedAt = new DateTime(2026, 10, 7, 0, 0, 0, 0, DateTimeKind.Utc), + ExpenseCategoryId = 4, + GlExpenseAccountCode = "5200-04", + IsSystem = true, + Name = "Cleaning", + RowVersion = 1u, + SortOrder = 4, + Status = "Active" + }, + new + { + ExpenseTitleId = 18, + CreatedAt = new DateTime(2026, 10, 7, 0, 0, 0, 0, DateTimeKind.Utc), + ExpenseCategoryId = 4, + GlExpenseAccountCode = "5200-04", + IsSystem = true, + Name = "Miscellaneous", + RowVersion = 1u, + SortOrder = 5, + Status = "Active" + }); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.GlAccountConfig", b => + { + b.Property("ConfigId") + .HasColumnType("int"); + + b.Property("AccountsPayableCode") + .HasMaxLength(20) + .HasColumnType("varchar(20)"); + + b.Property("AccountsReceivableCode") + .HasMaxLength(20) + .HasColumnType("varchar(20)"); + + b.Property("AccruedExpensesCode") + .HasMaxLength(20) + .HasColumnType("varchar(20)"); + + b.Property("CashAccountCode") + .HasMaxLength(20) + .HasColumnType("varchar(20)"); + + b.Property("ClearingAccountCode") + .HasMaxLength(20) + .HasColumnType("varchar(20)"); + + b.Property("CogsAccountCode") + .HasMaxLength(20) + .HasColumnType("varchar(20)"); + + b.Property("InventoryAccountCode") + .HasMaxLength(20) + .HasColumnType("varchar(20)"); + + b.Property("InventoryGainAccountCode") + .HasMaxLength(20) + .HasColumnType("varchar(20)"); + + b.Property("InventoryLossAccountCode") + .HasMaxLength(20) + .HasColumnType("varchar(20)"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("int unsigned"); + + b.Property("SalesDiscountAccountCode") + .HasMaxLength(20) + .HasColumnType("varchar(20)"); + + b.Property("SalesRevenueAccountCode") + .HasMaxLength(20) + .HasColumnType("varchar(20)"); + + b.Property("TaxPayableAccountCode") + .HasMaxLength(20) + .HasColumnType("varchar(20)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.Property("UpdatedBy") + .HasColumnType("int"); + + b.Property("VatRecoverableAccountCode") + .HasMaxLength(20) + .HasColumnType("varchar(20)"); + + b.HasKey("ConfigId"); + + b.HasIndex("UpdatedBy"); + + b.ToTable("gl_account_config", null, t => + { + t.HasCheckConstraint("ck_gl_account_config_singleton", "`ConfigId` = 1"); + }); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.Grn", b => + { + b.Property("GrnId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("GrnId")); + + b.Property("BranchId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("CreatedBy") + .HasColumnType("int"); + + b.Property("DocNo") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("varchar(30)"); + + b.Property("GlJournalNo") + .HasMaxLength(30) + .HasColumnType("varchar(30)"); + + b.Property("GlPostedAt") + .HasColumnType("datetime(6)"); + + b.Property("PoId") + .HasColumnType("int"); + + b.Property("PostedAt") + .HasColumnType("datetime(6)"); + + b.Property("PurchaseInvoiceId") + .HasColumnType("int"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("int unsigned"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("varchar(20)"); + + b.Property("VendorId") + .HasColumnType("int"); + + b.Property("WarehouseId") + .HasColumnType("int"); + + b.HasKey("GrnId"); + + b.HasIndex("BranchId"); + + b.HasIndex("CreatedBy"); + + b.HasIndex("DocNo") + .IsUnique(); + + b.HasIndex("PoId"); + + b.HasIndex("PurchaseInvoiceId"); + + b.HasIndex("Status"); + + b.HasIndex("VendorId"); + + b.HasIndex("WarehouseId"); + + b.ToTable("grns", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.GrnConfig", b => + { + b.Property("ConfigId") + .HasColumnType("int"); + + b.Property("AllowDiscounts") + .ValueGeneratedOnAdd() + .HasColumnType("tinyint(1)") + .HasDefaultValue(true); + + b.Property("AllowVat") + .ValueGeneratedOnAdd() + .HasColumnType("tinyint(1)") + .HasDefaultValue(true); + + b.Property("AllowWarehouseSelection") + .ValueGeneratedOnAdd() + .HasColumnType("tinyint(1)") + .HasDefaultValue(true); + + b.Property("DefaultWarehouseId") + .HasColumnType("int"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("int unsigned"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.Property("UpdatedBy") + .HasColumnType("int"); + + b.HasKey("ConfigId"); + + b.HasIndex("DefaultWarehouseId"); + + b.HasIndex("UpdatedBy"); + + b.ToTable("grn_config", null, t => + { + t.HasCheckConstraint("ck_grn_config_singleton", "`ConfigId` = 1"); + }); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.GrnLine", b => + { + b.Property("GrnLineId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("GrnLineId")); + + b.Property("BatchId") + .HasColumnType("int"); + + b.Property("BinId") + .HasColumnType("int"); + + b.Property("DiscountPct") + .HasPrecision(9, 4) + .HasColumnType("decimal(9,4)"); + + b.Property("GrnId") + .HasColumnType("int"); + + b.Property("HoldStatus") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("varchar(20)"); + + b.Property("ItemId") + .HasColumnType("int"); + + b.Property("LineTotal") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("NetUnitCost") + .HasPrecision(18, 6) + .HasColumnType("decimal(18,6)"); + + b.Property("PoLineId") + .HasColumnType("int"); + + b.Property("PoUnitPrice") + .HasPrecision(18, 6) + .HasColumnType("decimal(18,6)"); + + b.Property("Qty") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("ReceivedValue") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("UnitCost") + .HasPrecision(18, 6) + .HasColumnType("decimal(18,6)"); + + b.Property("VatAmount") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("VatPct") + .HasPrecision(9, 4) + .HasColumnType("decimal(9,4)"); + + b.HasKey("GrnLineId"); + + b.HasIndex("BatchId"); + + b.HasIndex("BinId"); + + b.HasIndex("GrnId"); + + b.HasIndex("ItemId"); + + b.HasIndex("PoLineId"); + + b.ToTable("grn_lines", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.GrnLineSerial", b => + { + b.Property("GrnLineSerialId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("GrnLineSerialId")); + + b.Property("GrnLineId") + .HasColumnType("int"); + + b.Property("SerialId") + .HasColumnType("int"); + + b.Property("SerialNo") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.HasKey("GrnLineSerialId"); + + b.HasIndex("SerialId"); + + b.HasIndex("GrnLineId", "SerialNo") + .IsUnique(); + + b.ToTable("grn_line_serials", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.GrnLineWarrantyNumber", b => + { + b.Property("GrnLineWarrantyNumberId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("GrnLineWarrantyNumberId")); + + b.Property("GrnLineId") + .HasColumnType("int"); + + b.Property("WarrantyNo") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("WarrantyPeriodMonths") + .HasColumnType("int"); + + b.HasKey("GrnLineWarrantyNumberId"); + + b.HasIndex("GrnLineId", "WarrantyNo") + .IsUnique(); + + b.ToTable("grn_line_warranty_numbers", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.HrDocumentType", b => + { + b.Property("HrDocumentTypeId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("HrDocumentTypeId")); + + b.Property("Category") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("varchar(20)"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("varchar(20)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("ExpiryTracked") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("RequiredAtOnboarding") + .HasColumnType("tinyint(1)"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("int unsigned"); + + b.Property("Status") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(20) + .HasColumnType("varchar(20)") + .HasDefaultValue("Active"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("HrDocumentTypeId"); + + b.HasIndex("Code") + .IsUnique(); + + b.HasIndex("Status"); + + b.ToTable("hr_document_types", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.Item", b => + { + b.Property("ItemId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("ItemId")); + + b.Property("Barcode") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("BaseUomId") + .HasColumnType("int"); + + b.Property("BrandId") + .HasColumnType("int"); + + b.Property("CategoryId") + .HasColumnType("int"); + + b.Property("ContentBaseQty") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("ContentBaseUnit") + .HasMaxLength(20) + .HasColumnType("varchar(20)"); + + b.Property("ContentQty") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("ContentUnit") + .HasMaxLength(20) + .HasColumnType("varchar(20)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("DefaultVendorId") + .HasColumnType("int"); + + b.Property("Description") + .HasMaxLength(1000) + .HasColumnType("varchar(1000)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("int unsigned"); + + b.Property("SellingFlatPrice") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("SellingMaxPrice") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("SellingMinPrice") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("Sku") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Status") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(20) + .HasColumnType("varchar(20)") + .HasDefaultValue("Active"); + + b.Property("StockNature") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("varchar(20)"); + + b.Property("SubCategoryId") + .HasColumnType("int"); + + b.Property("TaxRateId") + .HasColumnType("int"); + + b.Property("TrackingMode") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("varchar(20)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Warranty") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(20) + .HasColumnType("varchar(20)") + .HasDefaultValue("NonWarranty"); + + b.Property("WarrantyPeriodMonths") + .HasColumnType("int"); + + b.HasKey("ItemId"); + + b.HasIndex("Barcode") + .IsUnique(); + + b.HasIndex("BaseUomId"); + + b.HasIndex("BrandId"); + + b.HasIndex("CategoryId"); + + b.HasIndex("DefaultVendorId"); + + b.HasIndex("Sku") + .IsUnique(); + + b.HasIndex("Status"); + + b.HasIndex("SubCategoryId"); + + b.HasIndex("TaxRateId"); + + b.ToTable("items", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.ItemImage", b => + { + b.Property("ItemImageId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("ItemImageId")); + + b.Property("ContentType") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("ItemId") + .HasColumnType("int"); + + b.Property("OriginalFileName") + .IsRequired() + .HasMaxLength(260) + .HasColumnType("varchar(260)"); + + b.Property("RelativePath") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("SizeBytes") + .HasColumnType("bigint"); + + b.Property("SortOrder") + .HasColumnType("int"); + + b.Property("StoredFileName") + .IsRequired() + .HasMaxLength(260) + .HasColumnType("varchar(260)"); + + b.Property("UploadedAt") + .HasColumnType("datetime(6)"); + + b.Property("UploadedBy") + .HasColumnType("int"); + + b.HasKey("ItemImageId"); + + b.HasIndex("ItemId", "SortOrder"); + + b.ToTable("item_images", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.ItemPriceHistory", b => + { + b.Property("ItemPriceHistoryId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("ItemPriceHistoryId")); + + b.Property("AppliedAt") + .HasColumnType("datetime(6)"); + + b.Property("ApprovedLevel1By") + .HasColumnType("int"); + + b.Property("ApprovedLevel2By") + .HasColumnType("int"); + + b.Property("ItemId") + .HasColumnType("int"); + + b.Property("NewFlatPrice") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("NewMaxPrice") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("NewMinPrice") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("OldFlatPrice") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("OldMaxPrice") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("OldMinPrice") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("PriceAdjustmentId") + .HasColumnType("int"); + + b.Property("RequestedBy") + .HasColumnType("int"); + + b.HasKey("ItemPriceHistoryId"); + + b.HasIndex("PriceAdjustmentId"); + + b.HasIndex("ItemId", "AppliedAt"); + + b.ToTable("item_price_history", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.ItemReorder", b => + { + b.Property("ReorderId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("ReorderId")); + + b.Property("ItemId") + .HasColumnType("int"); + + b.Property("ReorderPoint") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("ReorderQty") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("WarehouseId") + .HasColumnType("int"); + + b.HasKey("ReorderId"); + + b.HasIndex("WarehouseId"); + + b.HasIndex("ItemId", "WarehouseId") + .IsUnique(); + + b.ToTable("item_reorders", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.ItemType", b => + { + b.Property("ItemTypeId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("ItemTypeId")); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("IsMeasurable") + .ValueGeneratedOnAdd() + .HasColumnType("tinyint(1)") + .HasDefaultValue(false); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("int unsigned"); + + b.Property("Status") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(20) + .HasColumnType("varchar(20)") + .HasDefaultValue("Active"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("ItemTypeId"); + + b.HasIndex("Name") + .IsUnique(); + + b.HasIndex("Status"); + + b.ToTable("item_types", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.JournalEntryStub", b => + { + b.Property("JournalId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("JournalId")); + + b.Property("Amount") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("CreditAccount") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("varchar(20)"); + + b.Property("DebitAccount") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("varchar(20)"); + + b.Property("SourceDocId") + .HasColumnType("int"); + + b.Property("SourceDocType") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("varchar(10)"); + + b.HasKey("JournalId"); + + b.HasIndex("SourceDocType", "SourceDocId"); + + b.ToTable("journal_entry_stubs", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.LeaveBalance", b => + { + b.Property("LeaveBalanceId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("LeaveBalanceId")); + + b.Property("AdjustmentDays") + .HasPrecision(6, 2) + .HasColumnType("decimal(6,2)"); + + b.Property("CarriedForwardDays") + .HasPrecision(6, 2) + .HasColumnType("decimal(6,2)"); + + b.Property("EmployeeId") + .HasColumnType("int"); + + b.Property("EntitledDays") + .HasPrecision(6, 2) + .HasColumnType("decimal(6,2)"); + + b.Property("LeaveTypeId") + .HasColumnType("int"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("int unsigned"); + + b.Property("TakenDays") + .HasPrecision(6, 2) + .HasColumnType("decimal(6,2)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Year") + .HasColumnType("int"); + + b.HasKey("LeaveBalanceId"); + + b.HasIndex("LeaveTypeId"); + + b.HasIndex("EmployeeId", "LeaveTypeId", "Year") + .IsUnique(); + + b.ToTable("hr_leave_balances", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.LeaveRequest", b => + { + b.Property("LeaveRequestId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("LeaveRequestId")); + + b.Property("ApprovedAt") + .HasColumnType("datetime(6)"); + + b.Property("ApprovedBy") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("CreatedBy") + .HasColumnType("int"); + + b.Property("DaysCount") + .HasPrecision(6, 2) + .HasColumnType("decimal(6,2)"); + + b.Property("DocNo") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("varchar(30)"); + + b.Property("EmployeeId") + .HasColumnType("int"); + + b.Property("EndDate") + .HasColumnType("datetime(6)"); + + b.Property("LeaveTypeId") + .HasColumnType("int"); + + b.Property("Reason") + .HasMaxLength(1000) + .HasColumnType("varchar(1000)"); + + b.Property("RejectionReason") + .HasMaxLength(1000) + .HasColumnType("varchar(1000)"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("int unsigned"); + + b.Property("StartDate") + .HasColumnType("datetime(6)"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("varchar(20)"); + + b.HasKey("LeaveRequestId"); + + b.HasIndex("DocNo") + .IsUnique(); + + b.HasIndex("EmployeeId"); + + b.HasIndex("LeaveTypeId"); + + b.HasIndex("Status"); + + b.HasIndex("StartDate", "EndDate"); + + b.ToTable("hr_leave_requests", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.LeaveType", b => + { + b.Property("LeaveTypeId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("LeaveTypeId")); + + b.Property("AccrualPerYear") + .HasPrecision(6, 2) + .HasColumnType("decimal(6,2)"); + + b.Property("CarryForwardAllowed") + .HasColumnType("tinyint(1)"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("varchar(20)"); + + b.Property("CountsAsNoPay") + .HasColumnType("tinyint(1)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("IsPaid") + .HasColumnType("tinyint(1)"); + + b.Property("MaxCarryForwardDays") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("RequiresApproval") + .HasColumnType("tinyint(1)"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("int unsigned"); + + b.Property("Status") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(20) + .HasColumnType("varchar(20)") + .HasDefaultValue("Active"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("LeaveTypeId"); + + b.HasIndex("Code") + .IsUnique(); + + b.HasIndex("Status"); + + b.ToTable("hr_leave_types", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.LoanInstallment", b => + { + b.Property("LoanInstallmentId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("LoanInstallmentId")); + + b.Property("DueMonth") + .HasColumnType("int"); + + b.Property("DueYear") + .HasColumnType("int"); + + b.Property("EmployeeLoanId") + .HasColumnType("int"); + + b.Property("InstallmentNumber") + .HasColumnType("int"); + + b.Property("PaidAmount") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("PayrollRunId") + .HasColumnType("int"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("int unsigned"); + + b.Property("ScheduledAmount") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("varchar(20)"); + + b.HasKey("LoanInstallmentId"); + + b.HasIndex("EmployeeLoanId"); + + b.HasIndex("PayrollRunId"); + + b.HasIndex("DueYear", "DueMonth"); + + b.ToTable("hr_loan_installments", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.LoginActivity", b => + { + b.Property("LoginActivityId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("LoginActivityId")); + + b.Property("DeviceLabel") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("IpAddress") + .HasMaxLength(64) + .HasColumnType("varchar(64)"); + + b.Property("LoginAt") + .HasColumnType("datetime(6)"); + + b.Property("UserAgent") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("LoginActivityId"); + + b.HasIndex("UserId", "LoginAt"); + + b.ToTable("login_activities", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.NavItem", b => + { + b.Property("NavItemId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("NavItemId")); + + b.Property("Code") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Href") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("Icon") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Label") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("SortOrder") + .HasColumnType("int"); + + b.Property("Status") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(20) + .HasColumnType("varchar(20)") + .HasDefaultValue("Active"); + + b.HasKey("NavItemId"); + + b.HasIndex("Code") + .IsUnique(); + + b.ToTable("nav_items", (string)null); + + b.HasData( + new + { + NavItemId = 1, + Code = "dashboard", + Href = "/dashboard", + Label = "Dashboard", + SortOrder = 1, + Status = "Active" + }, + new + { + NavItemId = 2, + Code = "products", + Href = "/dashboard/products", + Label = "Products", + SortOrder = 2, + Status = "Active" + }, + new + { + NavItemId = 4, + Code = "procurement", + Href = "/dashboard/procurement", + Label = "Procurement", + SortOrder = 4, + Status = "Active" + }, + new + { + NavItemId = 5, + Code = "receiving", + Href = "/dashboard/receiving/grn", + Label = "Receiving", + SortOrder = 5, + Status = "Active" + }, + new + { + NavItemId = 6, + Code = "stock", + Href = "/dashboard/stock", + Label = "Stock", + SortOrder = 6, + Status = "Active" + }, + new + { + NavItemId = 7, + Code = "warehouses", + Href = "/dashboard/warehouse", + Label = "Warehouses", + SortOrder = 7, + Status = "Active" + }, + new + { + NavItemId = 8, + Code = "orders", + Href = "/dashboard/orders", + Label = "Orders", + SortOrder = 8, + Status = "Active" + }, + new + { + NavItemId = 9, + Code = "settings", + Href = "/dashboard/settings", + Label = "Settings", + SortOrder = 9, + Status = "Active" + }, + new + { + NavItemId = 10, + Code = "help", + Href = "/dashboard/help", + Label = "Help", + SortOrder = 10, + Status = "Active" + }, + new + { + NavItemId = 11, + Code = "ledgers", + Href = "/dashboard/ledgers", + Label = "Ledgers", + SortOrder = 11, + Status = "Active" + }, + new + { + NavItemId = 12, + Code = "accounts", + Href = "/dashboard/accounts", + Label = "Accounts", + SortOrder = 12, + Status = "Active" + }, + new + { + NavItemId = 13, + Code = "sales", + Href = "/dashboard/sales", + Label = "Sales", + SortOrder = 13, + Status = "Active" + }, + new + { + NavItemId = 14, + Code = "production", + Href = "/dashboard/production", + Label = "Production", + SortOrder = 14, + Status = "Active" + }, + new + { + NavItemId = 15, + Code = "hrm", + Href = "/dashboard/hrm", + Label = "Human Resources", + SortOrder = 15, + Status = "Active" + }, + new + { + NavItemId = 16, + Code = "sales-returns", + Href = "/dashboard/sales/sales-returns", + Label = "Sales Returns", + SortOrder = 16, + Status = "Active" + }); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.Notification", b => + { + b.Property("NotificationId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("NotificationId")); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Message") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("varchar(1000)"); + + b.Property("ModuleKey") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("RelatedEntityId") + .HasColumnType("int"); + + b.Property("RelatedEntityType") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.HasKey("NotificationId"); + + b.HasIndex("CreatedAt"); + + b.ToTable("notifications", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.NotificationConfig", b => + { + b.Property("NotificationConfigId") + .HasColumnType("int"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("int unsigned"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.Property("UpdatedBy") + .HasColumnType("int"); + + b.HasKey("NotificationConfigId"); + + b.HasIndex("UpdatedBy"); + + b.ToTable("notification_config", null, t => + { + t.HasCheckConstraint("ck_notification_config_singleton", "`NotificationConfigId` = 1"); + }); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.NotificationConfigModule", b => + { + b.Property("NotificationConfigModuleId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("NotificationConfigModuleId")); + + b.Property("EmailEnabled") + .ValueGeneratedOnAdd() + .HasColumnType("tinyint(1)") + .HasDefaultValue(false); + + b.Property("InAppEnabled") + .ValueGeneratedOnAdd() + .HasColumnType("tinyint(1)") + .HasDefaultValue(false); + + b.Property("ModuleKey") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("NotificationConfigId") + .HasColumnType("int"); + + b.Property("SmsEnabled") + .ValueGeneratedOnAdd() + .HasColumnType("tinyint(1)") + .HasDefaultValue(false); + + b.HasKey("NotificationConfigModuleId"); + + b.HasIndex("ModuleKey") + .IsUnique(); + + b.HasIndex("NotificationConfigId"); + + b.ToTable("notification_config_modules", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.NotificationRecipient", b => + { + b.Property("NotificationRecipientId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("NotificationRecipientId")); + + b.Property("IsRead") + .HasColumnType("tinyint(1)"); + + b.Property("NotificationId") + .HasColumnType("int"); + + b.Property("ReadAt") + .HasColumnType("datetime(6)"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("NotificationRecipientId"); + + b.HasIndex("NotificationId"); + + b.HasIndex("UserId", "IsRead", "NotificationId"); + + b.ToTable("notification_recipients", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.NumberSequence", b => + { + b.Property("SequenceId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("SequenceId")); + + b.Property("BranchId") + .HasColumnType("int") + .HasColumnName("branch_id"); + + b.Property("DocType") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("varchar(10)") + .HasColumnName("doc_type"); + + b.Property("LastNumber") + .HasColumnType("int") + .HasColumnName("last_number"); + + b.Property("Year") + .HasColumnType("int") + .HasColumnName("year"); + + b.HasKey("SequenceId"); + + b.HasIndex("BranchId"); + + b.HasIndex("DocType", "BranchId", "Year") + .IsUnique(); + + b.ToTable("number_sequences", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.OpeningBalanceLine", b => + { + b.Property("OpeningBalanceLineId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("OpeningBalanceLineId")); + + b.Property("AccountCode") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("varchar(20)"); + + b.Property("AccountName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("Amount") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("OpeningBalanceRunId") + .HasColumnType("int"); + + b.HasKey("OpeningBalanceLineId"); + + b.HasIndex("OpeningBalanceRunId"); + + b.ToTable("opening_balance_lines", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.OpeningBalanceRun", b => + { + b.Property("OpeningBalanceRunId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("OpeningBalanceRunId")); + + b.Property("Currency") + .IsRequired() + .HasMaxLength(3) + .HasColumnType("varchar(3)"); + + b.Property("CutoverDate") + .HasColumnType("date"); + + b.Property("GlJournalNo") + .HasMaxLength(30) + .HasColumnType("varchar(30)"); + + b.Property("Kind") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("varchar(20)"); + + b.Property("OnceKey") + .HasMaxLength(20) + .HasColumnType("varchar(20)"); + + b.Property("PostedAt") + .HasColumnType("datetime(6)"); + + b.Property("PostedBy") + .HasColumnType("int"); + + b.Property("TotalAmount") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.HasKey("OpeningBalanceRunId"); + + b.HasIndex("Kind"); + + b.HasIndex("OnceKey") + .IsUnique(); + + b.ToTable("opening_balance_runs", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.OpeningStockLine", b => + { + b.Property("OpeningStockLineId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("OpeningStockLineId")); + + b.Property("ItemId") + .HasColumnType("int"); + + b.Property("OpeningBalanceRunId") + .HasColumnType("int"); + + b.Property("Qty") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("UnitCost") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("Value") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("WarehouseId") + .HasColumnType("int"); + + b.HasKey("OpeningStockLineId"); + + b.HasIndex("OpeningBalanceRunId"); + + b.HasIndex("WarehouseId"); + + b.HasIndex("ItemId", "WarehouseId"); + + b.ToTable("opening_stock_lines", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.OwnerReportConfig", b => + { + b.Property("ConfigId") + .HasColumnType("int"); + + b.Property("EmailEnabled") + .ValueGeneratedOnAdd() + .HasColumnType("tinyint(1)") + .HasDefaultValue(true); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("tinyint(1)") + .HasDefaultValue(false); + + b.Property("IncludeBranchBreakdown") + .ValueGeneratedOnAdd() + .HasColumnType("tinyint(1)") + .HasDefaultValue(true); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("int unsigned"); + + b.Property("SendAtLocalTime") + .HasColumnType("time(6)"); + + b.Property("SmsEnabled") + .ValueGeneratedOnAdd() + .HasColumnType("tinyint(1)") + .HasDefaultValue(true); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.Property("UpdatedBy") + .HasColumnType("int"); + + b.HasKey("ConfigId"); + + b.HasIndex("UpdatedBy"); + + b.ToTable("owner_report_config", null, t => + { + t.HasCheckConstraint("ck_owner_report_config_singleton", "`ConfigId` = 1"); + }); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.OwnerReportDispatch", b => + { + b.Property("DispatchId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("DispatchId")); + + b.Property("Attempts") + .HasColumnType("int"); + + b.Property("BusinessDate") + .HasColumnType("date"); + + b.Property("Channel") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("varchar(10)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("DedupeKey") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("varchar(40)"); + + b.Property("Destination") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("Error") + .HasMaxLength(1000) + .HasColumnType("varchar(1000)"); + + b.Property("IsTest") + .HasColumnType("tinyint(1)"); + + b.Property("NextAttemptAt") + .HasColumnType("datetime(6)"); + + b.Property("RecipientId") + .HasColumnType("int"); + + b.Property("RecipientName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("SentAt") + .HasColumnType("datetime(6)"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("varchar(10)"); + + b.HasKey("DispatchId"); + + b.HasIndex("CreatedAt"); + + b.HasIndex("RecipientId"); + + b.HasIndex("Status", "NextAttemptAt"); + + b.HasIndex("BusinessDate", "Channel", "RecipientId", "IsTest", "DedupeKey") + .IsUnique() + .HasDatabaseName("ux_owner_report_dispatch_once"); + + b.ToTable("owner_report_dispatches", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.OwnerReportRecipient", b => + { + b.Property("RecipientId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("RecipientId")); + + b.Property("Email") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("IsActive") + .ValueGeneratedOnAdd() + .HasColumnType("tinyint(1)") + .HasDefaultValue(true); + + b.Property("Mobile") + .HasMaxLength(20) + .HasColumnType("varchar(20)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.HasKey("RecipientId"); + + b.ToTable("owner_report_recipients", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.PayrollLine", b => + { + b.Property("PayrollLineId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("PayrollLineId")); + + b.Property("AbsentDays") + .HasColumnType("int"); + + b.Property("BasicSalary") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("EmployeeId") + .HasColumnType("int"); + + b.Property("EpfEmployeeAmount") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("EpfEmployerAmount") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("EtfEmployerAmount") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("GrossSalary") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("LateDeductionAmount") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("LateMinutesTotal") + .HasColumnType("int"); + + b.Property("LeaveDays") + .HasColumnType("int"); + + b.Property("LoanDeductionAmount") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("NetSalary") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("NoPayAmount") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("OtMinutesTotal") + .HasColumnType("int"); + + b.Property("OtherDeductionsAmount") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("OvertimeAmount") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("PayrollRunId") + .HasColumnType("int"); + + b.Property("PresentDays") + .HasColumnType("int"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("int unsigned"); + + b.Property("TaxAmount") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("TotalAllowances") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("WorkingDays") + .HasColumnType("int"); + + b.HasKey("PayrollLineId"); + + b.HasIndex("EmployeeId"); + + b.HasIndex("PayrollRunId", "EmployeeId") + .IsUnique(); + + b.ToTable("hr_payroll_lines", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.PayrollLineComponent", b => + { + b.Property("PayrollLineComponentId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("PayrollLineComponentId")); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("ComponentCategory") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("varchar(20)"); + + b.Property("Label") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("PayrollLineId") + .HasColumnType("int"); + + b.Property("SalaryComponentId") + .HasColumnType("int"); + + b.Property("SortOrder") + .HasColumnType("int"); + + b.HasKey("PayrollLineComponentId"); + + b.HasIndex("PayrollLineId"); + + b.HasIndex("SalaryComponentId"); + + b.ToTable("hr_payroll_line_components", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.PayrollRun", b => + { + b.Property("PayrollRunId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("PayrollRunId")); + + b.Property("ApprovedAt") + .HasColumnType("datetime(6)"); + + b.Property("ApprovedBy") + .HasColumnType("int"); + + b.Property("BranchId") + .HasColumnType("int"); + + b.Property("DocNo") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("varchar(30)"); + + b.Property("GeneratedAt") + .HasColumnType("datetime(6)"); + + b.Property("GeneratedBy") + .HasColumnType("int"); + + b.Property("LockedAt") + .HasColumnType("datetime(6)"); + + b.Property("LockedBy") + .HasColumnType("int"); + + b.Property("PeriodMonth") + .HasColumnType("int"); + + b.Property("PeriodYear") + .HasColumnType("int"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("int unsigned"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("varchar(20)"); + + b.Property("UnlockReason") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("UnlockedAt") + .HasColumnType("datetime(6)"); + + b.Property("UnlockedBy") + .HasColumnType("int"); + + b.HasKey("PayrollRunId"); + + b.HasIndex("BranchId"); + + b.HasIndex("DocNo") + .IsUnique(); + + b.HasIndex("Status"); + + b.HasIndex("PeriodYear", "PeriodMonth", "BranchId"); + + b.ToTable("hr_payroll_runs", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.PayrollStatutorySetting", b => + { + b.Property("PayrollStatutorySettingId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("PayrollStatutorySettingId")); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("CreatedBy") + .HasColumnType("int"); + + b.Property("EffectiveFrom") + .HasColumnType("datetime(6)"); + + b.Property("EffectiveTo") + .HasColumnType("datetime(6)"); + + b.Property("EpfEmployeeRate") + .HasPrecision(6, 4) + .HasColumnType("decimal(6,4)"); + + b.Property("EpfEmployerRate") + .HasPrecision(6, 4) + .HasColumnType("decimal(6,4)"); + + b.Property("EtfEmployerRate") + .HasPrecision(6, 4) + .HasColumnType("decimal(6,4)"); + + b.Property("OtMultiplierDefault") + .HasPrecision(6, 2) + .HasColumnType("decimal(6,2)"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("int unsigned"); + + b.HasKey("PayrollStatutorySettingId"); + + b.HasIndex("EffectiveFrom"); + + b.ToTable("hr_payroll_statutory_settings", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.Payslip", b => + { + b.Property("PayslipId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("PayslipId")); + + b.Property("GeneratedAt") + .HasColumnType("datetime(6)"); + + b.Property("PayrollLineId") + .HasColumnType("int"); + + b.Property("ReleasedAt") + .HasColumnType("datetime(6)"); + + b.Property("ReleasedBy") + .HasColumnType("int"); + + b.HasKey("PayslipId"); + + b.HasIndex("PayrollLineId") + .IsUnique(); + + b.ToTable("hr_payslips", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.Permission", b => + { + b.Property("PermissionId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("PermissionId")); + + b.Property("Code") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("varchar(80)"); + + b.Property("NavItemId") + .HasColumnType("int"); + + b.Property("SubNavItemId") + .HasColumnType("int"); + + b.HasKey("PermissionId"); + + b.HasIndex("Code") + .IsUnique(); + + b.HasIndex("NavItemId"); + + b.HasIndex("SubNavItemId"); + + b.ToTable("permissions", (string)null); + + b.HasData( + new + { + PermissionId = 1, + Code = "NAV:dashboard", + NavItemId = 1 + }, + new + { + PermissionId = 2, + Code = "NAV:products", + NavItemId = 2 + }, + new + { + PermissionId = 3, + Code = "NAV:procurement.vendors", + SubNavItemId = 30 + }, + new + { + PermissionId = 4, + Code = "NAV:procurement", + NavItemId = 4 + }, + new + { + PermissionId = 5, + Code = "NAV:receiving", + NavItemId = 5 + }, + new + { + PermissionId = 6, + Code = "NAV:stock", + NavItemId = 6 + }, + new + { + PermissionId = 7, + Code = "NAV:warehouses", + NavItemId = 7 + }, + new + { + PermissionId = 8, + Code = "NAV:orders", + NavItemId = 8 + }, + new + { + PermissionId = 9, + Code = "NAV:settings", + NavItemId = 9 + }, + new + { + PermissionId = 10, + Code = "NAV:help", + NavItemId = 10 + }, + new + { + PermissionId = 11, + Code = "NAV:products.item", + SubNavItemId = 1 + }, + new + { + PermissionId = 12, + Code = "NAV:products.category", + SubNavItemId = 2 + }, + new + { + PermissionId = 13, + Code = "NAV:products.brand", + SubNavItemId = 3 + }, + new + { + PermissionId = 14, + Code = "NAV:products.item-type", + SubNavItemId = 4 + }, + new + { + PermissionId = 15, + Code = "NAV:products.uom", + SubNavItemId = 5 + }, + new + { + PermissionId = 16, + Code = "NAV:products.configuration", + SubNavItemId = 6 + }, + new + { + PermissionId = 17, + Code = "NAV:settings.roles", + SubNavItemId = 7 + }, + new + { + PermissionId = 18, + Code = "NAV:settings.users", + SubNavItemId = 8 + }, + new + { + PermissionId = 28, + Code = "NAV:procurement.requisitions", + SubNavItemId = 17 + }, + new + { + PermissionId = 29, + Code = "NAV:procurement.rfqs", + SubNavItemId = 18 + }, + new + { + PermissionId = 30, + Code = "NAV:procurement.purchase-orders", + SubNavItemId = 19 + }, + new + { + PermissionId = 31, + Code = "NAV:procurement.purchase-returns", + SubNavItemId = 20 + }, + new + { + PermissionId = 19, + Code = "NAV:ledgers", + NavItemId = 11 + }, + new + { + PermissionId = 20, + Code = "NAV:ledgers.trial-balance", + SubNavItemId = 9 + }, + new + { + PermissionId = 21, + Code = "NAV:ledgers.balance-sheet", + SubNavItemId = 10 + }, + new + { + PermissionId = 22, + Code = "NAV:ledgers.general-ledger", + SubNavItemId = 11 + }, + new + { + PermissionId = 23, + Code = "NAV:ledgers.profit-and-loss", + SubNavItemId = 12 + }, + new + { + PermissionId = 24, + Code = "NAV:ledgers.cash-flow", + SubNavItemId = 13 + }, + new + { + PermissionId = 25, + Code = "NAV:ledgers.budget-vs-actual", + SubNavItemId = 14 + }, + new + { + PermissionId = 27, + Code = "NAV:ledgers.tax-report", + SubNavItemId = 16 + }, + new + { + PermissionId = 26, + Code = "NAV:accounts.bank-accounts", + SubNavItemId = 15 + }, + new + { + PermissionId = 32, + Code = "NAV:accounts", + NavItemId = 12 + }, + new + { + PermissionId = 33, + Code = "NAV:accounts.cheque-books", + SubNavItemId = 21 + }, + new + { + PermissionId = 34, + Code = "NAV:accounts.received-cheques", + SubNavItemId = 22 + }, + new + { + PermissionId = 35, + Code = "NAV:production", + NavItemId = 14 + }, + new + { + PermissionId = 36, + Code = "NAV:production.templates", + SubNavItemId = 27 + }, + new + { + PermissionId = 37, + Code = "NAV:production.runs", + SubNavItemId = 25 + }, + new + { + PermissionId = 38, + Code = "NAV:production.produced", + SubNavItemId = 26 + }, + new + { + PermissionId = 39, + Code = "NAV:procurement.purchase-invoices", + SubNavItemId = 28 + }, + new + { + PermissionId = 40, + Code = "NAV:settings.branches", + SubNavItemId = 29 + }, + new + { + PermissionId = 41, + Code = "NAV:stock.ledger", + SubNavItemId = 31 + }, + new + { + PermissionId = 42, + Code = "NAV:stock.enquiry", + SubNavItemId = 32 + }, + new + { + PermissionId = 43, + Code = "NAV:stock.valuation", + SubNavItemId = 33 + }, + new + { + PermissionId = 44, + Code = "NAV:stock.transfers", + SubNavItemId = 34 + }, + new + { + PermissionId = 45, + Code = "NAV:stock.adjustments", + SubNavItemId = 35 + }, + new + { + PermissionId = 46, + Code = "NAV:stock.counts", + SubNavItemId = 36 + }, + new + { + PermissionId = 47, + Code = "NAV:stock.reorder-alerts", + SubNavItemId = 37 + }, + new + { + PermissionId = 48, + Code = "NAV:stock.wastage", + SubNavItemId = 38 + }, + new + { + PermissionId = 49, + Code = "NAV:sales.bundle-templates", + SubNavItemId = 23 + }, + new + { + PermissionId = 50, + Code = "NAV:sales.invoices", + SubNavItemId = 40 + }, + new + { + PermissionId = 51, + Code = "NAV:sales.free-issues", + SubNavItemId = 41 + }, + new + { + PermissionId = 52, + Code = "NAV:sales.day-end", + SubNavItemId = 42 + }, + new + { + PermissionId = 53, + Code = "NAV:products.tax-rates", + SubNavItemId = 43 + }, + new + { + PermissionId = 54, + Code = "NAV:accounts.expenses", + SubNavItemId = 44 + }, + new + { + PermissionId = 55, + Code = "NAV:accounts.expense-setup", + SubNavItemId = 45 + }, + new + { + PermissionId = 56, + Code = "NAV:sales.day-end-report", + SubNavItemId = 46 + }, + new + { + PermissionId = 57, + Code = "NAV:accounts.bank-deposits", + SubNavItemId = 47 + }, + new + { + PermissionId = 58, + Code = "NAV:products.price-adjustments", + SubNavItemId = 48 + }, + new + { + PermissionId = 59, + Code = "NAV:settings.approvals", + SubNavItemId = 49 + }, + new + { + PermissionId = 60, + Code = "NAV:procurement.po-approvals", + SubNavItemId = 50 + }, + new + { + PermissionId = 61, + Code = "NAV:accounts.opening-balances", + SubNavItemId = 51 + }, + new + { + PermissionId = 62, + Code = "NAV:hrm", + NavItemId = 15 + }, + new + { + PermissionId = 63, + Code = "NAV:sales-returns", + NavItemId = 16 + }, + new + { + PermissionId = 64, + Code = "NAV:hrm.employees", + SubNavItemId = 52 + }, + new + { + PermissionId = 65, + Code = "NAV:hrm.attendance", + SubNavItemId = 53 + }, + new + { + PermissionId = 66, + Code = "NAV:hrm.leave", + SubNavItemId = 54 + }, + new + { + PermissionId = 67, + Code = "NAV:hrm.payroll", + SubNavItemId = 55 + }, + new + { + PermissionId = 68, + Code = "NAV:hrm.reports", + SubNavItemId = 56 + }, + new + { + PermissionId = 69, + Code = "NAV:hrm.settings", + SubNavItemId = 57 + }, + new + { + PermissionId = 70, + Code = "NAV:settings.audit-logs", + SubNavItemId = 58 + }, + new + { + PermissionId = 71, + Code = "NAV:settings.notification-config", + SubNavItemId = 24 + }); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.PoLine", b => + { + b.Property("PoLineId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("PoLineId")); + + b.Property("ItemId") + .HasColumnType("int"); + + b.Property("PoId") + .HasColumnType("int"); + + b.Property("Qty") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("QtyReceived") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("Tax") + .HasPrecision(9, 4) + .HasColumnType("decimal(9,4)"); + + b.Property("TaxAmount") + .ValueGeneratedOnAdd() + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)") + .HasDefaultValue(0m); + + b.Property("UnitPrice") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("WarehouseId") + .HasColumnType("int"); + + b.HasKey("PoLineId"); + + b.HasIndex("ItemId"); + + b.HasIndex("PoId"); + + b.HasIndex("WarehouseId"); + + b.ToTable("po_lines", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.PriceAdjustment", b => + { + b.Property("PriceAdjustmentId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("PriceAdjustmentId")); + + b.Property("AppliedAt") + .HasColumnType("datetime(6)"); + + b.Property("ApprovedLevel1At") + .HasColumnType("datetime(6)"); + + b.Property("ApprovedLevel1By") + .HasColumnType("int"); + + b.Property("ApprovedLevel2At") + .HasColumnType("datetime(6)"); + + b.Property("ApprovedLevel2By") + .HasColumnType("int"); + + b.Property("CancelledAt") + .HasColumnType("datetime(6)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("CreatedBy") + .HasColumnType("int"); + + b.Property("DocNo") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("varchar(30)"); + + b.Property("Reason") + .IsRequired() + .HasMaxLength(250) + .HasColumnType("varchar(250)"); + + b.Property("RejectReason") + .HasMaxLength(250) + .HasColumnType("varchar(250)"); + + b.Property("RejectedAt") + .HasColumnType("datetime(6)"); + + b.Property("RejectedBy") + .HasColumnType("int"); + + b.Property("RequiredApprovals") + .HasColumnType("int"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("int unsigned"); + + b.Property("Status") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(30) + .HasColumnType("varchar(30)") + .HasDefaultValue("PendingApproval"); + + b.HasKey("PriceAdjustmentId"); + + b.HasIndex("CreatedBy"); + + b.HasIndex("DocNo") + .IsUnique(); + + b.HasIndex("Status"); + + b.ToTable("price_adjustments", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.PriceAdjustmentLine", b => + { + b.Property("PriceAdjustmentLineId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("PriceAdjustmentLineId")); + + b.Property("ItemId") + .HasColumnType("int"); + + b.Property("NewFlatPrice") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("NewMaxPrice") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("NewMinPrice") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("OldFlatPrice") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("OldMaxPrice") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("OldMinPrice") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("PriceAdjustmentId") + .HasColumnType("int"); + + b.HasKey("PriceAdjustmentLineId"); + + b.HasIndex("ItemId"); + + b.HasIndex("PriceAdjustmentId"); + + b.ToTable("price_adjustment_lines", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.ProcurementConfig", b => + { + b.Property("ConfigId") + .HasColumnType("int"); + + b.Property("PoTaxEnabled") + .ValueGeneratedOnAdd() + .HasColumnType("tinyint(1)") + .HasDefaultValue(false); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("int unsigned"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.Property("UpdatedBy") + .HasColumnType("int"); + + b.Property("VendorAddressEnabled") + .ValueGeneratedOnAdd() + .HasColumnType("tinyint(1)") + .HasDefaultValue(false); + + b.Property("VendorContactMode") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(10) + .HasColumnType("varchar(10)") + .HasDefaultValue("None"); + + b.Property("VendorPaymentTermsEnabled") + .ValueGeneratedOnAdd() + .HasColumnType("tinyint(1)") + .HasDefaultValue(true); + + b.Property("VendorTaxRegEnabled") + .ValueGeneratedOnAdd() + .HasColumnType("tinyint(1)") + .HasDefaultValue(true); + + b.HasKey("ConfigId"); + + b.HasIndex("UpdatedBy"); + + b.ToTable("procurement_config", null, t => + { + t.HasCheckConstraint("ck_procurement_config_singleton", "`ConfigId` = 1"); + }); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.ProductConfig", b => + { + b.Property("ConfigId") + .HasColumnType("int"); + + b.Property("BatchListsEnabled") + .ValueGeneratedOnAdd() + .HasColumnType("tinyint(1)") + .HasDefaultValue(true); + + b.Property("BrandsEnabled") + .ValueGeneratedOnAdd() + .HasColumnType("tinyint(1)") + .HasDefaultValue(true); + + b.Property("CustomVariantsEnabled") + .ValueGeneratedOnAdd() + .HasColumnType("tinyint(1)") + .HasDefaultValue(false); + + b.Property("CustomWarrantyPeriodsEnabled") + .ValueGeneratedOnAdd() + .HasColumnType("tinyint(1)") + .HasDefaultValue(false); + + b.Property("ItemImagesEnabled") + .ValueGeneratedOnAdd() + .HasColumnType("tinyint(1)") + .HasDefaultValue(false); + + b.Property("ItemTypesEnabled") + .ValueGeneratedOnAdd() + .HasColumnType("tinyint(1)") + .HasDefaultValue(true); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("int unsigned"); + + b.Property("SellingPricesEnabled") + .ValueGeneratedOnAdd() + .HasColumnType("tinyint(1)") + .HasDefaultValue(true); + + b.Property("SerialNumbersEnabled") + .ValueGeneratedOnAdd() + .HasColumnType("tinyint(1)") + .HasDefaultValue(true); + + b.Property("SubcategoriesEnabled") + .ValueGeneratedOnAdd() + .HasColumnType("tinyint(1)") + .HasDefaultValue(true); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.Property("UpdatedBy") + .HasColumnType("int"); + + b.Property("WarrantyPeriodsEnabled") + .ValueGeneratedOnAdd() + .HasColumnType("tinyint(1)") + .HasDefaultValue(true); + + b.HasKey("ConfigId"); + + b.HasIndex("UpdatedBy"); + + b.ToTable("product_config", null, t => + { + t.HasCheckConstraint("ck_product_config_singleton", "`ConfigId` = 1"); + }); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.ProductionRun", b => + { + b.Property("RunId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("RunId")); + + b.Property("CancelReasonCodeId") + .HasColumnType("int"); + + b.Property("CompletedAt") + .HasColumnType("datetime(6)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("CreatedBy") + .HasColumnType("int"); + + b.Property("DocNo") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("varchar(30)"); + + b.Property("OutputBinId") + .HasColumnType("int"); + + b.Property("ReworkCount") + .HasColumnType("int"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("int unsigned"); + + b.Property("ScaleFactor") + .HasPrecision(18, 6) + .HasColumnType("decimal(18,6)"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("varchar(20)"); + + b.Property("TargetQty") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("TemplateId") + .HasColumnType("int"); + + b.Property("WarehouseId") + .HasColumnType("int"); + + b.HasKey("RunId"); + + b.HasIndex("CancelReasonCodeId"); + + b.HasIndex("CreatedBy"); + + b.HasIndex("DocNo") + .IsUnique(); + + b.HasIndex("OutputBinId"); + + b.HasIndex("Status"); + + b.HasIndex("WarehouseId"); + + b.HasIndex("TemplateId", "Status"); + + b.ToTable("production_runs", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.ProductionTemplate", b => + { + b.Property("TemplateId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("TemplateId")); + + b.Property("Annotations") + .HasColumnType("longtext"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("varchar(30)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("CreatedBy") + .HasColumnType("int"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("varchar(150)"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("int unsigned"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("varchar(20)"); + + b.HasKey("TemplateId"); + + b.HasIndex("Code") + .IsUnique(); + + b.HasIndex("CreatedBy"); + + b.HasIndex("Status"); + + b.ToTable("production_templates", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.PurchaseInvoice", b => + { + b.Property("PurchaseInvoiceId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("PurchaseInvoiceId")); + + b.Property("BalanceAmount") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("BranchId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("CreatedBy") + .HasColumnType("int"); + + b.Property("DocNo") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("varchar(30)"); + + b.Property("GlJournalNo") + .HasMaxLength(30) + .HasColumnType("varchar(30)"); + + b.Property("GlPostedAt") + .HasColumnType("datetime(6)"); + + b.Property("GrandTotal") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("PaidAmount") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("int unsigned"); + + b.Property("Status") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(20) + .HasColumnType("varchar(20)") + .HasDefaultValue("Draft"); + + b.Property("Subtotal") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.Property("VatTotal") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("VendorId") + .HasColumnType("int"); + + b.Property("VendorInvoiceDate") + .HasColumnType("datetime(6)"); + + b.Property("VendorInvoiceNo") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("VendorSnapshotName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("VendorSnapshotTaxNo") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.HasKey("PurchaseInvoiceId"); + + b.HasIndex("BranchId"); + + b.HasIndex("CreatedBy"); + + b.HasIndex("DocNo") + .IsUnique(); + + b.HasIndex("Status"); + + b.HasIndex("VendorId"); + + b.HasIndex("VendorId", "VendorInvoiceNo"); + + b.ToTable("purchase_invoices", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.PurchaseInvoiceLine", b => + { + b.Property("PurchaseInvoiceLineId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("PurchaseInvoiceLineId")); + + b.Property("GrnId") + .HasColumnType("int"); + + b.Property("LineTotal") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("PurchaseInvoiceId") + .HasColumnType("int"); + + b.Property("Subtotal") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("VatTotal") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.HasKey("PurchaseInvoiceLineId"); + + b.HasIndex("GrnId") + .IsUnique(); + + b.HasIndex("PurchaseInvoiceId"); + + b.ToTable("purchase_invoice_lines", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.PurchaseInvoicePayment", b => + { + b.Property("PurchaseInvoicePaymentId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("PurchaseInvoicePaymentId")); + + b.Property("Amount") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("BankAccountName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("CreatedBy") + .HasColumnType("int"); + + b.Property("GlBankAccountId") + .HasColumnType("bigint"); + + b.Property("GlJournalNo") + .HasMaxLength(30) + .HasColumnType("varchar(30)"); + + b.Property("PaymentDate") + .HasColumnType("datetime(6)"); + + b.Property("PurchaseInvoiceId") + .HasColumnType("int"); + + b.Property("Reference") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.HasKey("PurchaseInvoicePaymentId"); + + b.HasIndex("CreatedBy"); + + b.HasIndex("PurchaseInvoiceId"); + + b.ToTable("purchase_invoice_payments", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.PurchaseOrder", b => + { + b.Property("PoId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("PoId")); + + b.Property("ApprovalRequired") + .HasColumnType("tinyint(1)"); + + b.Property("ApprovedAt") + .HasColumnType("datetime(6)"); + + b.Property("ApprovedBy") + .HasColumnType("int"); + + b.Property("ApproverUserId") + .HasColumnType("int"); + + b.Property("BranchId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("CreatedBy") + .HasColumnType("int"); + + b.Property("DocNo") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("varchar(30)"); + + b.Property("RequisitionId") + .HasColumnType("int"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("int unsigned"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("varchar(20)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.Property("VendorId") + .HasColumnType("int"); + + b.HasKey("PoId"); + + b.HasIndex("ApproverUserId"); + + b.HasIndex("BranchId"); + + b.HasIndex("CreatedBy"); + + b.HasIndex("DocNo") + .IsUnique(); + + b.HasIndex("RequisitionId"); + + b.HasIndex("Status"); + + b.HasIndex("VendorId"); + + b.ToTable("purchase_orders", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.PurchaseReturn", b => + { + b.Property("ReturnId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("ReturnId")); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("CreatedBy") + .HasColumnType("int"); + + b.Property("DocNo") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("varchar(30)"); + + b.Property("GlJournalNo") + .HasMaxLength(30) + .HasColumnType("varchar(30)"); + + b.Property("GlPostedAt") + .HasColumnType("datetime(6)"); + + b.Property("ReasonCodeId") + .HasColumnType("int"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("varchar(20)"); + + b.Property("VendorId") + .HasColumnType("int"); + + b.Property("WarehouseId") + .HasColumnType("int"); + + b.HasKey("ReturnId"); + + b.HasIndex("CreatedBy"); + + b.HasIndex("DocNo") + .IsUnique(); + + b.HasIndex("ReasonCodeId"); + + b.HasIndex("VendorId"); + + b.HasIndex("WarehouseId"); + + b.ToTable("purchase_returns", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.PurchaseReturnLine", b => + { + b.Property("ReturnLineId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("ReturnLineId")); + + b.Property("GrnLineId") + .HasColumnType("int"); + + b.Property("ItemId") + .HasColumnType("int"); + + b.Property("Qty") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("ReturnId") + .HasColumnType("int"); + + b.HasKey("ReturnLineId"); + + b.HasIndex("GrnLineId"); + + b.HasIndex("ItemId"); + + b.HasIndex("ReturnId"); + + b.ToTable("purchase_return_lines", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.ReasonCode", b => + { + b.Property("ReasonCodeId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("ReasonCodeId")); + + b.Property("Code") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("varchar(20)"); + + b.Property("Context") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("varchar(20)"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.HasKey("ReasonCodeId"); + + b.HasIndex("Context", "Code") + .IsUnique(); + + b.ToTable("reason_codes", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.Requisition", b => + { + b.Property("RequisitionId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("RequisitionId")); + + b.Property("BranchId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("DocNo") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("varchar(30)"); + + b.Property("RequestedBy") + .HasColumnType("int"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("varchar(20)"); + + b.HasKey("RequisitionId"); + + b.HasIndex("BranchId"); + + b.HasIndex("DocNo") + .IsUnique(); + + b.HasIndex("RequestedBy"); + + b.HasIndex("Status"); + + b.ToTable("requisitions", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.RequisitionLine", b => + { + b.Property("ReqLineId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("ReqLineId")); + + b.Property("ItemId") + .HasColumnType("int"); + + b.Property("Qty") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("RequiredBy") + .HasColumnType("date"); + + b.Property("RequisitionId") + .HasColumnType("int"); + + b.HasKey("ReqLineId"); + + b.HasIndex("ItemId"); + + b.HasIndex("RequisitionId"); + + b.ToTable("requisition_lines", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.Rfq", b => + { + b.Property("RfqId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("RfqId")); + + b.Property("BranchId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("DocNo") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("varchar(30)"); + + b.Property("RequisitionId") + .HasColumnType("int"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("varchar(20)"); + + b.HasKey("RfqId"); + + b.HasIndex("BranchId"); + + b.HasIndex("DocNo") + .IsUnique(); + + b.HasIndex("RequisitionId"); + + b.ToTable("rfqs", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.RfqLine", b => + { + b.Property("RfqLineId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("RfqLineId")); + + b.Property("ItemId") + .HasColumnType("int"); + + b.Property("Qty") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("RfqId") + .HasColumnType("int"); + + b.HasKey("RfqLineId"); + + b.HasIndex("ItemId"); + + b.HasIndex("RfqId"); + + b.ToTable("rfq_lines", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.Role", b => + { + b.Property("RoleId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("RoleId")); + + b.Property("AuthRoleId") + .HasColumnType("char(36)") + .HasColumnName("auth_role_id"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("IsAdmin") + .ValueGeneratedOnAdd() + .HasColumnType("tinyint(1)") + .HasDefaultValue(false); + + b.Property("IsSystemRole") + .ValueGeneratedOnAdd() + .HasColumnType("tinyint(1)") + .HasDefaultValue(false); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("int unsigned"); + + b.Property("Status") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(20) + .HasColumnType("varchar(20)") + .HasDefaultValue("Active"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("RoleId"); + + b.HasIndex("AuthRoleId") + .IsUnique(); + + b.HasIndex("Code") + .IsUnique(); + + b.ToTable("roles", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.RolePermission", b => + { + b.Property("RoleId") + .HasColumnType("int"); + + b.Property("PermissionId") + .HasColumnType("int"); + + b.Property("Actions") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasDefaultValue(31); + + b.HasKey("RoleId", "PermissionId"); + + b.HasIndex("PermissionId"); + + b.ToTable("role_permissions", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.RunEdge", b => + { + b.Property("RunEdgeId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("RunEdgeId")); + + b.Property("ChildRunStageId") + .HasColumnType("int"); + + b.Property("ParentRunStageId") + .HasColumnType("int"); + + b.Property("RunId") + .HasColumnType("int"); + + b.HasKey("RunEdgeId"); + + b.HasIndex("ChildRunStageId"); + + b.HasIndex("RunId"); + + b.HasIndex("ParentRunStageId", "ChildRunStageId") + .IsUnique(); + + b.ToTable("run_edges", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.RunStage", b => + { + b.Property("RunStageId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("RunStageId")); + + b.Property("ActualEndAt") + .HasColumnType("datetime(6)"); + + b.Property("ActualStartAt") + .HasColumnType("datetime(6)"); + + b.Property("EstimatedMinutes") + .HasColumnType("int"); + + b.Property("FieldDefs") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("FieldValues") + .HasColumnType("longtext"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("varchar(150)"); + + b.Property("PosX") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("PosY") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("RoleLabel") + .HasMaxLength(60) + .HasColumnType("varchar(60)"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("int unsigned"); + + b.Property("RunId") + .HasColumnType("int"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("varchar(20)"); + + b.Property("TemplateStageId") + .HasColumnType("int"); + + b.HasKey("RunStageId"); + + b.HasIndex("TemplateStageId"); + + b.HasIndex("RunId", "Status"); + + b.ToTable("run_stages", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.RunStageEvent", b => + { + b.Property("EventId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("EventId")); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("EventType") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("varchar(20)"); + + b.Property("Note") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("Payload") + .HasColumnType("longtext"); + + b.Property("RunId") + .HasColumnType("int"); + + b.Property("RunStageId") + .HasColumnType("int"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("EventId"); + + b.HasIndex("RunStageId"); + + b.HasIndex("UserId"); + + b.HasIndex("RunId", "EventId"); + + b.ToTable("run_stage_events", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.RunStageInput", b => + { + b.Property("RunInputId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("RunInputId")); + + b.Property("ConsumedQty") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("ConsumedValue") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("DeliveredQty") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("FromRunOutputId") + .HasColumnType("int"); + + b.Property("ItemId") + .HasColumnType("int"); + + b.Property("PlannedQty") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("QtyUnit") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("varchar(20)"); + + b.Property("ReturnedQty") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("ReturnedValue") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("RunStageId") + .HasColumnType("int"); + + b.Property("Source") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("varchar(20)"); + + b.HasKey("RunInputId"); + + b.HasIndex("FromRunOutputId"); + + b.HasIndex("ItemId"); + + b.HasIndex("RunStageId"); + + b.ToTable("run_stage_inputs", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.RunStageOutput", b => + { + b.Property("RunOutputId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("RunOutputId")); + + b.Property("ItemId") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("varchar(150)"); + + b.Property("PlannedQty") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("ProducedQty") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("RunStageId") + .HasColumnType("int"); + + b.Property("ScrapReasonCodeId") + .HasColumnType("int"); + + b.Property("ScrappedQty") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("TransferredQty") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("UomId") + .HasColumnType("int"); + + b.HasKey("RunOutputId"); + + b.HasIndex("ItemId"); + + b.HasIndex("RunStageId"); + + b.HasIndex("ScrapReasonCodeId"); + + b.HasIndex("UomId"); + + b.ToTable("run_stage_outputs", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.SalaryComponent", b => + { + b.Property("SalaryComponentId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("SalaryComponentId")); + + b.Property("Code") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("varchar(20)"); + + b.Property("ComponentType") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("varchar(20)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("IsEpfEtfApplicable") + .HasColumnType("tinyint(1)"); + + b.Property("IsTaxable") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("int unsigned"); + + b.Property("Status") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(20) + .HasColumnType("varchar(20)") + .HasDefaultValue("Active"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("SalaryComponentId"); + + b.HasIndex("Code") + .IsUnique(); + + b.HasIndex("Status"); + + b.ToTable("hr_salary_components", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.SalesConfig", b => + { + b.Property("ConfigId") + .HasColumnType("int"); + + b.Property("AllowCashCustomer") + .ValueGeneratedOnAdd() + .HasColumnType("tinyint(1)") + .HasDefaultValue(false); + + b.Property("AllowDiscounts") + .ValueGeneratedOnAdd() + .HasColumnType("tinyint(1)") + .HasDefaultValue(true); + + b.Property("AllowFreeItems") + .ValueGeneratedOnAdd() + .HasColumnType("tinyint(1)") + .HasDefaultValue(true); + + b.Property("AllowOverStockSale") + .ValueGeneratedOnAdd() + .HasColumnType("tinyint(1)") + .HasDefaultValue(false); + + b.Property("AllowWarehouseSelection") + .ValueGeneratedOnAdd() + .HasColumnType("tinyint(1)") + .HasDefaultValue(true); + + b.Property("CashCustomerId") + .HasColumnType("int"); + + b.Property("CustomerAddressEnabled") + .ValueGeneratedOnAdd() + .HasColumnType("tinyint(1)") + .HasDefaultValue(true); + + b.Property("CustomerCityEnabled") + .ValueGeneratedOnAdd() + .HasColumnType("tinyint(1)") + .HasDefaultValue(true); + + b.Property("CustomerCountryEnabled") + .ValueGeneratedOnAdd() + .HasColumnType("tinyint(1)") + .HasDefaultValue(true); + + b.Property("CustomerCreditDaysEnabled") + .ValueGeneratedOnAdd() + .HasColumnType("tinyint(1)") + .HasDefaultValue(true); + + b.Property("CustomerCreditLimitEnabled") + .ValueGeneratedOnAdd() + .HasColumnType("tinyint(1)") + .HasDefaultValue(true); + + b.Property("CustomerEmailEnabled") + .ValueGeneratedOnAdd() + .HasColumnType("tinyint(1)") + .HasDefaultValue(true); + + b.Property("CustomerPhoneEnabled") + .ValueGeneratedOnAdd() + .HasColumnType("tinyint(1)") + .HasDefaultValue(true); + + b.Property("DefaultWarehouseId") + .HasColumnType("int"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("int unsigned"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.Property("UpdatedBy") + .HasColumnType("int"); + + b.HasKey("ConfigId"); + + b.HasIndex("CashCustomerId"); + + b.HasIndex("DefaultWarehouseId"); + + b.HasIndex("UpdatedBy"); + + b.ToTable("sales_config", null, t => + { + t.HasCheckConstraint("ck_sales_config_singleton", "`ConfigId` = 1"); + }); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.SalesDayEnd", b => + { + b.Property("SalesDayEndId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("SalesDayEndId")); + + b.Property("BalanceTotal") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("BundleCount") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasDefaultValue(0); + + b.Property("BusinessDate") + .HasColumnType("date"); + + b.Property("CashierUserId") + .HasColumnType("int"); + + b.Property("ClosedAt") + .HasColumnType("datetime(6)"); + + b.Property("ClosedBy") + .HasColumnType("int"); + + b.Property("CostOfGoodsSold") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("DiscountTotal") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("DocNo") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("varchar(30)"); + + b.Property("GlJournalNo") + .HasMaxLength(30) + .HasColumnType("varchar(30)"); + + b.Property("GlPostedAt") + .HasColumnType("datetime(6)"); + + b.Property("GrandTotal") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("PaidTotal") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("int unsigned"); + + b.Property("SlipCount") + .HasColumnType("int"); + + b.Property("Subtotal") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("TaxTotal") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.HasKey("SalesDayEndId"); + + b.HasIndex("ClosedBy"); + + b.HasIndex("DocNo") + .IsUnique(); + + b.HasIndex("CashierUserId", "BusinessDate") + .IsUnique(); + + b.ToTable("sales_day_ends", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.SalesInvoice", b => + { + b.Property("SalesInvoiceId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("SalesInvoiceId")); + + b.Property("BalanceAmount") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("BranchId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("CreatedBy") + .HasColumnType("int"); + + b.Property("CreatorUserId") + .HasColumnType("int"); + + b.Property("CustomerId") + .HasColumnType("int"); + + b.Property("CustomerSnapshotName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("CustomerSnapshotTaxNo") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("DiscountTotal") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("GlJournalNo") + .HasMaxLength(30) + .HasColumnType("varchar(30)"); + + b.Property("GlPostedAt") + .HasColumnType("datetime(6)"); + + b.Property("GrandTotal") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("InvoiceDate") + .HasColumnType("datetime(6)"); + + b.Property("InvoiceNo") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("InvoiceType") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(20) + .HasColumnType("varchar(20)") + .HasDefaultValue("B2C"); + + b.Property("NetPayable") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("PaidAmount") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("RoundOff") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("int unsigned"); + + b.Property("Status") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(20) + .HasColumnType("varchar(20)") + .HasDefaultValue("Draft"); + + b.Property("Subtotal") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("TaxTotal") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.Property("WarehouseId") + .HasColumnType("int"); + + b.HasKey("SalesInvoiceId"); + + b.HasIndex("BranchId"); + + b.HasIndex("CreatorUserId"); + + b.HasIndex("CustomerId"); + + b.HasIndex("InvoiceDate"); + + b.HasIndex("InvoiceNo") + .IsUnique(); + + b.HasIndex("Status"); + + b.HasIndex("WarehouseId"); + + b.ToTable("sales_invoices", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.SalesInvoiceLine", b => + { + b.Property("SalesInvoiceLineId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("SalesInvoiceLineId")); + + b.Property("BaseCost") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("BatchId") + .HasColumnType("int"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("DiscountAmount") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("DiscountMode") + .HasColumnType("int"); + + b.Property("DiscountPct") + .HasPrecision(9, 4) + .HasColumnType("decimal(9,4)"); + + b.Property("FreeQty") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("IsFreeIssue") + .HasColumnType("tinyint(1)"); + + b.Property("ItemId") + .HasColumnType("int"); + + b.Property("LineTotal") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("NetUnitPrice") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("ParentLineId") + .HasColumnType("int"); + + b.Property("PriceSource") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Qty") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("int unsigned"); + + b.Property("SalesInvoiceId") + .HasColumnType("int"); + + b.Property("TaxAmount") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("TaxPct") + .HasPrecision(9, 4) + .HasColumnType("decimal(9,4)"); + + b.Property("UnitPrice") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("WarehouseId") + .HasColumnType("int"); + + b.HasKey("SalesInvoiceLineId"); + + b.HasIndex("BatchId"); + + b.HasIndex("ItemId"); + + b.HasIndex("SalesInvoiceId"); + + b.HasIndex("WarehouseId"); + + b.ToTable("sales_invoice_lines", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.SalesInvoiceLineSerial", b => + { + b.Property("SalesInvoiceLineSerialId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("SalesInvoiceLineSerialId")); + + b.Property("SalesInvoiceLineId") + .HasColumnType("int"); + + b.Property("SerialId") + .HasColumnType("int"); + + b.HasKey("SalesInvoiceLineSerialId"); + + b.HasIndex("SerialId"); + + b.HasIndex("SalesInvoiceLineId", "SerialId") + .IsUnique(); + + b.ToTable("sales_invoice_line_serials", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.SalesInvoicePayment", b => + { + b.Property("SalesInvoicePaymentId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("SalesInvoicePaymentId")); + + b.Property("Amount") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("BankAccountName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("CreatedBy") + .HasColumnType("int"); + + b.Property("GlBankAccountId") + .HasColumnType("bigint"); + + b.Property("GlJournalNo") + .HasMaxLength(30) + .HasColumnType("varchar(30)"); + + b.Property("PaymentDate") + .HasColumnType("datetime(6)"); + + b.Property("PaymentMethod") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(20) + .HasColumnType("varchar(20)") + .HasDefaultValue("Cash"); + + b.Property("Reference") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("SalesInvoiceId") + .HasColumnType("int"); + + b.HasKey("SalesInvoicePaymentId"); + + b.HasIndex("CreatedBy"); + + b.HasIndex("SalesInvoiceId"); + + b.ToTable("sales_invoice_payments", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.SalesLineAllocation", b => + { + b.Property("SalesLineAllocationId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("SalesLineAllocationId")); + + b.Property("BatchId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("ItemId") + .HasColumnType("int"); + + b.Property("Qty") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("QtyReturned") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("SerialId") + .HasColumnType("int"); + + b.Property("SourceDocId") + .HasColumnType("int"); + + b.Property("SourceDocType") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("SourceLineId") + .HasColumnType("int"); + + b.Property("UnitCost") + .HasPrecision(18, 6) + .HasColumnType("decimal(18,6)"); + + b.Property("WarehouseId") + .HasColumnType("int"); + + b.HasKey("SalesLineAllocationId"); + + b.HasIndex("BatchId"); + + b.HasIndex("SerialId"); + + b.HasIndex("SourceDocType", "SourceDocId", "SourceLineId"); + + b.ToTable("sales_line_allocations", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.SalesReturn", b => + { + b.Property("ReturnId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("ReturnId")); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("CreatedBy") + .HasColumnType("int"); + + b.Property("CustomerId") + .HasColumnType("int"); + + b.Property("DocNo") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("varchar(30)"); + + b.Property("GlJournalNo") + .HasMaxLength(30) + .HasColumnType("varchar(30)"); + + b.Property("GlPostedAt") + .HasColumnType("datetime(6)"); + + b.Property("ReasonCodeId") + .HasColumnType("int"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("varchar(20)"); + + b.Property("WarehouseId") + .HasColumnType("int"); + + b.HasKey("ReturnId"); + + b.HasIndex("CreatedBy"); + + b.HasIndex("CustomerId"); + + b.HasIndex("DocNo") + .IsUnique(); + + b.HasIndex("ReasonCodeId"); + + b.HasIndex("WarehouseId"); + + b.ToTable("sales_returns", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.SalesReturnLine", b => + { + b.Property("ReturnLineId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("ReturnLineId")); + + b.Property("BatchId") + .HasColumnType("int"); + + b.Property("ItemId") + .HasColumnType("int"); + + b.Property("Qty") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("ReturnId") + .HasColumnType("int"); + + b.Property("SalesInvoiceLineId") + .HasColumnType("int"); + + b.HasKey("ReturnLineId"); + + b.HasIndex("BatchId"); + + b.HasIndex("ItemId"); + + b.HasIndex("ReturnId"); + + b.HasIndex("SalesInvoiceLineId"); + + b.ToTable("sales_return_lines", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.SalesReturnLineSerial", b => + { + b.Property("SalesReturnLineSerialId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("SalesReturnLineSerialId")); + + b.Property("ReturnLineId") + .HasColumnType("int"); + + b.Property("SerialId") + .HasColumnType("int"); + + b.HasKey("SalesReturnLineSerialId"); + + b.HasIndex("SerialId"); + + b.HasIndex("ReturnLineId", "SerialId") + .IsUnique(); + + b.ToTable("sales_return_line_serials", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.SalesSlip", b => + { + b.Property("SalesSlipId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("SalesSlipId")); + + b.Property("BalanceAmount") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("CardAmount") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("CashAmount") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("CashierUserId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("CustomerId") + .HasColumnType("int"); + + b.Property("CustomerSnapshotName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("DayEndId") + .HasColumnType("int"); + + b.Property("DiscountTotal") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("GrandTotal") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("PaidAmount") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("int unsigned"); + + b.Property("SlipDate") + .HasColumnType("datetime(6)"); + + b.Property("SlipNo") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Status") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(20) + .HasColumnType("varchar(20)") + .HasDefaultValue("Draft"); + + b.Property("Subtotal") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("TaxTotal") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.Property("WarehouseId") + .HasColumnType("int"); + + b.HasKey("SalesSlipId"); + + b.HasIndex("CashierUserId"); + + b.HasIndex("CustomerId"); + + b.HasIndex("DayEndId"); + + b.HasIndex("SlipDate"); + + b.HasIndex("SlipNo") + .IsUnique(); + + b.HasIndex("Status"); + + b.HasIndex("WarehouseId"); + + b.ToTable("sales_slips", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.SalesSlipLine", b => + { + b.Property("SalesSlipLineId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("SalesSlipLineId")); + + b.Property("BaseCost") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("BatchId") + .HasColumnType("int"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("DiscountAmount") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("DiscountMode") + .HasColumnType("int"); + + b.Property("DiscountPct") + .HasPrecision(9, 4) + .HasColumnType("decimal(9,4)"); + + b.Property("FreeQty") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("IsFreeIssue") + .HasColumnType("tinyint(1)"); + + b.Property("ItemId") + .HasColumnType("int"); + + b.Property("LineTotal") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("NetUnitPrice") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("ParentLineId") + .HasColumnType("int"); + + b.Property("PriceSource") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Qty") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("int unsigned"); + + b.Property("SalesSlipId") + .HasColumnType("int"); + + b.Property("SerialId") + .HasColumnType("int"); + + b.Property("TaxAmount") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("TaxPct") + .HasPrecision(9, 4) + .HasColumnType("decimal(9,4)"); + + b.Property("UnitPrice") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("WarehouseId") + .HasColumnType("int"); + + b.HasKey("SalesSlipLineId"); + + b.HasIndex("BatchId"); + + b.HasIndex("ItemId"); + + b.HasIndex("SalesSlipId"); + + b.HasIndex("SerialId"); + + b.HasIndex("WarehouseId"); + + b.ToTable("sales_slip_lines", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.SalesSlipLineSerial", b => + { + b.Property("SalesSlipLineSerialId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("SalesSlipLineSerialId")); + + b.Property("SalesSlipLineId") + .HasColumnType("int"); + + b.Property("SerialId") + .HasColumnType("int"); + + b.HasKey("SalesSlipLineSerialId"); + + b.HasIndex("SerialId"); + + b.HasIndex("SalesSlipLineId", "SerialId") + .IsUnique(); + + b.ToTable("sales_slip_line_serials", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.Serial", b => + { + b.Property("SerialId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("SerialId")); + + b.Property("Barcode") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("BinId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("GrnLineId") + .HasColumnType("int"); + + b.Property("ItemId") + .HasColumnType("int"); + + b.Property("ReceivedAt") + .HasColumnType("datetime(6)"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("int unsigned"); + + b.Property("SerialNo") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("varchar(20)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.Property("WarehouseId") + .HasColumnType("int"); + + b.Property("WarrantyPeriodMonths") + .HasColumnType("int"); + + b.HasKey("SerialId"); + + b.HasIndex("Barcode") + .IsUnique(); + + b.HasIndex("BinId"); + + b.HasIndex("GrnLineId"); + + b.HasIndex("ItemId", "SerialNo") + .IsUnique(); + + b.HasIndex("ItemId", "Status"); + + b.HasIndex("WarehouseId", "ItemId", "Status"); + + b.ToTable("serials", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.SerialMovement", b => + { + b.Property("SerialMovementId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("SerialMovementId")); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("FromWarehouseId") + .HasColumnType("int"); + + b.Property("SerialId") + .HasColumnType("int"); + + b.Property("SourceDocId") + .HasColumnType("int"); + + b.Property("SourceDocType") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("varchar(20)"); + + b.Property("UserId") + .HasColumnType("int"); + + b.Property("WarehouseId") + .HasColumnType("int"); + + b.HasKey("SerialMovementId"); + + b.HasIndex("SerialId", "SerialMovementId"); + + b.HasIndex("SourceDocType", "SourceDocId"); + + b.ToTable("serial_movements", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.StageEdge", b => + { + b.Property("EdgeId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("EdgeId")); + + b.Property("ChildStageId") + .HasColumnType("int"); + + b.Property("ParentStageId") + .HasColumnType("int"); + + b.Property("TemplateId") + .HasColumnType("int"); + + b.HasKey("EdgeId"); + + b.HasIndex("ChildStageId"); + + b.HasIndex("TemplateId"); + + b.HasIndex("ParentStageId", "ChildStageId") + .IsUnique(); + + b.ToTable("stage_edges", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.StageInput", b => + { + b.Property("InputId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("InputId")); + + b.Property("FromOutputId") + .HasColumnType("int"); + + b.Property("ItemId") + .HasColumnType("int"); + + b.Property("QtyPerBatch") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("QtyUnit") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("varchar(20)"); + + b.Property("Source") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("varchar(20)"); + + b.Property("StageId") + .HasColumnType("int"); + + b.HasKey("InputId"); + + b.HasIndex("FromOutputId"); + + b.HasIndex("ItemId"); + + b.HasIndex("StageId"); + + b.ToTable("stage_inputs", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.StageOutput", b => + { + b.Property("OutputId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("OutputId")); + + b.Property("ItemId") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("varchar(150)"); + + b.Property("QtyPerBatch") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("StageId") + .HasColumnType("int"); + + b.Property("UomId") + .HasColumnType("int"); + + b.HasKey("OutputId"); + + b.HasIndex("ItemId"); + + b.HasIndex("StageId"); + + b.HasIndex("UomId"); + + b.ToTable("stage_outputs", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.StockAdjustment", b => + { + b.Property("AdjustmentId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("AdjustmentId")); + + b.Property("ApprovedLevel1At") + .HasColumnType("datetime(6)"); + + b.Property("ApprovedLevel1By") + .HasColumnType("int"); + + b.Property("ApprovedLevel2At") + .HasColumnType("datetime(6)"); + + b.Property("ApprovedLevel2By") + .HasColumnType("int"); + + b.Property("BranchId") + .HasColumnType("int"); + + b.Property("CancelledAt") + .HasColumnType("datetime(6)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("CreatedBy") + .HasColumnType("int"); + + b.Property("DocNo") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("varchar(30)"); + + b.Property("GlJournalNo") + .HasMaxLength(30) + .HasColumnType("varchar(30)"); + + b.Property("GlPostedAt") + .HasColumnType("datetime(6)"); + + b.Property("PostedAt") + .HasColumnType("datetime(6)"); + + b.Property("ReasonCodeId") + .HasColumnType("int"); + + b.Property("RejectReason") + .HasMaxLength(250) + .HasColumnType("varchar(250)"); + + b.Property("RejectedAt") + .HasColumnType("datetime(6)"); + + b.Property("RejectedBy") + .HasColumnType("int"); + + b.Property("RequiredApprovals") + .HasColumnType("int"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("int unsigned"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("varchar(30)"); + + b.Property("WarehouseId") + .HasColumnType("int"); + + b.HasKey("AdjustmentId"); + + b.HasIndex("BranchId"); + + b.HasIndex("CreatedBy"); + + b.HasIndex("DocNo") + .IsUnique(); + + b.HasIndex("ReasonCodeId"); + + b.HasIndex("Status"); + + b.HasIndex("WarehouseId"); + + b.ToTable("stock_adjustments", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.StockAdjustmentLine", b => + { + b.Property("AdjLineId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("AdjLineId")); + + b.Property("AdjustmentId") + .HasColumnType("int"); + + b.Property("BatchId") + .HasColumnType("int"); + + b.Property("BinId") + .HasColumnType("int"); + + b.Property("ItemId") + .HasColumnType("int"); + + b.Property("QtyDelta") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("RequestedSerialIds") + .HasMaxLength(4000) + .HasColumnType("varchar(4000)"); + + b.Property("RequestedSerialNos") + .HasMaxLength(4000) + .HasColumnType("varchar(4000)"); + + b.Property("SerialId") + .HasColumnType("int"); + + b.HasKey("AdjLineId"); + + b.HasIndex("AdjustmentId"); + + b.HasIndex("BatchId"); + + b.HasIndex("BinId"); + + b.HasIndex("ItemId"); + + b.HasIndex("SerialId"); + + b.ToTable("stock_adjustment_lines", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.StockCount", b => + { + b.Property("CountId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("CountId")); + + b.Property("BranchId") + .HasColumnType("int"); + + b.Property("CountType") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("varchar(20)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("CreatedBy") + .HasColumnType("int"); + + b.Property("DocNo") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("varchar(30)"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("int unsigned"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("varchar(20)"); + + b.Property("WarehouseId") + .HasColumnType("int"); + + b.HasKey("CountId"); + + b.HasIndex("BranchId"); + + b.HasIndex("CreatedBy"); + + b.HasIndex("DocNo") + .IsUnique(); + + b.HasIndex("Status"); + + b.HasIndex("WarehouseId"); + + b.ToTable("stock_counts", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.StockCountLine", b => + { + b.Property("CountLineId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("CountLineId")); + + b.Property("BatchId") + .HasColumnType("int"); + + b.Property("BinId") + .HasColumnType("int"); + + b.Property("CountId") + .HasColumnType("int"); + + b.Property("CountedQty") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("ItemId") + .HasColumnType("int"); + + b.Property("SerialId") + .HasColumnType("int"); + + b.Property("SystemQty") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("Variance") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.HasKey("CountLineId"); + + b.HasIndex("BatchId"); + + b.HasIndex("BinId"); + + b.HasIndex("CountId"); + + b.HasIndex("ItemId"); + + b.HasIndex("SerialId"); + + b.ToTable("stock_count_lines", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.StockLayer", b => + { + b.Property("LayerId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("LayerId")); + + b.Property("BatchId") + .HasColumnType("int"); + + b.Property("GrnLineId") + .HasColumnType("int"); + + b.Property("ItemId") + .HasColumnType("int"); + + b.Property("QtyReceived") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("QtyRemaining") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("ReceiptDate") + .HasColumnType("datetime(6)"); + + b.Property("SerialId") + .HasColumnType("int"); + + b.Property("UnitCost") + .HasPrecision(18, 6) + .HasColumnType("decimal(18,6)"); + + b.Property("WarehouseId") + .HasColumnType("int"); + + b.HasKey("LayerId"); + + b.HasIndex("BatchId"); + + b.HasIndex("GrnLineId"); + + b.HasIndex("SerialId"); + + b.HasIndex("WarehouseId"); + + b.HasIndex("ItemId", "WarehouseId", "ReceiptDate", "LayerId"); + + b.ToTable("stock_layers", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.StockLedger", b => + { + b.Property("LedgerId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("LedgerId")); + + b.Property("BatchId") + .HasColumnType("int"); + + b.Property("BinId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Direction") + .IsRequired() + .HasMaxLength(5) + .HasColumnType("varchar(5)"); + + b.Property("ItemId") + .HasColumnType("int"); + + b.Property("QtyBase") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("RunningBalance") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("SerialId") + .HasColumnType("int"); + + b.Property("SourceDocId") + .HasColumnType("int"); + + b.Property("SourceDocType") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("varchar(10)"); + + b.Property("UnitCost") + .HasPrecision(18, 6) + .HasColumnType("decimal(18,6)"); + + b.Property("UserId") + .HasColumnType("int"); + + b.Property("Value") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("WarehouseId") + .HasColumnType("int"); + + b.HasKey("LedgerId"); + + b.HasIndex("BatchId"); + + b.HasIndex("BinId"); + + b.HasIndex("SerialId"); + + b.HasIndex("UserId"); + + b.HasIndex("WarehouseId"); + + b.HasIndex("SourceDocType", "SourceDocId"); + + b.HasIndex("ItemId", "WarehouseId", "CreatedAt"); + + b.HasIndex("ItemId", "WarehouseId", "LedgerId"); + + b.ToTable("stock_ledger", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.StockTransfer", b => + { + b.Property("TransferId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("TransferId")); + + b.Property("BranchId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("CreatedBy") + .HasColumnType("int"); + + b.Property("DestWarehouseId") + .HasColumnType("int"); + + b.Property("DocNo") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("varchar(30)"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("int unsigned"); + + b.Property("SrcWarehouseId") + .HasColumnType("int"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("varchar(20)"); + + b.HasKey("TransferId"); + + b.HasIndex("BranchId"); + + b.HasIndex("CreatedBy"); + + b.HasIndex("DestWarehouseId"); + + b.HasIndex("DocNo") + .IsUnique(); + + b.HasIndex("SrcWarehouseId"); + + b.HasIndex("Status"); + + b.ToTable("stock_transfers", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.StockTransferLine", b => + { + b.Property("TransferLineId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("TransferLineId")); + + b.Property("BatchId") + .HasColumnType("int"); + + b.Property("DestBinId") + .HasColumnType("int"); + + b.Property("ItemId") + .HasColumnType("int"); + + b.Property("Qty") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("QtyReceived") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("SerialId") + .HasColumnType("int"); + + b.Property("SrcBinId") + .HasColumnType("int"); + + b.Property("TransferId") + .HasColumnType("int"); + + b.Property("UnitCost") + .HasPrecision(18, 6) + .HasColumnType("decimal(18,6)"); + + b.HasKey("TransferLineId"); + + b.HasIndex("BatchId"); + + b.HasIndex("DestBinId"); + + b.HasIndex("ItemId"); + + b.HasIndex("SerialId"); + + b.HasIndex("SrcBinId"); + + b.HasIndex("TransferId"); + + b.ToTable("stock_transfer_lines", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.StockTransferLineSerial", b => + { + b.Property("StockTransferLineSerialId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("StockTransferLineSerialId")); + + b.Property("BatchId") + .HasColumnType("int"); + + b.Property("ReceivedAt") + .HasColumnType("datetime(6)"); + + b.Property("SerialId") + .HasColumnType("int"); + + b.Property("TransferLineId") + .HasColumnType("int"); + + b.Property("UnitCost") + .HasPrecision(18, 6) + .HasColumnType("decimal(18,6)"); + + b.HasKey("StockTransferLineSerialId"); + + b.HasIndex("BatchId"); + + b.HasIndex("SerialId"); + + b.HasIndex("TransferLineId", "SerialId") + .IsUnique(); + + b.ToTable("stock_transfer_line_serials", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.SubCategory", b => + { + b.Property("SubCategoryId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("SubCategoryId")); + + b.Property("CategoryId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("int unsigned"); + + b.Property("Status") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(20) + .HasColumnType("varchar(20)") + .HasDefaultValue("Active"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("SubCategoryId"); + + b.HasIndex("Status"); + + b.HasIndex("CategoryId", "Name") + .IsUnique(); + + b.ToTable("subcategories", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.SubNavItem", b => + { + b.Property("SubNavItemId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("SubNavItemId")); + + b.Property("Code") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Href") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("Icon") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Label") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("NavItemId") + .HasColumnType("int"); + + b.Property("SortOrder") + .HasColumnType("int"); + + b.Property("Status") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(20) + .HasColumnType("varchar(20)") + .HasDefaultValue("Active"); + + b.HasKey("SubNavItemId"); + + b.HasIndex("Code") + .IsUnique(); + + b.HasIndex("NavItemId"); + + b.ToTable("sub_nav_items", (string)null); + + b.HasData( + new + { + SubNavItemId = 1, + Code = "products.item", + Href = "/dashboard/products", + Label = "Item", + NavItemId = 2, + SortOrder = 1, + Status = "Active" + }, + new + { + SubNavItemId = 2, + Code = "products.category", + Href = "/dashboard/products/categories", + Label = "Category", + NavItemId = 2, + SortOrder = 2, + Status = "Active" + }, + new + { + SubNavItemId = 3, + Code = "products.brand", + Href = "/dashboard/products/brands", + Label = "Brand", + NavItemId = 2, + SortOrder = 3, + Status = "Active" + }, + new + { + SubNavItemId = 4, + Code = "products.item-type", + Href = "/dashboard/products/item-types", + Label = "Item Type", + NavItemId = 2, + SortOrder = 4, + Status = "Active" + }, + new + { + SubNavItemId = 5, + Code = "products.uom", + Href = "/dashboard/products/uoms", + Label = "UOM", + NavItemId = 2, + SortOrder = 5, + Status = "Active" + }, + new + { + SubNavItemId = 6, + Code = "products.configuration", + Href = "/dashboard/products/settings", + Label = "Configuration", + NavItemId = 2, + SortOrder = 6, + Status = "Active" + }, + new + { + SubNavItemId = 43, + Code = "products.tax-rates", + Href = "/dashboard/products/tax-rates", + Label = "Tax Rates", + NavItemId = 2, + SortOrder = 7, + Status = "Active" + }, + new + { + SubNavItemId = 48, + Code = "products.price-adjustments", + Href = "/dashboard/products/price-adjustments", + Label = "Price Adjustments", + NavItemId = 2, + SortOrder = 8, + Status = "Active" + }, + new + { + SubNavItemId = 29, + Code = "settings.branches", + Href = "/dashboard/settings/branches", + Label = "Branches", + NavItemId = 9, + SortOrder = 0, + Status = "Active" + }, + new + { + SubNavItemId = 7, + Code = "settings.roles", + Href = "/dashboard/settings/roles", + Label = "Roles", + NavItemId = 9, + SortOrder = 1, + Status = "Active" + }, + new + { + SubNavItemId = 8, + Code = "settings.users", + Href = "/dashboard/settings/users", + Label = "Users", + NavItemId = 9, + SortOrder = 2, + Status = "Active" + }, + new + { + SubNavItemId = 24, + Code = "settings.notification-config", + Href = "/dashboard/settings/notification-config", + Label = "Notification Config", + NavItemId = 9, + SortOrder = 3, + Status = "Active" + }, + new + { + SubNavItemId = 49, + Code = "settings.approvals", + Href = "/dashboard/settings/approvals", + Label = "Approvals", + NavItemId = 9, + SortOrder = 4, + Status = "Active" + }, + new + { + SubNavItemId = 40, + Code = "sales.invoices", + Href = "/dashboard/sales/invoices", + Label = "Sales", + NavItemId = 13, + SortOrder = 1, + Status = "Active" + }, + new + { + SubNavItemId = 23, + Code = "sales.bundle-templates", + Href = "/dashboard/sales/bundles/templates", + Label = "Bundle Templates", + NavItemId = 13, + SortOrder = 2, + Status = "Active" + }, + new + { + SubNavItemId = 41, + Code = "sales.free-issues", + Href = "/dashboard/sales/free-issues", + Label = "Free Issues", + NavItemId = 13, + SortOrder = 3, + Status = "Active" + }, + new + { + SubNavItemId = 42, + Code = "sales.day-end", + Href = "/dashboard/sales/day-end", + Label = "Approved Invoices", + NavItemId = 13, + SortOrder = 4, + Status = "Active" + }, + new + { + SubNavItemId = 46, + Code = "sales.day-end-report", + Href = "/dashboard/sales/day-end-report", + Label = "Day-End Report", + NavItemId = 13, + SortOrder = 5, + Status = "Active" + }, + new + { + SubNavItemId = 30, + Code = "procurement.vendors", + Href = "/dashboard/vendors", + Label = "Vendors", + NavItemId = 4, + SortOrder = 0, + Status = "Active" + }, + new + { + SubNavItemId = 17, + Code = "procurement.requisitions", + Href = "/dashboard/procurement/requisitions", + Label = "Requisitions", + NavItemId = 4, + SortOrder = 1, + Status = "Active" + }, + new + { + SubNavItemId = 18, + Code = "procurement.rfqs", + Href = "/dashboard/procurement/rfqs", + Label = "RFQs", + NavItemId = 4, + SortOrder = 2, + Status = "Active" + }, + new + { + SubNavItemId = 19, + Code = "procurement.purchase-orders", + Href = "/dashboard/procurement/purchase-orders", + Label = "Purchase Orders", + NavItemId = 4, + SortOrder = 3, + Status = "Active" + }, + new + { + SubNavItemId = 50, + Code = "procurement.po-approvals", + Href = "/dashboard/procurement/purchase-orders/approvals", + Label = "PO Approval Queue", + NavItemId = 4, + SortOrder = 6, + Status = "Active" + }, + new + { + SubNavItemId = 20, + Code = "procurement.purchase-returns", + Href = "/dashboard/procurement/purchase-returns", + Label = "Purchase Returns", + NavItemId = 4, + SortOrder = 4, + Status = "Active" + }, + new + { + SubNavItemId = 28, + Code = "procurement.purchase-invoices", + Href = "/dashboard/procurement/purchase-invoices", + Label = "Purchase Invoices", + NavItemId = 4, + SortOrder = 5, + Status = "Active" + }, + new + { + SubNavItemId = 9, + Code = "ledgers.trial-balance", + Href = "/dashboard/ledgers/trial-balance", + Label = "Trial Balance", + NavItemId = 11, + SortOrder = 1, + Status = "Active" + }, + new + { + SubNavItemId = 10, + Code = "ledgers.balance-sheet", + Href = "/dashboard/ledgers/balance-sheet", + Label = "Balance Sheet", + NavItemId = 11, + SortOrder = 2, + Status = "Active" + }, + new + { + SubNavItemId = 11, + Code = "ledgers.general-ledger", + Href = "/dashboard/ledgers/general-ledger", + Label = "General Ledger", + NavItemId = 11, + SortOrder = 3, + Status = "Active" + }, + new + { + SubNavItemId = 12, + Code = "ledgers.profit-and-loss", + Href = "/dashboard/ledgers/profit-and-loss", + Label = "Profit & Loss", + NavItemId = 11, + SortOrder = 4, + Status = "Active" + }, + new + { + SubNavItemId = 13, + Code = "ledgers.cash-flow", + Href = "/dashboard/ledgers/cash-flow", + Label = "Cash Flow", + NavItemId = 11, + SortOrder = 5, + Status = "Active" + }, + new + { + SubNavItemId = 14, + Code = "ledgers.budget-vs-actual", + Href = "/dashboard/ledgers/budget-vs-actual", + Label = "Budget vs Actual", + NavItemId = 11, + SortOrder = 6, + Status = "Active" + }, + new + { + SubNavItemId = 16, + Code = "ledgers.tax-report", + Href = "/dashboard/ledgers/tax-report", + Label = "Tax Report", + NavItemId = 11, + SortOrder = 7, + Status = "Active" + }, + new + { + SubNavItemId = 15, + Code = "accounts.bank-accounts", + Href = "/dashboard/accounts/bank-accounts", + Label = "Cash / Bank Accounts", + NavItemId = 12, + SortOrder = 1, + Status = "Active" + }, + new + { + SubNavItemId = 21, + Code = "accounts.cheque-books", + Href = "/dashboard/accounts/cheque-books", + Label = "Cheque Books", + NavItemId = 12, + SortOrder = 2, + Status = "Active" + }, + new + { + SubNavItemId = 22, + Code = "accounts.received-cheques", + Href = "/dashboard/accounts/received-cheques", + Label = "Received Cheques", + NavItemId = 12, + SortOrder = 3, + Status = "Active" + }, + new + { + SubNavItemId = 44, + Code = "accounts.expenses", + Href = "/dashboard/accounts/expenses", + Label = "Expenses", + NavItemId = 12, + SortOrder = 4, + Status = "Active" + }, + new + { + SubNavItemId = 45, + Code = "accounts.expense-setup", + Href = "/dashboard/accounts/expense-setup", + Label = "Expense Setup", + NavItemId = 12, + SortOrder = 5, + Status = "Active" + }, + new + { + SubNavItemId = 47, + Code = "accounts.bank-deposits", + Href = "/dashboard/accounts/bank-deposits", + Label = "Bank Deposits", + NavItemId = 12, + SortOrder = 6, + Status = "Active" + }, + new + { + SubNavItemId = 51, + Code = "accounts.opening-balances", + Href = "/dashboard/accounts/opening-balances", + Label = "Opening Balances", + NavItemId = 12, + SortOrder = 7, + Status = "Active" + }, + new + { + SubNavItemId = 27, + Code = "production.templates", + Href = "/dashboard/production/templates", + Label = "Templates", + NavItemId = 14, + SortOrder = 1, + Status = "Active" + }, + new + { + SubNavItemId = 25, + Code = "production.runs", + Href = "/dashboard/production/runs", + Label = "Runs", + NavItemId = 14, + SortOrder = 2, + Status = "Active" + }, + new + { + SubNavItemId = 26, + Code = "production.produced", + Href = "/dashboard/production/produced", + Label = "Produced", + NavItemId = 14, + SortOrder = 3, + Status = "Active" + }, + new + { + SubNavItemId = 31, + Code = "stock.ledger", + Href = "/dashboard/stock/ledger", + Label = "Stock Ledger", + NavItemId = 6, + SortOrder = 1, + Status = "Active" + }, + new + { + SubNavItemId = 32, + Code = "stock.enquiry", + Href = "/dashboard/stock/enquiry", + Label = "Stock Enquiry", + NavItemId = 6, + SortOrder = 2, + Status = "Active" + }, + new + { + SubNavItemId = 33, + Code = "stock.valuation", + Href = "/dashboard/stock/valuation", + Label = "Valuation", + NavItemId = 6, + SortOrder = 3, + Status = "Active" + }, + new + { + SubNavItemId = 34, + Code = "stock.transfers", + Href = "/dashboard/stock/transfers", + Label = "Transfers", + NavItemId = 6, + SortOrder = 4, + Status = "Active" + }, + new + { + SubNavItemId = 35, + Code = "stock.adjustments", + Href = "/dashboard/stock/adjustments", + Label = "Adjustments", + NavItemId = 6, + SortOrder = 5, + Status = "Active" + }, + new + { + SubNavItemId = 36, + Code = "stock.counts", + Href = "/dashboard/stock/counts", + Label = "Counts", + NavItemId = 6, + SortOrder = 6, + Status = "Active" + }, + new + { + SubNavItemId = 37, + Code = "stock.reorder-alerts", + Href = "/dashboard/stock/reorder-alerts", + Label = "Reorder Alerts", + NavItemId = 6, + SortOrder = 7, + Status = "Active" + }, + new + { + SubNavItemId = 38, + Code = "stock.wastage", + Href = "/dashboard/stock/wastage", + Label = "Wastage", + NavItemId = 6, + SortOrder = 8, + Status = "Active" + }, + new + { + SubNavItemId = 52, + Code = "hrm.employees", + Href = "/dashboard/hrm/employees", + Label = "Employees", + NavItemId = 15, + SortOrder = 1, + Status = "Active" + }, + new + { + SubNavItemId = 53, + Code = "hrm.attendance", + Href = "/dashboard/hrm/attendance", + Label = "Attendance", + NavItemId = 15, + SortOrder = 2, + Status = "Active" + }, + new + { + SubNavItemId = 54, + Code = "hrm.leave", + Href = "/dashboard/hrm/leave", + Label = "Leave", + NavItemId = 15, + SortOrder = 3, + Status = "Active" + }, + new + { + SubNavItemId = 55, + Code = "hrm.payroll", + Href = "/dashboard/hrm/payroll", + Label = "Payroll", + NavItemId = 15, + SortOrder = 4, + Status = "Active" + }, + new + { + SubNavItemId = 56, + Code = "hrm.reports", + Href = "/dashboard/hrm/reports", + Label = "Reports", + NavItemId = 15, + SortOrder = 5, + Status = "Active" + }, + new + { + SubNavItemId = 57, + Code = "hrm.settings", + Href = "/dashboard/hrm/settings", + Label = "HR Settings", + NavItemId = 15, + SortOrder = 6, + Status = "Active" + }, + new + { + SubNavItemId = 58, + Code = "settings.audit-logs", + Href = "/dashboard/settings/audit-logs", + Label = "Audit Log", + NavItemId = 9, + SortOrder = 5, + Status = "Active" + }); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.SupportTicket", b => + { + b.Property("TicketId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("TicketId")); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("CreatedBy") + .HasColumnType("int"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("int unsigned"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("varchar(20)"); + + b.Property("Subject") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.Property("UpdatedBy") + .HasColumnType("int"); + + b.HasKey("TicketId"); + + b.HasIndex("CreatedBy"); + + b.HasIndex("Status"); + + b.ToTable("support_tickets", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.SupportTicketMessage", b => + { + b.Property("MessageId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("MessageId")); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("IsFromAdmin") + .HasColumnType("tinyint(1)"); + + b.Property("Message") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("varchar(2000)"); + + b.Property("SenderUserId") + .HasColumnType("int"); + + b.Property("TicketId") + .HasColumnType("int"); + + b.HasKey("MessageId"); + + b.HasIndex("TicketId", "CreatedAt"); + + b.ToTable("support_ticket_messages", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.TaxRate", b => + { + b.Property("TaxRateId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("TaxRateId")); + + b.Property("Code") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("varchar(20)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("Percentage") + .HasPrecision(5, 2) + .HasColumnType("decimal(5,2)"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("int unsigned"); + + b.Property("Status") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(20) + .HasColumnType("varchar(20)") + .HasDefaultValue("Active"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("TaxRateId"); + + b.HasIndex("Code") + .IsUnique(); + + b.HasIndex("Status"); + + b.ToTable("tax_rates", (string)null); + + b.HasData( + new + { + TaxRateId = 1, + Code = "STANDARD", + CreatedAt = new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Name = "Standard", + Percentage = 15m, + RowVersion = 1u, + Status = "Active" + }, + new + { + TaxRateId = 2, + Code = "ZERO", + CreatedAt = new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Name = "Zero-Rated", + Percentage = 0m, + RowVersion = 1u, + Status = "Active" + }, + new + { + TaxRateId = 3, + Code = "EXEMPT", + CreatedAt = new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Name = "Exempt", + Percentage = 0m, + RowVersion = 1u, + Status = "Active" + }); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.TaxSlab", b => + { + b.Property("TaxSlabId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("TaxSlabId")); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("EffectiveFrom") + .HasColumnType("datetime(6)"); + + b.Property("EffectiveTo") + .HasColumnType("datetime(6)"); + + b.Property("LowerBound") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("Rate") + .HasPrecision(6, 4) + .HasColumnType("decimal(6,4)"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("int unsigned"); + + b.Property("UpperBound") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.HasKey("TaxSlabId"); + + b.HasIndex("EffectiveFrom"); + + b.ToTable("hr_tax_slabs", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.TemplateStage", b => + { + b.Property("StageId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("StageId")); + + b.Property("EstimatedMinutes") + .HasColumnType("int"); + + b.Property("FieldDefs") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("varchar(150)"); + + b.Property("PosX") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("PosY") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("RoleLabel") + .HasMaxLength(60) + .HasColumnType("varchar(60)"); + + b.Property("TemplateId") + .HasColumnType("int"); + + b.HasKey("StageId"); + + b.HasIndex("TemplateId"); + + b.ToTable("template_stages", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.Uom", b => + { + b.Property("UomId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("UomId")); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Status") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(20) + .HasColumnType("varchar(20)") + .HasDefaultValue("Active"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("UomId"); + + b.HasIndex("Name") + .IsUnique(); + + b.HasIndex("Status"); + + b.ToTable("uoms", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.User", b => + { + b.Property("UserId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("UserId")); + + b.Property("AuthUserId") + .HasColumnType("char(36)") + .HasColumnName("auth_user_id"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("Email") + .HasMaxLength(320) + .HasColumnType("varchar(320)"); + + b.Property("RoleId") + .HasColumnType("int"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("varchar(20)"); + + b.Property("Username") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.HasKey("UserId"); + + b.HasIndex("AuthUserId") + .IsUnique(); + + b.HasIndex("Email") + .IsUnique(); + + b.HasIndex("RoleId"); + + b.HasIndex("Username") + .IsUnique(); + + b.ToTable("users", (string)null); + + b.HasData( + new + { + UserId = 1, + DisplayName = "System", + Status = "Active", + Username = "system" + }); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.Vendor", b => + { + b.Property("VendorId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("VendorId")); + + b.Property("Address") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("BranchId") + .HasColumnType("int"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Currency") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(3) + .HasColumnType("varchar(3)") + .HasDefaultValue("LKR"); + + b.Property("Email") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("Mobile") + .HasMaxLength(20) + .HasColumnType("varchar(20)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("int unsigned"); + + b.Property("Status") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(20) + .HasColumnType("varchar(20)") + .HasDefaultValue("Active"); + + b.Property("TaxReg") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Terms") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("VendorId"); + + b.HasIndex("BranchId"); + + b.HasIndex("Code") + .IsUnique(); + + b.HasIndex("Status"); + + b.ToTable("vendors", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.VendorQuotation", b => + { + b.Property("QuotationId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("QuotationId")); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("RfqId") + .HasColumnType("int"); + + b.Property("VendorId") + .HasColumnType("int"); + + b.HasKey("QuotationId"); + + b.HasIndex("VendorId"); + + b.HasIndex("RfqId", "VendorId") + .IsUnique(); + + b.ToTable("vendor_quotations", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.VendorQuotationLine", b => + { + b.Property("QuotationLineId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("QuotationLineId")); + + b.Property("ItemId") + .HasColumnType("int"); + + b.Property("LeadDays") + .HasColumnType("int"); + + b.Property("QuotationId") + .HasColumnType("int"); + + b.Property("UnitPrice") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.HasKey("QuotationLineId"); + + b.HasIndex("ItemId"); + + b.HasIndex("QuotationId"); + + b.ToTable("vendor_quotation_lines", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.Warehouse", b => + { + b.Property("WarehouseId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("WarehouseId")); + + b.Property("Address") + .HasColumnType("longtext"); + + b.Property("BranchId") + .HasColumnType("int"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("Phone") + .HasColumnType("longtext"); + + b.HasKey("WarehouseId"); + + b.HasIndex("BranchId"); + + b.HasIndex("Code") + .IsUnique(); + + b.ToTable("warehouses", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.WarrantyPeriod", b => + { + b.Property("WarrantyPeriodId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("WarrantyPeriodId")); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("Months") + .HasColumnType("int"); + + b.Property("Status") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(20) + .HasColumnType("varchar(20)") + .HasDefaultValue("Active"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("WarrantyPeriodId"); + + b.HasIndex("Months") + .IsUnique(); + + b.HasIndex("Status"); + + b.ToTable("warranty_periods", null, t => + { + t.HasCheckConstraint("ck_warranty_periods_months", "`Months` BETWEEN 1 AND 600"); + }); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.WarrantyRegistration", b => + { + b.Property("WarrantyRegistrationId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("WarrantyRegistrationId")); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("CustomerId") + .HasColumnType("int"); + + b.Property("EndDate") + .HasColumnType("date"); + + b.Property("ItemId") + .HasColumnType("int"); + + b.Property("PeriodMonths") + .HasColumnType("int"); + + b.Property("Qty") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("int unsigned"); + + b.Property("SerialId") + .HasColumnType("int"); + + b.Property("SourceDocId") + .HasColumnType("int"); + + b.Property("SourceDocType") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("SourceLineId") + .HasColumnType("int"); + + b.Property("StartDate") + .HasColumnType("date"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("varchar(20)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.HasKey("WarrantyRegistrationId"); + + b.HasIndex("CustomerId"); + + b.HasIndex("ItemId"); + + b.HasIndex("SerialId"); + + b.HasIndex("SourceDocType", "SourceDocId"); + + b.HasIndex("Status", "EndDate"); + + b.ToTable("warranty_registrations", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.WorkShift", b => + { + b.Property("WorkShiftId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("WorkShiftId")); + + b.Property("BreakMinutes") + .HasColumnType("int"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("varchar(20)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("EndTime") + .HasColumnType("time(6)"); + + b.Property("GraceMinutes") + .HasColumnType("int"); + + b.Property("IsOvernight") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("OtMultiplier") + .HasPrecision(6, 2) + .HasColumnType("decimal(6,2)"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("int unsigned"); + + b.Property("StandardWorkingMinutes") + .HasColumnType("int"); + + b.Property("StartTime") + .HasColumnType("time(6)"); + + b.Property("Status") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(20) + .HasColumnType("varchar(20)") + .HasDefaultValue("Active"); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.Property("WorkingDaysMask") + .HasColumnType("int"); + + b.HasKey("WorkShiftId"); + + b.HasIndex("Code") + .IsUnique(); + + b.HasIndex("Status"); + + b.ToTable("hr_work_shifts", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.AccessConfig", b => + { + b.HasOne("ERPCore.Domain.Entities.Role", "DefaultRole") + .WithMany() + .HasForeignKey("DefaultRoleId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("ERPCore.Domain.Entities.User", "UpdatedByUser") + .WithMany() + .HasForeignKey("UpdatedBy") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("DefaultRole"); + + b.Navigation("UpdatedByUser"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.AttendanceRecord", b => + { + b.HasOne("ERPCore.Domain.Entities.AttendanceUploadBatch", "AttendanceUploadBatch") + .WithMany() + .HasForeignKey("AttendanceUploadBatchId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ERPCore.Domain.Entities.Employee", "Employee") + .WithMany() + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.WorkShift", "WorkShift") + .WithMany() + .HasForeignKey("WorkShiftId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("AttendanceUploadBatch"); + + b.Navigation("Employee"); + + b.Navigation("WorkShift"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.AuditLog", b => + { + b.HasOne("ERPCore.Domain.Entities.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.BankDeposit", b => + { + b.HasOne("ERPCore.Domain.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.User", "Creator") + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Branch"); + + b.Navigation("Creator"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.Batch", b => + { + b.HasOne("ERPCore.Domain.Entities.Item", "Item") + .WithMany() + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Item"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.Bin", b => + { + b.HasOne("ERPCore.Domain.Entities.Warehouse", "Warehouse") + .WithMany("Bins") + .HasForeignKey("WarehouseId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Warehouse"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.BranchConfig", b => + { + b.HasOne("ERPCore.Domain.Entities.User", "UpdatedByUser") + .WithMany() + .HasForeignKey("UpdatedBy") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("UpdatedByUser"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.BundleSale", b => + { + b.HasOne("ERPCore.Domain.Entities.BundleSaleTemplate", "BundleSaleTemplate") + .WithMany() + .HasForeignKey("BundleSaleTemplateId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.User", "CashierUser") + .WithMany() + .HasForeignKey("CashierUserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.Customer", "Customer") + .WithMany() + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.SalesDayEnd", "DayEnd") + .WithMany("BundleSales") + .HasForeignKey("DayEndId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("ERPCore.Domain.Entities.Warehouse", "Warehouse") + .WithMany() + .HasForeignKey("WarehouseId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("BundleSaleTemplate"); + + b.Navigation("CashierUser"); + + b.Navigation("Customer"); + + b.Navigation("DayEnd"); + + b.Navigation("Warehouse"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.BundleSaleLine", b => + { + b.HasOne("ERPCore.Domain.Entities.BundleSale", "BundleSale") + .WithMany("Lines") + .HasForeignKey("BundleSaleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.Item", "Item") + .WithMany() + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.Warehouse", "Warehouse") + .WithMany() + .HasForeignKey("WarehouseId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("BundleSale"); + + b.Navigation("Item"); + + b.Navigation("Warehouse"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.BundleSaleTemplateLine", b => + { + b.HasOne("ERPCore.Domain.Entities.BundleSaleTemplate", "BundleSaleTemplate") + .WithMany("Lines") + .HasForeignKey("BundleSaleTemplateId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.Item", "Item") + .WithMany() + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.Warehouse", "Warehouse") + .WithMany() + .HasForeignKey("WarehouseId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("BundleSaleTemplate"); + + b.Navigation("Item"); + + b.Navigation("Warehouse"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.Customer", b => + { + b.HasOne("ERPCore.Domain.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.Warehouse", "DefaultWarehouse") + .WithMany() + .HasForeignKey("DefaultWarehouseId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Branch"); + + b.Navigation("DefaultWarehouse"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.Department", b => + { + b.HasOne("ERPCore.Domain.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("ERPCore.Domain.Entities.Employee", "HeadEmployee") + .WithMany() + .HasForeignKey("HeadEmployeeId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("ERPCore.Domain.Entities.Department", "ParentDepartment") + .WithMany() + .HasForeignKey("ParentDepartmentId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("Branch"); + + b.Navigation("HeadEmployee"); + + b.Navigation("ParentDepartment"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.DocumentApproval", b => + { + b.HasOne("ERPCore.Domain.Entities.User", "Decider") + .WithMany() + .HasForeignKey("DecidedBy") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Decider"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.Employee", b => + { + b.HasOne("ERPCore.Domain.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.Department", "Department") + .WithMany() + .HasForeignKey("DepartmentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.Designation", "Designation") + .WithMany() + .HasForeignKey("DesignationId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.EmploymentType", "EmploymentType") + .WithMany() + .HasForeignKey("EmploymentTypeId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.Employee", "ReportingManager") + .WithMany() + .HasForeignKey("ReportingManagerId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("ERPCore.Domain.Entities.User", "User") + .WithOne() + .HasForeignKey("ERPCore.Domain.Entities.Employee", "UserId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("ERPCore.Domain.Entities.WorkShift", "WorkShift") + .WithMany() + .HasForeignKey("WorkShiftId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Branch"); + + b.Navigation("Department"); + + b.Navigation("Designation"); + + b.Navigation("EmploymentType"); + + b.Navigation("ReportingManager"); + + b.Navigation("User"); + + b.Navigation("WorkShift"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.EmployeeBankDetail", b => + { + b.HasOne("ERPCore.Domain.Entities.Employee", "Employee") + .WithMany() + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.EmployeeDocument", b => + { + b.HasOne("ERPCore.Domain.Entities.Employee", "Employee") + .WithMany() + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.HrDocumentType", "HrDocumentType") + .WithMany() + .HasForeignKey("HrDocumentTypeId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Employee"); + + b.Navigation("HrDocumentType"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.EmployeeLoan", b => + { + b.HasOne("ERPCore.Domain.Entities.Employee", "Employee") + .WithMany() + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.EmployeeSalaryStructure", b => + { + b.HasOne("ERPCore.Domain.Entities.Employee", "Employee") + .WithMany() + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.EmployeeSalaryStructureLine", b => + { + b.HasOne("ERPCore.Domain.Entities.EmployeeSalaryStructure", "EmployeeSalaryStructure") + .WithMany("Lines") + .HasForeignKey("EmployeeSalaryStructureId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.SalaryComponent", "SalaryComponent") + .WithMany() + .HasForeignKey("SalaryComponentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("EmployeeSalaryStructure"); + + b.Navigation("SalaryComponent"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.Expense", b => + { + b.HasOne("ERPCore.Domain.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.User", "Creator") + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.ExpenseCategory", "Category") + .WithMany() + .HasForeignKey("ExpenseCategoryId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.ExpenseTitle", "Title") + .WithMany() + .HasForeignKey("ExpenseTitleId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.Vendor", "Vendor") + .WithMany() + .HasForeignKey("VendorId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("Branch"); + + b.Navigation("Category"); + + b.Navigation("Creator"); + + b.Navigation("Title"); + + b.Navigation("Vendor"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.ExpensePayment", b => + { + b.HasOne("ERPCore.Domain.Entities.User", "Creator") + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.Expense", "Expense") + .WithMany("Payments") + .HasForeignKey("ExpenseId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Creator"); + + b.Navigation("Expense"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.ExpenseTitle", b => + { + b.HasOne("ERPCore.Domain.Entities.ExpenseCategory", "Category") + .WithMany("Titles") + .HasForeignKey("ExpenseCategoryId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Category"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.GlAccountConfig", b => + { + b.HasOne("ERPCore.Domain.Entities.User", "UpdatedByUser") + .WithMany() + .HasForeignKey("UpdatedBy") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("UpdatedByUser"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.Grn", b => + { + b.HasOne("ERPCore.Domain.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.User", "Creator") + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.PurchaseOrder", "PurchaseOrder") + .WithMany() + .HasForeignKey("PoId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("ERPCore.Domain.Entities.PurchaseInvoice", "PurchaseInvoice") + .WithMany() + .HasForeignKey("PurchaseInvoiceId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("ERPCore.Domain.Entities.Vendor", "Vendor") + .WithMany() + .HasForeignKey("VendorId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.Warehouse", "Warehouse") + .WithMany() + .HasForeignKey("WarehouseId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Branch"); + + b.Navigation("Creator"); + + b.Navigation("PurchaseInvoice"); + + b.Navigation("PurchaseOrder"); + + b.Navigation("Vendor"); + + b.Navigation("Warehouse"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.GrnConfig", b => + { + b.HasOne("ERPCore.Domain.Entities.Warehouse", "DefaultWarehouse") + .WithMany() + .HasForeignKey("DefaultWarehouseId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("ERPCore.Domain.Entities.User", "UpdatedByUser") + .WithMany() + .HasForeignKey("UpdatedBy") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("DefaultWarehouse"); + + b.Navigation("UpdatedByUser"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.GrnLine", b => + { + b.HasOne("ERPCore.Domain.Entities.Batch", "Batch") + .WithMany() + .HasForeignKey("BatchId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("ERPCore.Domain.Entities.Bin", "Bin") + .WithMany() + .HasForeignKey("BinId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("ERPCore.Domain.Entities.Grn", "Grn") + .WithMany("Lines") + .HasForeignKey("GrnId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.Item", "Item") + .WithMany() + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.PoLine", "PoLine") + .WithMany() + .HasForeignKey("PoLineId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("Batch"); + + b.Navigation("Bin"); + + b.Navigation("Grn"); + + b.Navigation("Item"); + + b.Navigation("PoLine"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.GrnLineSerial", b => + { + b.HasOne("ERPCore.Domain.Entities.GrnLine", "GrnLine") + .WithMany("Serials") + .HasForeignKey("GrnLineId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.Serial", "Serial") + .WithMany() + .HasForeignKey("SerialId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("GrnLine"); + + b.Navigation("Serial"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.GrnLineWarrantyNumber", b => + { + b.HasOne("ERPCore.Domain.Entities.GrnLine", "GrnLine") + .WithMany("WarrantyNumbers") + .HasForeignKey("GrnLineId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("GrnLine"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.Item", b => + { + b.HasOne("ERPCore.Domain.Entities.Uom", "BaseUom") + .WithMany() + .HasForeignKey("BaseUomId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.Brand", "Brand") + .WithMany() + .HasForeignKey("BrandId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("ERPCore.Domain.Entities.Category", "Category") + .WithMany() + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.Vendor", "DefaultVendor") + .WithMany() + .HasForeignKey("DefaultVendorId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("ERPCore.Domain.Entities.SubCategory", "SubCategory") + .WithMany() + .HasForeignKey("SubCategoryId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("ERPCore.Domain.Entities.TaxRate", "TaxRate") + .WithMany() + .HasForeignKey("TaxRateId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("BaseUom"); + + b.Navigation("Brand"); + + b.Navigation("Category"); + + b.Navigation("DefaultVendor"); + + b.Navigation("SubCategory"); + + b.Navigation("TaxRate"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.ItemImage", b => + { + b.HasOne("ERPCore.Domain.Entities.Item", "Item") + .WithMany() + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.ItemPriceHistory", b => + { + b.HasOne("ERPCore.Domain.Entities.Item", "Item") + .WithMany() + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.PriceAdjustment", "PriceAdjustment") + .WithMany() + .HasForeignKey("PriceAdjustmentId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("Item"); + + b.Navigation("PriceAdjustment"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.ItemReorder", b => + { + b.HasOne("ERPCore.Domain.Entities.Item", "Item") + .WithMany("ReorderSettings") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.Warehouse", "Warehouse") + .WithMany() + .HasForeignKey("WarehouseId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Item"); + + b.Navigation("Warehouse"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.LeaveBalance", b => + { + b.HasOne("ERPCore.Domain.Entities.Employee", "Employee") + .WithMany() + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.LeaveType", "LeaveType") + .WithMany() + .HasForeignKey("LeaveTypeId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Employee"); + + b.Navigation("LeaveType"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.LeaveRequest", b => + { + b.HasOne("ERPCore.Domain.Entities.Employee", "Employee") + .WithMany() + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.LeaveType", "LeaveType") + .WithMany() + .HasForeignKey("LeaveTypeId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Employee"); + + b.Navigation("LeaveType"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.LoanInstallment", b => + { + b.HasOne("ERPCore.Domain.Entities.EmployeeLoan", "EmployeeLoan") + .WithMany("Installments") + .HasForeignKey("EmployeeLoanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.PayrollRun", "PayrollRun") + .WithMany() + .HasForeignKey("PayrollRunId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("EmployeeLoan"); + + b.Navigation("PayrollRun"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.LoginActivity", b => + { + b.HasOne("ERPCore.Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.NotificationConfig", b => + { + b.HasOne("ERPCore.Domain.Entities.User", "UpdatedByUser") + .WithMany() + .HasForeignKey("UpdatedBy") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("UpdatedByUser"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.NotificationConfigModule", b => + { + b.HasOne("ERPCore.Domain.Entities.NotificationConfig", "NotificationConfig") + .WithMany("Modules") + .HasForeignKey("NotificationConfigId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("NotificationConfig"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.NotificationRecipient", b => + { + b.HasOne("ERPCore.Domain.Entities.Notification", "Notification") + .WithMany("Recipients") + .HasForeignKey("NotificationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Notification"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.NumberSequence", b => + { + b.HasOne("ERPCore.Domain.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Branch"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.OpeningBalanceLine", b => + { + b.HasOne("ERPCore.Domain.Entities.OpeningBalanceRun", "Run") + .WithMany("Lines") + .HasForeignKey("OpeningBalanceRunId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Run"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.OpeningStockLine", b => + { + b.HasOne("ERPCore.Domain.Entities.Item", "Item") + .WithMany() + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.OpeningBalanceRun", "Run") + .WithMany("StockLines") + .HasForeignKey("OpeningBalanceRunId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.Warehouse", "Warehouse") + .WithMany() + .HasForeignKey("WarehouseId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Item"); + + b.Navigation("Run"); + + b.Navigation("Warehouse"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.OwnerReportConfig", b => + { + b.HasOne("ERPCore.Domain.Entities.User", "UpdatedByUser") + .WithMany() + .HasForeignKey("UpdatedBy") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("UpdatedByUser"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.OwnerReportDispatch", b => + { + b.HasOne("ERPCore.Domain.Entities.OwnerReportRecipient", "Recipient") + .WithMany() + .HasForeignKey("RecipientId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Recipient"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.PayrollLine", b => + { + b.HasOne("ERPCore.Domain.Entities.Employee", "Employee") + .WithMany() + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.PayrollRun", "PayrollRun") + .WithMany("Lines") + .HasForeignKey("PayrollRunId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + + b.Navigation("PayrollRun"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.PayrollLineComponent", b => + { + b.HasOne("ERPCore.Domain.Entities.PayrollLine", "PayrollLine") + .WithMany("Components") + .HasForeignKey("PayrollLineId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.SalaryComponent", "SalaryComponent") + .WithMany() + .HasForeignKey("SalaryComponentId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("PayrollLine"); + + b.Navigation("SalaryComponent"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.PayrollRun", b => + { + b.HasOne("ERPCore.Domain.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("Branch"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.Payslip", b => + { + b.HasOne("ERPCore.Domain.Entities.PayrollLine", "PayrollLine") + .WithOne() + .HasForeignKey("ERPCore.Domain.Entities.Payslip", "PayrollLineId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("PayrollLine"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.Permission", b => + { + b.HasOne("ERPCore.Domain.Entities.NavItem", "NavItem") + .WithMany() + .HasForeignKey("NavItemId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("ERPCore.Domain.Entities.SubNavItem", "SubNavItem") + .WithMany() + .HasForeignKey("SubNavItemId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("NavItem"); + + b.Navigation("SubNavItem"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.PoLine", b => + { + b.HasOne("ERPCore.Domain.Entities.Item", "Item") + .WithMany() + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.PurchaseOrder", "PurchaseOrder") + .WithMany("Lines") + .HasForeignKey("PoId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.Warehouse", "Warehouse") + .WithMany() + .HasForeignKey("WarehouseId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Item"); + + b.Navigation("PurchaseOrder"); + + b.Navigation("Warehouse"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.PriceAdjustment", b => + { + b.HasOne("ERPCore.Domain.Entities.User", "Creator") + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Creator"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.PriceAdjustmentLine", b => + { + b.HasOne("ERPCore.Domain.Entities.Item", "Item") + .WithMany() + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.PriceAdjustment", "PriceAdjustment") + .WithMany("Lines") + .HasForeignKey("PriceAdjustmentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + + b.Navigation("PriceAdjustment"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.ProcurementConfig", b => + { + b.HasOne("ERPCore.Domain.Entities.User", "UpdatedByUser") + .WithMany() + .HasForeignKey("UpdatedBy") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("UpdatedByUser"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.ProductConfig", b => + { + b.HasOne("ERPCore.Domain.Entities.User", "UpdatedByUser") + .WithMany() + .HasForeignKey("UpdatedBy") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("UpdatedByUser"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.ProductionRun", b => + { + b.HasOne("ERPCore.Domain.Entities.ReasonCode", "CancelReason") + .WithMany() + .HasForeignKey("CancelReasonCodeId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("ERPCore.Domain.Entities.User", "Creator") + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.Bin", "OutputBin") + .WithMany() + .HasForeignKey("OutputBinId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("ERPCore.Domain.Entities.ProductionTemplate", "Template") + .WithMany("Runs") + .HasForeignKey("TemplateId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.Warehouse", "Warehouse") + .WithMany() + .HasForeignKey("WarehouseId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("CancelReason"); + + b.Navigation("Creator"); + + b.Navigation("OutputBin"); + + b.Navigation("Template"); + + b.Navigation("Warehouse"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.ProductionTemplate", b => + { + b.HasOne("ERPCore.Domain.Entities.User", "Creator") + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Creator"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.PurchaseInvoice", b => + { + b.HasOne("ERPCore.Domain.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.User", "Creator") + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.Vendor", "Vendor") + .WithMany() + .HasForeignKey("VendorId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Branch"); + + b.Navigation("Creator"); + + b.Navigation("Vendor"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.PurchaseInvoiceLine", b => + { + b.HasOne("ERPCore.Domain.Entities.Grn", "Grn") + .WithMany() + .HasForeignKey("GrnId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.PurchaseInvoice", "PurchaseInvoice") + .WithMany("Lines") + .HasForeignKey("PurchaseInvoiceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Grn"); + + b.Navigation("PurchaseInvoice"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.PurchaseInvoicePayment", b => + { + b.HasOne("ERPCore.Domain.Entities.User", "Creator") + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.PurchaseInvoice", "PurchaseInvoice") + .WithMany("Payments") + .HasForeignKey("PurchaseInvoiceId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Creator"); + + b.Navigation("PurchaseInvoice"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.PurchaseOrder", b => + { + b.HasOne("ERPCore.Domain.Entities.User", "Approver") + .WithMany() + .HasForeignKey("ApproverUserId"); + + b.HasOne("ERPCore.Domain.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.User", "Creator") + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.Requisition", "Requisition") + .WithMany() + .HasForeignKey("RequisitionId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("ERPCore.Domain.Entities.Vendor", "Vendor") + .WithMany() + .HasForeignKey("VendorId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Approver"); + + b.Navigation("Branch"); + + b.Navigation("Creator"); + + b.Navigation("Requisition"); + + b.Navigation("Vendor"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.PurchaseReturn", b => + { + b.HasOne("ERPCore.Domain.Entities.User", "Creator") + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.ReasonCode", "ReasonCode") + .WithMany() + .HasForeignKey("ReasonCodeId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.Vendor", "Vendor") + .WithMany() + .HasForeignKey("VendorId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.Warehouse", "Warehouse") + .WithMany() + .HasForeignKey("WarehouseId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Creator"); + + b.Navigation("ReasonCode"); + + b.Navigation("Vendor"); + + b.Navigation("Warehouse"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.PurchaseReturnLine", b => + { + b.HasOne("ERPCore.Domain.Entities.GrnLine", "GrnLine") + .WithMany() + .HasForeignKey("GrnLineId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("ERPCore.Domain.Entities.Item", "Item") + .WithMany() + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.PurchaseReturn", "Return") + .WithMany("Lines") + .HasForeignKey("ReturnId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("GrnLine"); + + b.Navigation("Item"); + + b.Navigation("Return"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.Requisition", b => + { + b.HasOne("ERPCore.Domain.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.User", "Requester") + .WithMany() + .HasForeignKey("RequestedBy") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Branch"); + + b.Navigation("Requester"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.RequisitionLine", b => + { + b.HasOne("ERPCore.Domain.Entities.Item", "Item") + .WithMany() + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.Requisition", "Requisition") + .WithMany("Lines") + .HasForeignKey("RequisitionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + + b.Navigation("Requisition"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.Rfq", b => + { + b.HasOne("ERPCore.Domain.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.Requisition", "Requisition") + .WithMany() + .HasForeignKey("RequisitionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Branch"); + + b.Navigation("Requisition"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.RfqLine", b => + { + b.HasOne("ERPCore.Domain.Entities.Item", "Item") + .WithMany() + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.Rfq", "Rfq") + .WithMany("Lines") + .HasForeignKey("RfqId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + + b.Navigation("Rfq"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.RolePermission", b => + { + b.HasOne("ERPCore.Domain.Entities.Permission", "Permission") + .WithMany() + .HasForeignKey("PermissionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.Role", "Role") + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Permission"); + + b.Navigation("Role"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.RunEdge", b => + { + b.HasOne("ERPCore.Domain.Entities.RunStage", "ChildRunStage") + .WithMany() + .HasForeignKey("ChildRunStageId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.RunStage", "ParentRunStage") + .WithMany() + .HasForeignKey("ParentRunStageId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.ProductionRun", "Run") + .WithMany("Edges") + .HasForeignKey("RunId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ChildRunStage"); + + b.Navigation("ParentRunStage"); + + b.Navigation("Run"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.RunStage", b => + { + b.HasOne("ERPCore.Domain.Entities.ProductionRun", "Run") + .WithMany("Stages") + .HasForeignKey("RunId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.TemplateStage", "TemplateStage") + .WithMany() + .HasForeignKey("TemplateStageId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Run"); + + b.Navigation("TemplateStage"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.RunStageEvent", b => + { + b.HasOne("ERPCore.Domain.Entities.ProductionRun", "Run") + .WithMany("Events") + .HasForeignKey("RunId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.RunStage", "RunStage") + .WithMany("Events") + .HasForeignKey("RunStageId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("ERPCore.Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Run"); + + b.Navigation("RunStage"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.RunStageInput", b => + { + b.HasOne("ERPCore.Domain.Entities.RunStageOutput", "FromRunOutput") + .WithMany() + .HasForeignKey("FromRunOutputId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("ERPCore.Domain.Entities.Item", "Item") + .WithMany() + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("ERPCore.Domain.Entities.RunStage", "RunStage") + .WithMany("Inputs") + .HasForeignKey("RunStageId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("FromRunOutput"); + + b.Navigation("Item"); + + b.Navigation("RunStage"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.RunStageOutput", b => + { + b.HasOne("ERPCore.Domain.Entities.Item", "Item") + .WithMany() + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("ERPCore.Domain.Entities.RunStage", "RunStage") + .WithMany("Outputs") + .HasForeignKey("RunStageId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.ReasonCode", "ScrapReason") + .WithMany() + .HasForeignKey("ScrapReasonCodeId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("ERPCore.Domain.Entities.Uom", "Uom") + .WithMany() + .HasForeignKey("UomId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("Item"); + + b.Navigation("RunStage"); + + b.Navigation("ScrapReason"); + + b.Navigation("Uom"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.SalesConfig", b => + { + b.HasOne("ERPCore.Domain.Entities.Customer", "CashCustomer") + .WithMany() + .HasForeignKey("CashCustomerId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("ERPCore.Domain.Entities.Warehouse", "DefaultWarehouse") + .WithMany() + .HasForeignKey("DefaultWarehouseId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("ERPCore.Domain.Entities.User", "UpdatedByUser") + .WithMany() + .HasForeignKey("UpdatedBy") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("CashCustomer"); + + b.Navigation("DefaultWarehouse"); + + b.Navigation("UpdatedByUser"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.SalesDayEnd", b => + { + b.HasOne("ERPCore.Domain.Entities.User", "CashierUser") + .WithMany() + .HasForeignKey("CashierUserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.User", "ClosedByUser") + .WithMany() + .HasForeignKey("ClosedBy") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("CashierUser"); + + b.Navigation("ClosedByUser"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.SalesInvoice", b => + { + b.HasOne("ERPCore.Domain.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.User", "Creator") + .WithMany() + .HasForeignKey("CreatorUserId"); + + b.HasOne("ERPCore.Domain.Entities.Customer", "Customer") + .WithMany() + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.Warehouse", "Warehouse") + .WithMany() + .HasForeignKey("WarehouseId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Branch"); + + b.Navigation("Creator"); + + b.Navigation("Customer"); + + b.Navigation("Warehouse"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.SalesInvoiceLine", b => + { + b.HasOne("ERPCore.Domain.Entities.Batch", "Batch") + .WithMany() + .HasForeignKey("BatchId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("ERPCore.Domain.Entities.Item", "Item") + .WithMany() + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.SalesInvoice", "SalesInvoice") + .WithMany("Lines") + .HasForeignKey("SalesInvoiceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.Warehouse", "Warehouse") + .WithMany() + .HasForeignKey("WarehouseId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Batch"); + + b.Navigation("Item"); + + b.Navigation("SalesInvoice"); + + b.Navigation("Warehouse"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.SalesInvoiceLineSerial", b => + { + b.HasOne("ERPCore.Domain.Entities.SalesInvoiceLine", "SalesInvoiceLine") + .WithMany("Serials") + .HasForeignKey("SalesInvoiceLineId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.Serial", "Serial") + .WithMany() + .HasForeignKey("SerialId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("SalesInvoiceLine"); + + b.Navigation("Serial"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.SalesInvoicePayment", b => + { + b.HasOne("ERPCore.Domain.Entities.User", "Creator") + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.SalesInvoice", "SalesInvoice") + .WithMany("Payments") + .HasForeignKey("SalesInvoiceId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Creator"); + + b.Navigation("SalesInvoice"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.SalesLineAllocation", b => + { + b.HasOne("ERPCore.Domain.Entities.Batch", "Batch") + .WithMany() + .HasForeignKey("BatchId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("ERPCore.Domain.Entities.Serial", "Serial") + .WithMany() + .HasForeignKey("SerialId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("Batch"); + + b.Navigation("Serial"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.SalesReturn", b => + { + b.HasOne("ERPCore.Domain.Entities.User", "Creator") + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.Customer", "Customer") + .WithMany() + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.ReasonCode", "ReasonCode") + .WithMany() + .HasForeignKey("ReasonCodeId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.Warehouse", "Warehouse") + .WithMany() + .HasForeignKey("WarehouseId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Creator"); + + b.Navigation("Customer"); + + b.Navigation("ReasonCode"); + + b.Navigation("Warehouse"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.SalesReturnLine", b => + { + b.HasOne("ERPCore.Domain.Entities.Batch", "Batch") + .WithMany() + .HasForeignKey("BatchId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("ERPCore.Domain.Entities.Item", "Item") + .WithMany() + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.SalesReturn", "Return") + .WithMany("Lines") + .HasForeignKey("ReturnId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.SalesInvoiceLine", "SalesInvoiceLine") + .WithMany() + .HasForeignKey("SalesInvoiceLineId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("Batch"); + + b.Navigation("Item"); + + b.Navigation("Return"); + + b.Navigation("SalesInvoiceLine"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.SalesReturnLineSerial", b => + { + b.HasOne("ERPCore.Domain.Entities.SalesReturnLine", "ReturnLine") + .WithMany("Serials") + .HasForeignKey("ReturnLineId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.Serial", "Serial") + .WithMany() + .HasForeignKey("SerialId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("ReturnLine"); + + b.Navigation("Serial"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.SalesSlip", b => + { + b.HasOne("ERPCore.Domain.Entities.User", "CashierUser") + .WithMany() + .HasForeignKey("CashierUserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.Customer", "Customer") + .WithMany() + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.SalesDayEnd", "DayEnd") + .WithMany("Slips") + .HasForeignKey("DayEndId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("ERPCore.Domain.Entities.Warehouse", "Warehouse") + .WithMany() + .HasForeignKey("WarehouseId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("CashierUser"); + + b.Navigation("Customer"); + + b.Navigation("DayEnd"); + + b.Navigation("Warehouse"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.SalesSlipLine", b => + { + b.HasOne("ERPCore.Domain.Entities.Batch", "Batch") + .WithMany() + .HasForeignKey("BatchId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("ERPCore.Domain.Entities.Item", "Item") + .WithMany() + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.SalesSlip", "SalesSlip") + .WithMany("Lines") + .HasForeignKey("SalesSlipId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.Serial", "Serial") + .WithMany() + .HasForeignKey("SerialId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("ERPCore.Domain.Entities.Warehouse", "Warehouse") + .WithMany() + .HasForeignKey("WarehouseId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Batch"); + + b.Navigation("Item"); + + b.Navigation("SalesSlip"); + + b.Navigation("Serial"); + + b.Navigation("Warehouse"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.SalesSlipLineSerial", b => + { + b.HasOne("ERPCore.Domain.Entities.SalesSlipLine", "SalesSlipLine") + .WithMany("Serials") + .HasForeignKey("SalesSlipLineId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.Serial", "Serial") + .WithMany() + .HasForeignKey("SerialId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("SalesSlipLine"); + + b.Navigation("Serial"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.Serial", b => + { + b.HasOne("ERPCore.Domain.Entities.Bin", "Bin") + .WithMany() + .HasForeignKey("BinId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("ERPCore.Domain.Entities.GrnLine", "GrnLine") + .WithMany() + .HasForeignKey("GrnLineId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("ERPCore.Domain.Entities.Item", "Item") + .WithMany() + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.Warehouse", "Warehouse") + .WithMany() + .HasForeignKey("WarehouseId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("Bin"); + + b.Navigation("GrnLine"); + + b.Navigation("Item"); + + b.Navigation("Warehouse"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.SerialMovement", b => + { + b.HasOne("ERPCore.Domain.Entities.Serial", "Serial") + .WithMany() + .HasForeignKey("SerialId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Serial"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.StageEdge", b => + { + b.HasOne("ERPCore.Domain.Entities.TemplateStage", "ChildStage") + .WithMany() + .HasForeignKey("ChildStageId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.TemplateStage", "ParentStage") + .WithMany() + .HasForeignKey("ParentStageId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.ProductionTemplate", "Template") + .WithMany("Edges") + .HasForeignKey("TemplateId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ChildStage"); + + b.Navigation("ParentStage"); + + b.Navigation("Template"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.StageInput", b => + { + b.HasOne("ERPCore.Domain.Entities.StageOutput", "FromOutput") + .WithMany() + .HasForeignKey("FromOutputId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("ERPCore.Domain.Entities.Item", "Item") + .WithMany() + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("ERPCore.Domain.Entities.TemplateStage", "Stage") + .WithMany("Inputs") + .HasForeignKey("StageId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("FromOutput"); + + b.Navigation("Item"); + + b.Navigation("Stage"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.StageOutput", b => + { + b.HasOne("ERPCore.Domain.Entities.Item", "Item") + .WithMany() + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("ERPCore.Domain.Entities.TemplateStage", "Stage") + .WithMany("Outputs") + .HasForeignKey("StageId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.Uom", "Uom") + .WithMany() + .HasForeignKey("UomId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("Item"); + + b.Navigation("Stage"); + + b.Navigation("Uom"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.StockAdjustment", b => + { + b.HasOne("ERPCore.Domain.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.User", "Creator") + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.ReasonCode", "ReasonCode") + .WithMany() + .HasForeignKey("ReasonCodeId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.Warehouse", "Warehouse") + .WithMany() + .HasForeignKey("WarehouseId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Branch"); + + b.Navigation("Creator"); + + b.Navigation("ReasonCode"); + + b.Navigation("Warehouse"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.StockAdjustmentLine", b => + { + b.HasOne("ERPCore.Domain.Entities.StockAdjustment", "Adjustment") + .WithMany("Lines") + .HasForeignKey("AdjustmentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.Batch", null) + .WithMany() + .HasForeignKey("BatchId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("ERPCore.Domain.Entities.Bin", null) + .WithMany() + .HasForeignKey("BinId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("ERPCore.Domain.Entities.Item", "Item") + .WithMany() + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.Serial", "Serial") + .WithMany() + .HasForeignKey("SerialId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("Adjustment"); + + b.Navigation("Item"); + + b.Navigation("Serial"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.StockCount", b => + { + b.HasOne("ERPCore.Domain.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.User", "Creator") + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.Warehouse", "Warehouse") + .WithMany() + .HasForeignKey("WarehouseId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Branch"); + + b.Navigation("Creator"); + + b.Navigation("Warehouse"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.StockCountLine", b => + { + b.HasOne("ERPCore.Domain.Entities.Batch", "Batch") + .WithMany() + .HasForeignKey("BatchId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("ERPCore.Domain.Entities.Bin", null) + .WithMany() + .HasForeignKey("BinId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("ERPCore.Domain.Entities.StockCount", "Count") + .WithMany("Lines") + .HasForeignKey("CountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.Item", "Item") + .WithMany() + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.Serial", "Serial") + .WithMany() + .HasForeignKey("SerialId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("Batch"); + + b.Navigation("Count"); + + b.Navigation("Item"); + + b.Navigation("Serial"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.StockLayer", b => + { + b.HasOne("ERPCore.Domain.Entities.Batch", "Batch") + .WithMany() + .HasForeignKey("BatchId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("ERPCore.Domain.Entities.GrnLine", "GrnLine") + .WithMany() + .HasForeignKey("GrnLineId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("ERPCore.Domain.Entities.Item", "Item") + .WithMany() + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.Serial", "Serial") + .WithMany() + .HasForeignKey("SerialId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("ERPCore.Domain.Entities.Warehouse", "Warehouse") + .WithMany() + .HasForeignKey("WarehouseId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Batch"); + + b.Navigation("GrnLine"); + + b.Navigation("Item"); + + b.Navigation("Serial"); + + b.Navigation("Warehouse"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.StockLedger", b => + { + b.HasOne("ERPCore.Domain.Entities.Batch", null) + .WithMany() + .HasForeignKey("BatchId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("ERPCore.Domain.Entities.Bin", null) + .WithMany() + .HasForeignKey("BinId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("ERPCore.Domain.Entities.Item", null) + .WithMany() + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.Serial", null) + .WithMany() + .HasForeignKey("SerialId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("ERPCore.Domain.Entities.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.Warehouse", null) + .WithMany() + .HasForeignKey("WarehouseId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.StockTransfer", b => + { + b.HasOne("ERPCore.Domain.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.User", "Creator") + .WithMany() + .HasForeignKey("CreatedBy") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.Warehouse", "DestWarehouse") + .WithMany() + .HasForeignKey("DestWarehouseId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.Warehouse", "SrcWarehouse") + .WithMany() + .HasForeignKey("SrcWarehouseId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Branch"); + + b.Navigation("Creator"); + + b.Navigation("DestWarehouse"); + + b.Navigation("SrcWarehouse"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.StockTransferLine", b => + { + b.HasOne("ERPCore.Domain.Entities.Batch", null) + .WithMany() + .HasForeignKey("BatchId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("ERPCore.Domain.Entities.Bin", null) + .WithMany() + .HasForeignKey("DestBinId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("ERPCore.Domain.Entities.Item", "Item") + .WithMany() + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.Serial", null) + .WithMany() + .HasForeignKey("SerialId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("ERPCore.Domain.Entities.Bin", null) + .WithMany() + .HasForeignKey("SrcBinId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("ERPCore.Domain.Entities.StockTransfer", "Transfer") + .WithMany("Lines") + .HasForeignKey("TransferId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + + b.Navigation("Transfer"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.StockTransferLineSerial", b => + { + b.HasOne("ERPCore.Domain.Entities.Batch", null) + .WithMany() + .HasForeignKey("BatchId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("ERPCore.Domain.Entities.Serial", "Serial") + .WithMany() + .HasForeignKey("SerialId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.StockTransferLine", "TransferLine") + .WithMany("Serials") + .HasForeignKey("TransferLineId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Serial"); + + b.Navigation("TransferLine"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.SubCategory", b => + { + b.HasOne("ERPCore.Domain.Entities.Category", "Category") + .WithMany("SubCategories") + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Category"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.SubNavItem", b => + { + b.HasOne("ERPCore.Domain.Entities.NavItem", "NavItem") + .WithMany("Children") + .HasForeignKey("NavItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("NavItem"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.SupportTicketMessage", b => + { + b.HasOne("ERPCore.Domain.Entities.SupportTicket", "Ticket") + .WithMany("Messages") + .HasForeignKey("TicketId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Ticket"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.TemplateStage", b => + { + b.HasOne("ERPCore.Domain.Entities.ProductionTemplate", "Template") + .WithMany("Stages") + .HasForeignKey("TemplateId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Template"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.User", b => + { + b.HasOne("ERPCore.Domain.Entities.Role", "Role") + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("Role"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.Vendor", b => + { + b.HasOne("ERPCore.Domain.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Branch"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.VendorQuotation", b => + { + b.HasOne("ERPCore.Domain.Entities.Rfq", "Rfq") + .WithMany("Quotations") + .HasForeignKey("RfqId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.Vendor", "Vendor") + .WithMany() + .HasForeignKey("VendorId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Rfq"); + + b.Navigation("Vendor"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.VendorQuotationLine", b => + { + b.HasOne("ERPCore.Domain.Entities.Item", "Item") + .WithMany() + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.VendorQuotation", "Quotation") + .WithMany("Lines") + .HasForeignKey("QuotationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Item"); + + b.Navigation("Quotation"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.Warehouse", b => + { + b.HasOne("ERPCore.Domain.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Branch"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.WarrantyRegistration", b => + { + b.HasOne("ERPCore.Domain.Entities.Customer", "Customer") + .WithMany() + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.Item", "Item") + .WithMany() + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ERPCore.Domain.Entities.Serial", "Serial") + .WithMany() + .HasForeignKey("SerialId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("Customer"); + + b.Navigation("Item"); + + b.Navigation("Serial"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.BundleSale", b => + { + b.Navigation("Lines"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.BundleSaleTemplate", b => + { + b.Navigation("Lines"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.Category", b => + { + b.Navigation("SubCategories"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.EmployeeLoan", b => + { + b.Navigation("Installments"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.EmployeeSalaryStructure", b => + { + b.Navigation("Lines"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.Expense", b => + { + b.Navigation("Payments"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.ExpenseCategory", b => + { + b.Navigation("Titles"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.Grn", b => + { + b.Navigation("Lines"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.GrnLine", b => + { + b.Navigation("Serials"); + + b.Navigation("WarrantyNumbers"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.Item", b => + { + b.Navigation("ReorderSettings"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.NavItem", b => + { + b.Navigation("Children"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.Notification", b => + { + b.Navigation("Recipients"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.NotificationConfig", b => + { + b.Navigation("Modules"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.OpeningBalanceRun", b => + { + b.Navigation("Lines"); + + b.Navigation("StockLines"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.PayrollLine", b => + { + b.Navigation("Components"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.PayrollRun", b => + { + b.Navigation("Lines"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.PriceAdjustment", b => + { + b.Navigation("Lines"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.ProductionRun", b => + { + b.Navigation("Edges"); + + b.Navigation("Events"); + + b.Navigation("Stages"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.ProductionTemplate", b => + { + b.Navigation("Edges"); + + b.Navigation("Runs"); + + b.Navigation("Stages"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.PurchaseInvoice", b => + { + b.Navigation("Lines"); + + b.Navigation("Payments"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.PurchaseOrder", b => + { + b.Navigation("Lines"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.PurchaseReturn", b => + { + b.Navigation("Lines"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.Requisition", b => + { + b.Navigation("Lines"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.Rfq", b => + { + b.Navigation("Lines"); + + b.Navigation("Quotations"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.RunStage", b => + { + b.Navigation("Events"); + + b.Navigation("Inputs"); + + b.Navigation("Outputs"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.SalesDayEnd", b => + { + b.Navigation("BundleSales"); + + b.Navigation("Slips"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.SalesInvoice", b => + { + b.Navigation("Lines"); + + b.Navigation("Payments"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.SalesInvoiceLine", b => + { + b.Navigation("Serials"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.SalesReturn", b => + { + b.Navigation("Lines"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.SalesReturnLine", b => + { + b.Navigation("Serials"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.SalesSlip", b => + { + b.Navigation("Lines"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.SalesSlipLine", b => + { + b.Navigation("Serials"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.StockAdjustment", b => + { + b.Navigation("Lines"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.StockCount", b => + { + b.Navigation("Lines"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.StockTransfer", b => + { + b.Navigation("Lines"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.StockTransferLine", b => + { + b.Navigation("Serials"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.SupportTicket", b => + { + b.Navigation("Messages"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.TemplateStage", b => + { + b.Navigation("Inputs"); + + b.Navigation("Outputs"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.VendorQuotation", b => + { + b.Navigation("Lines"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.Warehouse", b => + { + b.Navigation("Bins"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Backend/ERPCore/Migrations/20261019053150_AddOwnerReport.cs b/Backend/ERPCore/Migrations/20261019053150_AddOwnerReport.cs new file mode 100644 index 0000000..1ac0b7d --- /dev/null +++ b/Backend/ERPCore/Migrations/20261019053150_AddOwnerReport.cs @@ -0,0 +1,131 @@ +using System; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace ERPCore.Migrations +{ + /// + public partial class AddOwnerReport : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "owner_report_config", + columns: table => new + { + ConfigId = table.Column(type: "int", nullable: false), + Enabled = table.Column(type: "tinyint(1)", nullable: false, defaultValue: false), + SmsEnabled = table.Column(type: "tinyint(1)", nullable: false, defaultValue: true), + EmailEnabled = table.Column(type: "tinyint(1)", nullable: false, defaultValue: true), + SendAtLocalTime = table.Column(type: "time(6)", nullable: false), + IncludeBranchBreakdown = table.Column(type: "tinyint(1)", nullable: false, defaultValue: true), + UpdatedAt = table.Column(type: "datetime(6)", nullable: true), + UpdatedBy = table.Column(type: "int", nullable: true), + RowVersion = table.Column(type: "int unsigned", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_owner_report_config", x => x.ConfigId); + table.CheckConstraint("ck_owner_report_config_singleton", "`ConfigId` = 1"); + table.ForeignKey( + name: "FK_owner_report_config_users_UpdatedBy", + column: x => x.UpdatedBy, + principalTable: "users", + principalColumn: "UserId", + onDelete: ReferentialAction.Restrict); + }) + .Annotation("Relational:Collation", "utf8mb4_unicode_ci"); + + migrationBuilder.CreateTable( + name: "owner_report_recipients", + columns: table => new + { + RecipientId = table.Column(type: "int", nullable: false) + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn), + Name = table.Column(type: "varchar(100)", maxLength: 100, nullable: false, collation: "utf8mb4_unicode_ci"), + Mobile = table.Column(type: "varchar(20)", maxLength: 20, nullable: true, collation: "utf8mb4_unicode_ci"), + Email = table.Column(type: "varchar(200)", maxLength: 200, nullable: true, collation: "utf8mb4_unicode_ci"), + IsActive = table.Column(type: "tinyint(1)", nullable: false, defaultValue: true) + }, + constraints: table => + { + table.PrimaryKey("PK_owner_report_recipients", x => x.RecipientId); + }) + .Annotation("Relational:Collation", "utf8mb4_unicode_ci"); + + migrationBuilder.CreateTable( + name: "owner_report_dispatches", + columns: table => new + { + DispatchId = table.Column(type: "bigint", nullable: false) + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn), + BusinessDate = table.Column(type: "date", nullable: false), + Channel = table.Column(type: "varchar(10)", maxLength: 10, nullable: false, collation: "utf8mb4_unicode_ci"), + RecipientId = table.Column(type: "int", nullable: true), + Destination = table.Column(type: "varchar(200)", maxLength: 200, nullable: false, collation: "utf8mb4_unicode_ci"), + RecipientName = table.Column(type: "varchar(100)", maxLength: 100, nullable: false, collation: "utf8mb4_unicode_ci"), + Status = table.Column(type: "varchar(10)", maxLength: 10, nullable: false, collation: "utf8mb4_unicode_ci"), + Attempts = table.Column(type: "int", nullable: false), + Error = table.Column(type: "varchar(1000)", maxLength: 1000, nullable: true, collation: "utf8mb4_unicode_ci"), + IsTest = table.Column(type: "tinyint(1)", nullable: false), + DedupeKey = table.Column(type: "varchar(40)", maxLength: 40, nullable: false, collation: "utf8mb4_unicode_ci"), + CreatedAt = table.Column(type: "datetime(6)", nullable: false), + SentAt = table.Column(type: "datetime(6)", nullable: true), + NextAttemptAt = table.Column(type: "datetime(6)", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_owner_report_dispatches", x => x.DispatchId); + table.ForeignKey( + name: "FK_owner_report_dispatches_owner_report_recipients_RecipientId", + column: x => x.RecipientId, + principalTable: "owner_report_recipients", + principalColumn: "RecipientId", + onDelete: ReferentialAction.SetNull); + }) + .Annotation("Relational:Collation", "utf8mb4_unicode_ci"); + + migrationBuilder.CreateIndex( + name: "IX_owner_report_config_UpdatedBy", + table: "owner_report_config", + column: "UpdatedBy"); + + migrationBuilder.CreateIndex( + name: "IX_owner_report_dispatches_CreatedAt", + table: "owner_report_dispatches", + column: "CreatedAt"); + + migrationBuilder.CreateIndex( + name: "IX_owner_report_dispatches_RecipientId", + table: "owner_report_dispatches", + column: "RecipientId"); + + migrationBuilder.CreateIndex( + name: "IX_owner_report_dispatches_Status_NextAttemptAt", + table: "owner_report_dispatches", + columns: new[] { "Status", "NextAttemptAt" }); + + migrationBuilder.CreateIndex( + name: "ux_owner_report_dispatch_once", + table: "owner_report_dispatches", + columns: new[] { "BusinessDate", "Channel", "RecipientId", "IsTest", "DedupeKey" }, + unique: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "owner_report_config"); + + migrationBuilder.DropTable( + name: "owner_report_dispatches"); + + migrationBuilder.DropTable( + name: "owner_report_recipients"); + } + } +} diff --git a/Backend/ERPCore/Migrations/ErpDbContextModelSnapshot.cs b/Backend/ERPCore/Migrations/ErpDbContextModelSnapshot.cs index 92866ba..e3b4494 100644 --- a/Backend/ERPCore/Migrations/ErpDbContextModelSnapshot.cs +++ b/Backend/ERPCore/Migrations/ErpDbContextModelSnapshot.cs @@ -3733,6 +3733,158 @@ namespace ERPCore.Migrations b.ToTable("opening_stock_lines", (string)null); }); + modelBuilder.Entity("ERPCore.Domain.Entities.OwnerReportConfig", b => + { + b.Property("ConfigId") + .HasColumnType("int"); + + b.Property("EmailEnabled") + .ValueGeneratedOnAdd() + .HasColumnType("tinyint(1)") + .HasDefaultValue(true); + + b.Property("Enabled") + .ValueGeneratedOnAdd() + .HasColumnType("tinyint(1)") + .HasDefaultValue(false); + + b.Property("IncludeBranchBreakdown") + .ValueGeneratedOnAdd() + .HasColumnType("tinyint(1)") + .HasDefaultValue(true); + + b.Property("RowVersion") + .IsConcurrencyToken() + .HasColumnType("int unsigned"); + + b.Property("SendAtLocalTime") + .HasColumnType("time(6)"); + + b.Property("SmsEnabled") + .ValueGeneratedOnAdd() + .HasColumnType("tinyint(1)") + .HasDefaultValue(true); + + b.Property("UpdatedAt") + .HasColumnType("datetime(6)"); + + b.Property("UpdatedBy") + .HasColumnType("int"); + + b.HasKey("ConfigId"); + + b.HasIndex("UpdatedBy"); + + b.ToTable("owner_report_config", null, t => + { + t.HasCheckConstraint("ck_owner_report_config_singleton", "`ConfigId` = 1"); + }); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.OwnerReportDispatch", b => + { + b.Property("DispatchId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("DispatchId")); + + b.Property("Attempts") + .HasColumnType("int"); + + b.Property("BusinessDate") + .HasColumnType("date"); + + b.Property("Channel") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("varchar(10)"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("DedupeKey") + .IsRequired() + .HasMaxLength(40) + .HasColumnType("varchar(40)"); + + b.Property("Destination") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("Error") + .HasMaxLength(1000) + .HasColumnType("varchar(1000)"); + + b.Property("IsTest") + .HasColumnType("tinyint(1)"); + + b.Property("NextAttemptAt") + .HasColumnType("datetime(6)"); + + b.Property("RecipientId") + .HasColumnType("int"); + + b.Property("RecipientName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("SentAt") + .HasColumnType("datetime(6)"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("varchar(10)"); + + b.HasKey("DispatchId"); + + b.HasIndex("CreatedAt"); + + b.HasIndex("RecipientId"); + + b.HasIndex("Status", "NextAttemptAt"); + + b.HasIndex("BusinessDate", "Channel", "RecipientId", "IsTest", "DedupeKey") + .IsUnique() + .HasDatabaseName("ux_owner_report_dispatch_once"); + + b.ToTable("owner_report_dispatches", (string)null); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.OwnerReportRecipient", b => + { + b.Property("RecipientId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("RecipientId")); + + b.Property("Email") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("IsActive") + .ValueGeneratedOnAdd() + .HasColumnType("tinyint(1)") + .HasDefaultValue(true); + + b.Property("Mobile") + .HasMaxLength(20) + .HasColumnType("varchar(20)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.HasKey("RecipientId"); + + b.ToTable("owner_report_recipients", (string)null); + }); + modelBuilder.Entity("ERPCore.Domain.Entities.PayrollLine", b => { b.Property("PayrollLineId") @@ -9631,6 +9783,26 @@ namespace ERPCore.Migrations b.Navigation("Warehouse"); }); + modelBuilder.Entity("ERPCore.Domain.Entities.OwnerReportConfig", b => + { + b.HasOne("ERPCore.Domain.Entities.User", "UpdatedByUser") + .WithMany() + .HasForeignKey("UpdatedBy") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("UpdatedByUser"); + }); + + modelBuilder.Entity("ERPCore.Domain.Entities.OwnerReportDispatch", b => + { + b.HasOne("ERPCore.Domain.Entities.OwnerReportRecipient", "Recipient") + .WithMany() + .HasForeignKey("RecipientId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Recipient"); + }); + modelBuilder.Entity("ERPCore.Domain.Entities.PayrollLine", b => { b.HasOne("ERPCore.Domain.Entities.Employee", "Employee") diff --git a/Backend/ERPCore/Program.cs b/Backend/ERPCore/Program.cs index b58ed8e..fdfcdd8 100644 --- a/Backend/ERPCore/Program.cs +++ b/Backend/ERPCore/Program.cs @@ -50,9 +50,17 @@ if (string.IsNullOrWhiteSpace(connectionString)) "ConnectionStrings:DefaultConnection is not configured. Set it in appsettings..json " + "or as the ConnectionStrings__DefaultConnection environment variable."); +// Optional override for a different local server, e.g. "8.0.44-mysql" in user-secrets +// (Database:ServerVersion) — Pomelo otherwise emits MariaDB-only SQL (INSERT … RETURNING) that +// MySQL rejects. Unset everywhere else, so the MariaDB 10.11 default above still applies. +var serverVersionOverride = builder.Configuration["Database:ServerVersion"]; +ServerVersion serverVersion = string.IsNullOrWhiteSpace(serverVersionOverride) + ? new MariaDbServerVersion(new Version(10, 11, 15)) + : ServerVersion.Parse(serverVersionOverride); + builder.Services.AddDbContext(o => { - o.UseMySql(connectionString, new MariaDbServerVersion(new Version(10, 11, 15))); + o.UseMySql(connectionString, serverVersion); }); // ProblemDetails (RFC 7807) + domain-exception mapping @@ -126,6 +134,16 @@ builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); + +// Day-end owner report (docs/DAY_END_OWNER_REPORT_PLAN.md). Messages go through AuthHex, which +// holds the SMTP / SMS credentials; Messaging:Provider = "Log" writes them to the log instead. +builder.Services.AddScoped(); +builder.Services.AddScoped(); +if (string.Equals(builder.Configuration["Messaging:Provider"], "Log", StringComparison.OrdinalIgnoreCase)) + builder.Services.AddScoped(); +else + builder.Services.AddScoped(); +builder.Services.AddHostedService(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); diff --git a/Backend/ERPCore/Services/OwnerReport/OwnerReportContentBuilder.cs b/Backend/ERPCore/Services/OwnerReport/OwnerReportContentBuilder.cs new file mode 100644 index 0000000..69fa6a0 --- /dev/null +++ b/Backend/ERPCore/Services/OwnerReport/OwnerReportContentBuilder.cs @@ -0,0 +1,193 @@ +using System.Globalization; +using System.Net; +using System.Text; +using ERPCore.Domain.Entities; +using ERPCore.Domain.Enums; +using ERPCore.Dtos.Reports; +using ERPCore.Repositories.Interfaces; +using ERPCore.Services.Interfaces; +using Microsoft.EntityFrameworkCore; + +namespace ERPCore.Services.OwnerReport; + +/// The day-end report rendered for each channel. +public sealed record OwnerReportContent(DateOnly Date, string SmsText, string EmailSubject, string EmailHtml, string EmailText); + +/// +/// Turns one day's into the owner SMS (short, ASCII so it stays a +/// cheap GSM message) and email (full detail) — docs/DAY_END_OWNER_REPORT_PLAN.md §1 / §3.3. +/// Figures come straight from , so they always match the +/// Day-End Report screen for the same day. +/// +public sealed class OwnerReportContentBuilder +{ + public const string AppName = "Hexa ERP"; + private const int TopSales = 10; + private static readonly CultureInfo Inv = CultureInfo.InvariantCulture; + + private readonly IDayEndReportService _report; + private readonly IRepository _branches; + private readonly IRepository _branchConfig; + private readonly IConfiguration _configuration; + + public OwnerReportContentBuilder( + IDayEndReportService report, IRepository branches, IRepository branchConfig, IConfiguration configuration) + { + _report = report; + _branches = branches; + _branchConfig = branchConfig; + _configuration = configuration; + } + + public async Task BuildAsync(DateOnly date, bool includeBranchBreakdown, CancellationToken ct) + { + // branchId null = every branch. Inside the background job there is no signed-in user, so + // ICurrentBranchContext is inactive and this really is the company-wide total. + var day = await _report.GetDayAsync(date, null, ct); + + var branchRows = new List<(string Name, DayEndReportDayDto Summary)>(); + var branchingOn = await _branchConfig.Query().AsNoTracking() + .Where(c => c.ConfigId == BranchConfig.SingletonId).Select(c => c.BranchingEnabled).FirstOrDefaultAsync(ct); + if (includeBranchBreakdown && branchingOn) + { + var branches = await _branches.Query().AsNoTracking() + .Where(b => b.Status == EntityStatus.Active).OrderBy(b => b.Name).Select(b => new { b.BranchId, b.Name }).ToListAsync(ct); + if (branches.Count > 1) + foreach (var b in branches) + branchRows.Add((b.Name, (await _report.GetDayAsync(date, b.BranchId, ct)).Summary)); + } + + var s = day.Summary; + var subject = $"Day-end report - {date.ToString("dd MMM yyyy", Inv)} - Sales LKR {Amt(s.TotalSales)}"; + return new OwnerReportContent(date, BuildSms(date, s), subject, BuildHtml(date, day, branchRows), BuildText(date, day)); + } + + // ---- SMS ------------------------------------------------------------------ + + private static string BuildSms(DateOnly date, DayEndReportDayDto s) + { + var sb = new StringBuilder(); + sb.Append(AppName).Append(" - Day end ").Append(date.ToString("dd/MM/yyyy", Inv)).Append('\n'); + sb.Append("Sales: LKR ").Append(Amt(s.TotalSales)).Append(" (").Append(s.SalesCount).Append(")\n"); + sb.Append("Paid: ").Append(Amt(s.PaidAmount)).Append(" | Due: ").Append(Amt(s.DueAmount)).Append('\n'); + sb.Append("Cash: ").Append(Amt(s.PaidByCash)).Append(" | Card: ").Append(Amt(s.PaidByCard)).Append('\n'); + sb.Append("Expenses: ").Append(Amt(s.ExpenseTotal)).Append('\n'); + sb.Append(ProfitLabel(s.NetProfit)).Append(": ").Append(Amt(Math.Abs(s.NetProfit))).Append('\n'); + sb.Append("Cash in hand: ").Append(Amt(s.ClosingCashInHand)); + return sb.ToString(); + } + + // ---- Email ------------------------------------------------------------------ + + private string BuildHtml(DateOnly date, DayEndReportDto day, List<(string Name, DayEndReportDayDto Summary)> branches) + { + var s = day.Summary; + var h = new StringBuilder(); + h.Append("
"); + h.Append($"

Day-end report

"); + h.Append($"

{E(date.ToString("dddd, dd MMMM yyyy", Inv))} · all branches

"); + + Section(h, "Summary", new (string, string)[] + { + ("Total sales", $"LKR {Amt(s.TotalSales)} ({s.SalesCount} sales)"), + ("Paid", Amt(s.PaidAmount)), + ("Due", Amt(s.DueAmount)), + ("Paid by cash", Amt(s.PaidByCash)), + ("Paid by card", Amt(s.PaidByCard)), + ("Paid by other (transfer / cheque)", Amt(s.PaidByOther)), + ("Expenses", Amt(s.ExpenseTotal)), + ("Income (sales - expenses)", Signed(s.Income)), + ("Cost of goods sold", Amt(s.CostOfGoodsSold)), + ("Gross profit", Signed(s.GrossProfit)), + (ProfitLabel(s.NetProfit), Amt(Math.Abs(s.NetProfit))), + }, emphasiseLast: true, lastIsLoss: s.NetProfit < 0); + + Section(h, "Cash", new (string, string)[] + { + ("Opening cash in hand", Amt(s.OpeningCashInHand)), + ("Net cash today", Signed(s.NetCash)), + ("Deposited to bank", Amt(s.CashDeposited)), + ("Closing cash in hand", Amt(s.ClosingCashInHand)), + ("Collected on earlier invoices", Amt(s.EarlierInvoiceCollections)), + }, emphasiseLast: false, lastIsLoss: false); + + if (branches.Count > 0) + Table(h, "By branch", new[] { "Branch", "Sales", "Paid", "Expenses", "Net profit" }, firstNumericColumn: 1, + branches.Select(x => new[] { x.Name, Amt(x.Summary.TotalSales), Amt(x.Summary.PaidAmount), Amt(x.Summary.ExpenseTotal), Signed(x.Summary.NetProfit) })); + + var top = day.Sales.OrderByDescending(x => x.Total).Take(TopSales).ToList(); + if (top.Count > 0) + Table(h, day.Sales.Count > TopSales ? $"Top {TopSales} sales (of {day.Sales.Count})" : "Sales", + new[] { "No.", "Customer", "Paid by", "Total" }, firstNumericColumn: 3, + top.Select(x => new[] { x.DocNo, x.CustomerName, x.PaidBy, Amt(x.Total) })); + + if (day.Expenses.Count > 0) + Table(h, "Expenses", new[] { "No.", "Category / title", "Amount" }, firstNumericColumn: 2, + day.Expenses.Select(x => new[] { x.DocNo, $"{x.CategoryName} / {x.TitleName}", Amt(x.Amount) })); + + var link = ReportLink(date); + if (link is not null) + h.Append($"

Open the Day-End Report

"); + h.Append($"

Sent automatically by {AppName}.

"); + h.Append("
"); + return h.ToString(); + } + + private string BuildText(DateOnly date, DayEndReportDto day) + { + var text = new StringBuilder(BuildSms(date, day.Summary)); + var link = ReportLink(date); + if (link is not null) text.Append("\n\nOpen the Day-End Report: ").Append(link); + text.Append("\n\nSent automatically by ").Append(AppName).Append('.'); + return text.ToString(); + } + + /// Link back to the report screen, when OwnerReport:AppUrl is configured (e.g. https://erp.example.com). + private string? ReportLink(DateOnly date) + { + var baseUrl = _configuration["OwnerReport:AppUrl"]?.TrimEnd('/'); + return string.IsNullOrWhiteSpace(baseUrl) ? null : $"{baseUrl}/dashboard/sales/day-end-report?date={date:yyyy-MM-dd}"; + } + + private static void Section(StringBuilder h, string title, (string Label, string Value)[] rows, bool emphasiseLast, bool lastIsLoss) + { + h.Append(SectionTitle(title)); + h.Append(""); + for (var i = 0; i < rows.Length; i++) + { + var last = i == rows.Length - 1 && emphasiseLast; + var colour = last ? (lastIsLoss ? "#dc2626" : "#16a34a") : "#1f2937"; + var weight = last ? "bold" : "normal"; + h.Append($""); + h.Append($""); + } + h.Append("
{E(rows[i].Label)}{E(rows[i].Value)}
"); + } + + private static string SectionTitle(string title) + => $"

{E(title)}

"; + + /// A titled table; columns from on are right-aligned amounts. Cells are HTML-encoded here. + private static void Table(StringBuilder h, string title, string[] headers, int firstNumericColumn, IEnumerable rows) + { + string Align(int i) => i >= firstNumericColumn ? "right" : "left"; + h.Append(SectionTitle(title)); + h.Append(""); + for (var i = 0; i < headers.Length; i++) + h.Append($""); + h.Append(""); + foreach (var cells in rows) + { + h.Append(""); + for (var i = 0; i < cells.Length; i++) + h.Append($""); + h.Append(""); + } + h.Append("
{E(headers[i])}
{E(cells[i])}
"); + } + + private static string ProfitLabel(decimal net) => net < 0 ? "Net loss" : "Net profit"; + private static string Amt(decimal v) => v.ToString("#,##0.00", Inv); + private static string Signed(decimal v) => (v < 0 ? "-" : "") + Amt(Math.Abs(v)); + private static string E(string v) => WebUtility.HtmlEncode(v); +} diff --git a/Backend/ERPCore/Services/OwnerReport/OwnerReportJob.cs b/Backend/ERPCore/Services/OwnerReport/OwnerReportJob.cs new file mode 100644 index 0000000..66925a3 --- /dev/null +++ b/Backend/ERPCore/Services/OwnerReport/OwnerReportJob.cs @@ -0,0 +1,49 @@ +namespace ERPCore.Services.OwnerReport; + +/// +/// Sends the day-end owner report once its configured time has passed +/// (docs/DAY_END_OWNER_REPORT_PLAN.md §3.4). Wakes every minute; all the "is it time / already +/// sent / retry" logic lives in . +/// Never lets an exception escape — a failed tick is logged and the next one tries again. +/// +public sealed class OwnerReportJob : BackgroundService +{ + private static readonly TimeSpan Interval = TimeSpan.FromMinutes(1); + + private readonly IServiceScopeFactory _scopes; + private readonly ILogger _logger; + + public OwnerReportJob(IServiceScopeFactory scopes, ILogger logger) + { + _scopes = scopes; + _logger = logger; + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + // Let startup (migrations, seeding) finish before the first tick. + try { await Task.Delay(TimeSpan.FromSeconds(30), stoppingToken); } + catch (OperationCanceledException) { return; } + + using var timer = new PeriodicTimer(Interval); + do + { + try + { + // Fresh scope per tick: its own DbContext, and no HTTP user (so the day-end + // figures are company-wide, not limited to someone's branch). + using var scope = _scopes.CreateScope(); + await scope.ServiceProvider.GetRequiredService().RunScheduledAsync(stoppingToken); + } + catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested) + { + return; + } + catch (Exception ex) + { + _logger.LogError(ex, "Owner report tick failed; will retry on the next tick."); + } + } + while (await timer.WaitForNextTickAsync(stoppingToken).ConfigureAwait(false)); + } +} diff --git a/Backend/ERPCore/Services/OwnerReport/OwnerReportService.cs b/Backend/ERPCore/Services/OwnerReport/OwnerReportService.cs new file mode 100644 index 0000000..c79c8fd --- /dev/null +++ b/Backend/ERPCore/Services/OwnerReport/OwnerReportService.cs @@ -0,0 +1,360 @@ +using System.Text.RegularExpressions; +using ERPCore.Common.Http; +using ERPCore.Domain.Entities; +using ERPCore.Domain.Enums; +using ERPCore.Dtos.Common; +using ERPCore.Dtos.Config; +using ERPCore.Dtos.Warehouses; +using ERPCore.Infra.Auth; +using ERPCore.Infra.Messaging; +using ERPCore.Infra.Persistence; +using ERPCore.System.Errors; +using Microsoft.EntityFrameworkCore; + +namespace ERPCore.Services.OwnerReport; + +public interface IOwnerReportService +{ + Task> GetAsync(CancellationToken ct = default); + Task> UpdateAsync(UpdateOwnerReportConfigRequest request, uint expectedRowVersion, CancellationToken ct = default); + Task PreviewAsync(DateOnly? date, CancellationToken ct = default); + Task> SendTestAsync(DateOnly? date, CancellationToken ct = default); + Task> ListDispatchesAsync(PageQuery query, CancellationToken ct = default); + + /// One scheduler tick (OwnerReportJob): sends today's report once its time has come. + Task RunScheduledAsync(CancellationToken ct = default); +} + +/// +/// Day-end owner report: settings, preview, test sends, the send log and the scheduled daily +/// send (docs/DAY_END_OWNER_REPORT_PLAN.md). Messages go out through +/// (AuthHex), so this service never handles SMTP or SMS credentials. +/// +public sealed class OwnerReportService : IOwnerReportService +{ + private const int MaxRecipients = 5; + private const int MaxAttempts = 3; + private static readonly TimeSpan RetryDelay = TimeSpan.FromMinutes(5); + private static readonly Regex SriLankanPhone = new(PhoneRules.Pattern, RegexOptions.Compiled); + + private readonly ErpDbContext _db; + private readonly OwnerReportContentBuilder _content; + private readonly IMessageSender _sender; + private readonly BusinessDayClock _clock; + private readonly ICurrentUser _currentUser; + private readonly ILogger _logger; + + public OwnerReportService( + ErpDbContext db, OwnerReportContentBuilder content, IMessageSender sender, BusinessDayClock clock, + ICurrentUser currentUser, ILogger logger) + { + _db = db; + _content = content; + _sender = sender; + _clock = clock; + _currentUser = currentUser; + _logger = logger; + } + + // ---- Settings ------------------------------------------------------------------ + + public async Task> GetAsync(CancellationToken ct = default) + { + var config = await LoadConfigAsync(asNoTracking: true, ct); + var recipients = await _db.OwnerReportRecipients.AsNoTracking().OrderBy(r => r.RecipientId).ToListAsync(ct); + return new ETagged(Map(config, recipients), config.RowVersion); + } + + public async Task> UpdateAsync(UpdateOwnerReportConfigRequest request, uint expectedRowVersion, CancellationToken ct = default) + { + // Validate everything before touching the rows. + if (request.Recipients.Count > MaxRecipients) + throw Invalid($"At most {MaxRecipients} recipients."); + var cleaned = request.Recipients.Select(CleanRecipient).ToList(); + + var config = await LoadConfigAsync(asNoTracking: false, ct); + if (config.RowVersion != expectedRowVersion) + throw new DomainException(ErrorCodes.ConcurrencyConflict, "The owner report settings were changed by someone else — reload and try again.", 412); + + config.Enabled = request.Enabled!.Value; + config.SmsEnabled = request.SmsEnabled!.Value; + config.EmailEnabled = request.EmailEnabled!.Value; + config.IncludeBranchBreakdown = request.IncludeBranchBreakdown!.Value; + config.SendAtLocalTime = TimeOnly.ParseExact(request.SendAtLocalTime, "HH:mm"); + config.UpdatedAt = DateTime.UtcNow; + config.UpdatedBy = _currentUser.AuditUserId; + + if (config.Enabled && !cleaned.Any(r => r.IsActive)) + throw Invalid("Add at least one active recipient before switching the report on."); + if (config.Enabled && !config.SmsEnabled && !config.EmailEnabled) + throw Invalid("Switch on SMS, email or both before switching the report on."); + + // Replace the recipient list: update kept rows, add new ones, remove the rest. + var existing = await _db.OwnerReportRecipients.ToListAsync(ct); + var keptIds = cleaned.Where(r => r.RecipientId is not null).Select(r => r.RecipientId!.Value).ToHashSet(); + _db.OwnerReportRecipients.RemoveRange(existing.Where(e => !keptIds.Contains(e.RecipientId))); + foreach (var input in cleaned) + { + var row = input.RecipientId is int id ? existing.FirstOrDefault(e => e.RecipientId == id) : null; + if (input.RecipientId is not null && row is null) + throw Invalid($"Recipient {input.RecipientId} no longer exists — reload and try again."); + row ??= _db.OwnerReportRecipients.Add(new OwnerReportRecipient()).Entity; + row.Name = input.Name; + row.Mobile = input.Mobile; + row.Email = input.Email; + row.IsActive = input.IsActive; + } + + try + { + await _db.SaveChangesAsync(ct); + } + catch (DbUpdateConcurrencyException) + { + throw new DomainException(ErrorCodes.ConcurrencyConflict, "The owner report settings were changed by someone else — reload and try again.", 412); + } + + return await GetAsync(ct); + } + + // ---- Preview / test --------------------------------------------------------------- + + public async Task PreviewAsync(DateOnly? date, CancellationToken ct = default) + { + var config = await LoadConfigAsync(asNoTracking: true, ct); + var content = await _content.BuildAsync(date ?? _clock.Today, config.IncludeBranchBreakdown, ct); + return new OwnerReportPreviewDto(content.Date, content.SmsText, content.EmailSubject, content.EmailHtml); + } + + public async Task> SendTestAsync(DateOnly? date, CancellationToken ct = default) + { + var config = await LoadConfigAsync(asNoTracking: true, ct); + if (!config.SmsEnabled && !config.EmailEnabled) + throw Invalid("Switch on SMS, email or both to send a test."); + + var recipients = await _db.OwnerReportRecipients.AsNoTracking().Where(r => r.IsActive).ToListAsync(ct); + if (recipients.Count == 0) + throw Invalid("Add at least one active recipient and save before sending a test."); + + var day = date ?? _clock.Today; + var content = await _content.BuildAsync(day, config.IncludeBranchBreakdown, ct); + + // Tests are logged like real sends but flagged, with a unique key so they never collide + // with (or stand in for) the day's automatic send. + var batch = Guid.NewGuid().ToString("N"); + var rows = Targets(config, recipients) + .Select(t => NewDispatch(day, t.Channel, t.Recipient, t.Destination, isTest: true, dedupeKey: batch)) + .ToList(); + _db.OwnerReportDispatches.AddRange(rows); + await _db.SaveChangesAsync(ct); + + await SendAsync(rows, content, ct); + return rows.Select(r => new OwnerReportSendResultDto( + r.Channel.ToString(), r.RecipientName, r.Destination, r.Status == OwnerReportDispatchStatus.Sent, r.Error)).ToList(); + } + + public async Task> ListDispatchesAsync(PageQuery query, CancellationToken ct = default) + { + var q = _db.OwnerReportDispatches.AsNoTracking(); + var total = await q.CountAsync(ct); + var rows = await q.OrderByDescending(d => d.DispatchId).Skip(query.Skip).Take(query.PageSize) + .Select(d => new OwnerReportDispatchDto( + d.DispatchId, d.BusinessDate, d.Channel.ToString(), d.RecipientName, d.Destination, + d.Status.ToString(), d.Attempts, d.Error, d.IsTest, d.CreatedAt, d.SentAt)) + .ToListAsync(ct); + return PagedResponse.Create(rows, query.Page, query.PageSize, total); + } + + // ---- Scheduled send ---------------------------------------------------------------- + + public async Task RunScheduledAsync(CancellationToken ct = default) + { + var config = await _db.OwnerReportConfig.AsNoTracking().FirstOrDefaultAsync(c => c.ConfigId == OwnerReportConfig.SingletonId, ct); + if (config is null || !config.Enabled || (!config.SmsEnabled && !config.EmailEnabled)) return; + + var nowLocal = TimeZoneInfo.ConvertTimeFromUtc(DateTime.UtcNow, _clock.Zone); + if (TimeOnly.FromDateTime(nowLocal) < config.SendAtLocalTime) return; + // Only today: a day missed while the server was down is sent once it is back, the same + // day — older days are never sent automatically (plan R8). + var today = DateOnly.FromDateTime(nowLocal); + + await EnsureTodaysDispatchesAsync(config, today, ct); + + var now = DateTime.UtcNow; + var due = await _db.OwnerReportDispatches + .Where(d => d.BusinessDate == today && !d.IsTest && d.Attempts < MaxAttempts + && (d.Status == OwnerReportDispatchStatus.Pending || d.Status == OwnerReportDispatchStatus.Failed) + && (d.NextAttemptAt == null || d.NextAttemptAt <= now)) + .ToListAsync(ct); + if (due.Count == 0) return; + + // Claim each row before sending (attempt counter as an optimistic lock), so a second + // server instance running the same tick skips it instead of sending it again. + var claimed = new List(); + foreach (var d in due) + { + var updated = await _db.OwnerReportDispatches + .Where(x => x.DispatchId == d.DispatchId && x.Attempts == d.Attempts) + .ExecuteUpdateAsync(s => s + .SetProperty(x => x.Attempts, x => x.Attempts + 1) + .SetProperty(x => x.NextAttemptAt, now.Add(RetryDelay)), ct); + if (updated == 1) + { + d.Attempts += 1; + d.NextAttemptAt = now.Add(RetryDelay); + claimed.Add(d); + } + } + if (claimed.Count == 0) return; + + var content = await _content.BuildAsync(today, config.IncludeBranchBreakdown, ct); + await SendAsync(claimed, content, ct); + _logger.LogInformation("Owner report for {Date}: {Sent} sent, {Failed} failed.", + today, claimed.Count(d => d.Status == OwnerReportDispatchStatus.Sent), claimed.Count(d => d.Status == OwnerReportDispatchStatus.Failed)); + } + + /// Creates today's Pending row for each active recipient × enabled channel that doesn't have one yet. + private async Task EnsureTodaysDispatchesAsync(OwnerReportConfig config, DateOnly today, CancellationToken ct) + { + var recipients = await _db.OwnerReportRecipients.AsNoTracking().Where(r => r.IsActive).ToListAsync(ct); + var existing = (await _db.OwnerReportDispatches.AsNoTracking() + .Where(d => d.BusinessDate == today && !d.IsTest) + .Select(d => new { d.Channel, d.RecipientId }) + .ToListAsync(ct)) + .Select(d => (d.Channel, d.RecipientId)) + .ToHashSet(); + + var missing = Targets(config, recipients) + .Where(t => !existing.Contains((t.Channel, (int?)t.Recipient.RecipientId))) + .Select(t => NewDispatch(today, t.Channel, t.Recipient, t.Destination, isTest: false, dedupeKey: string.Empty)) + .ToList(); + if (missing.Count == 0) return; + + _db.OwnerReportDispatches.AddRange(missing); + try + { + await _db.SaveChangesAsync(ct); + } + catch (DbUpdateException) + { + // Another instance created them first (unique index) — theirs stand. + _db.ChangeTracker.Clear(); + } + } + + // ---- Sending ----------------------------------------------------------------------- + + /// Sends (grouped per channel) and records each outcome. + private async Task SendAsync(List rows, OwnerReportContent content, CancellationToken ct) + { + foreach (var group in rows.GroupBy(r => r.Channel)) + { + var destinations = group.Select(r => r.Destination).Distinct(StringComparer.OrdinalIgnoreCase).ToList(); + IReadOnlyList results; + try + { + results = group.Key == OwnerReportChannel.Email + ? await _sender.SendEmailAsync(destinations, content.EmailSubject, content.EmailHtml, content.EmailText, ct) + : await _sender.SendSmsAsync(destinations, content.SmsText, ct); + } + catch (Exception ex) when (ex is not OperationCanceledException || !ct.IsCancellationRequested) + { + // The whole call failed (e.g. AuthHex unreachable) — every row of this channel failed. + _logger.LogWarning(ex, "Owner report {Channel} send failed.", group.Key); + results = destinations.Select(d => new SendResult(d, false, ex.Message)).ToList(); + } + + foreach (var row in group) + { + var result = results.FirstOrDefault(r => string.Equals(r.To, row.Destination, StringComparison.OrdinalIgnoreCase)); + if (result is { Ok: true }) + { + row.Status = OwnerReportDispatchStatus.Sent; + row.SentAt = DateTime.UtcNow; + row.Error = null; + row.NextAttemptAt = null; + } + else + { + row.Status = OwnerReportDispatchStatus.Failed; + row.Error = Truncate(result?.Error ?? "No result returned for this recipient.", 1000); + } + } + } + + // Rows may be tracked (test) or detached copies (scheduled) — attach updates either way. + foreach (var row in rows) + { + var entry = _db.Entry(row); + if (entry.State == EntityState.Detached) _db.OwnerReportDispatches.Attach(row); + entry = _db.Entry(row); + entry.Property(r => r.Status).IsModified = true; + entry.Property(r => r.SentAt).IsModified = true; + entry.Property(r => r.Error).IsModified = true; + entry.Property(r => r.NextAttemptAt).IsModified = true; + } + await _db.SaveChangesAsync(ct); + } + + private static IEnumerable<(OwnerReportChannel Channel, OwnerReportRecipient Recipient, string Destination)> Targets( + OwnerReportConfig config, IEnumerable recipients) + { + foreach (var r in recipients) + { + if (config.SmsEnabled && !string.IsNullOrWhiteSpace(r.Mobile)) yield return (OwnerReportChannel.Sms, r, r.Mobile!); + if (config.EmailEnabled && !string.IsNullOrWhiteSpace(r.Email)) yield return (OwnerReportChannel.Email, r, r.Email!); + } + } + + private static OwnerReportDispatch NewDispatch( + DateOnly day, OwnerReportChannel channel, OwnerReportRecipient recipient, string destination, bool isTest, string dedupeKey) + => new() + { + BusinessDate = day, + Channel = channel, + RecipientId = recipient.RecipientId, + RecipientName = recipient.Name, + Destination = destination, + Status = OwnerReportDispatchStatus.Pending, + Attempts = isTest ? 1 : 0, + IsTest = isTest, + DedupeKey = dedupeKey, + CreatedAt = DateTime.UtcNow, + }; + + // ---- Helpers ------------------------------------------------------------------------ + + private async Task LoadConfigAsync(bool asNoTracking, CancellationToken ct) + { + var q = asNoTracking ? _db.OwnerReportConfig.AsNoTracking() : _db.OwnerReportConfig; + return await q.FirstOrDefaultAsync(c => c.ConfigId == OwnerReportConfig.SingletonId, ct) + ?? throw new NotFoundException("Owner report settings have not been seeded."); + } + + private OwnerReportConfigDto Map(OwnerReportConfig c, IEnumerable recipients) => new( + c.Enabled, c.SmsEnabled, c.EmailEnabled, c.SendAtLocalTime.ToString("HH:mm"), c.IncludeBranchBreakdown, + recipients.Select(r => new OwnerReportRecipientDto(r.RecipientId, r.Name, r.Mobile, r.Email, r.IsActive)).ToList(), + _clock.Zone.Id, c.UpdatedAt, c.UpdatedBy); + + /// Trims, validates and normalises one recipient (mobile → +94XXXXXXXXX, whatever local/94/+94 form was typed). + private static OwnerReportRecipientInput CleanRecipient(OwnerReportRecipientInput input) + { + var name = input.Name.Trim(); + var mobile = string.IsNullOrWhiteSpace(input.Mobile) ? null : input.Mobile.Trim(); + var email = string.IsNullOrWhiteSpace(input.Email) ? null : input.Email.Trim(); + + if (name.Length == 0) throw Invalid("Every recipient needs a name."); + if (mobile is null && email is null) throw Invalid($"{name}: add a mobile number, an email or both."); + if (mobile is not null) + { + if (!SriLankanPhone.IsMatch(mobile)) throw Invalid($"{name}: {PhoneRules.Message}"); + var digits = Regex.Replace(mobile, @"\D", ""); + mobile = "+94" + (digits.StartsWith("94") ? digits[2..] : digits[1..]); + } + + return new OwnerReportRecipientInput { RecipientId = input.RecipientId, Name = name, Mobile = mobile, Email = email?.ToLowerInvariant(), IsActive = input.IsActive }; + } + + private static DomainException Invalid(string message) => new(ErrorCodes.Validation, message, 422); + private static string Truncate(string value, int max) => value.Length <= max ? value : value[..max]; +} diff --git a/Backend/PROGRESS.md b/Backend/PROGRESS.md index ea0a3a2..de9b2a1 100644 --- a/Backend/PROGRESS.md +++ b/Backend/PROGRESS.md @@ -661,3 +661,12 @@ Spec: `docs/30-BACKEND-PHASE2.md` (model + rules **and** API — one doc, unlike - **Fix in code:** `GlAccountRole.AccruedExpenses` + `gl_account_config.AccruedExpensesCode` (migration `AddAccruedExpensesGlRole`, **not applied**, fills existing config with `2300`; fresh databases seed it from the old appsettings key). `ExpenseService` / `ExpensePaymentService` read the mapped role, so it is edited, validated (must exist and be postable) and checked at startup like every other GL role, on Settings -> GL Accounts. - **GL data (done through the GL's own API, not SQL):** created `5200 Operating Expenses` (header) with `5200-01` Utility, `5200-02` Rental, `5200-03` Salaries and Wages, `5200-04` Other Expenses, and `2300 Accrued Expenses`. - **Still true for a brand-new GL:** the chart of accounts itself is created in the GL service, not in the ERP; the ERP only maps roles to existing accounts and now tells you which are missing. + +### 2026-10-19 — Day-end owner report (SMS + email to the owner) +- **What:** every evening (default 21:00, `DayEndReport:TimeZone`) the day's figures from `DayEndReportService.GetDayAsync` go to the owner — short SMS + full HTML email (summary, cash, per-branch, top 10 sales, expenses). Plan: `docs/DAY_END_OWNER_REPORT_PLAN.md`. +- **Data:** `owner_report_config` (singleton, seeded off), `owner_report_recipients` (≤ 5; mobile normalised to `+94XXXXXXXXX`, Sri Lankan `PhoneRules`), `owner_report_dispatches` (send log + "send once" guard: unique `(BusinessDate, Channel, RecipientId, IsTest, DedupeKey)`). Migration `AddOwnerReport` (additive; generated in MariaDB mode). +- **Sending:** `IMessageSender` → `AuthHexMessageSender` → AuthHex `POST /api/notify/email|sms` (new, admin-key gated) — **SMTP/SMS credentials stay in AuthHex**. `Messaging:Provider = Log` swaps in `LogOnlyMessageSender`. Optional `OwnerReport:AppUrl` adds a link back to the Day-End Report. +- **Schedule:** `OwnerReportJob` (`BackgroundService`, 1-min tick) → `OwnerReportService.RunScheduledAsync`: creates today's rows once the time passes, claims each row by bumping `Attempts` (multi-instance safe), retries failures up to 3× every 5 min; only today is ever sent automatically. +- **API:** `GET/PUT /api/v1/settings/owner-report` (ETag), `GET .../preview?date`, `POST .../test` (1/min), `GET .../dispatches` — `settings` nav policy, like the other config screens. +- **AuthHex (`ERP_Auth_Service`):** `NotifyController`, `ThirdPartyService.SendCustomEmailAsync` / `SendTextSmsAsync` (OTP methods untouched), `SmtpSettings:SenderName` → "Hexa ERP". +- **Verified locally (MySQL 8, `Messaging:Provider=Log`):** migration + seed applied; with a test recipient and send time 00:00 the job sent 1 SMS + 1 email, and later ticks sent nothing more. Real AuthHex delivery **not yet tested** (AuthHex must be redeployed with `/api/notify`). diff --git a/Frontend/PROGRESS.md b/Frontend/PROGRESS.md index 0ac6d5a..2af6786 100644 --- a/Frontend/PROGRESS.md +++ b/Frontend/PROGRESS.md @@ -407,3 +407,8 @@ Spec: `docs/21-FRONTEND-PHASE2.md` (flows/screens) · contract: `docs/30-BACKEND 5. **Inline "add variant category":** a "+" icon button next to the checkboxes opens an inline name field that calls `variantCategoriesApi.create()` directly from `/new`, appends the result to the in-memory list, and auto-checks it — so a brand-new dimension (e.g. "Material") can be added without leaving the Item form, and it also then appears on `/dashboard/products/variants`. 6. **Color gets a real color picker:** for whichever checked category is literally named "Color" (case-insensitive), the free-text input is replaced with a native `` swatch picker *plus* a required "Color name" text field — picking red alone isn't enough, a name is mandatory too. The pair is encoded as a single string `"|"` in `valuesByCategory` (helpers `encodeColorValue`/`decodeColorValue`/`partLabel` in `new/page.tsx`) so the existing generic value-list plumbing didn't need a parallel data shape; every place that displays or SKU-generates from a color value decodes it back to just the name (the hex only ever drives the swatch dot next to chips and table cells) — so SKUs read `HAR-CRI` (from "Crimson"), never `HAR-EF4`. - Verified: `tsc --noEmit` clean after every step (same pre-existing `login/page.tsx` error only, confirmed unchanged throughout). Each change was driven end-to-end through a headless Playwright session against the dev server and screenshotted — field removal, subcategory always-visible + disabled state, checkbox show/hide of category builders, cartesian flat table with 2+ active categories, inline category creation followed by its builder appearing immediately, and the color picker + name → chip swatch → table swatch → final SKU/item name chain — with `console --errors` clean at every step and at least one full create-and-redirect-to-Items-list confirmed per major change. + +### 2026-10-19 — Settings → Configuration → Day-End Owner Report +- New page `app/dashboard/settings/owner-report` (`lib/api/owner-report.ts`, `types/owner-report.ts`), card on Settings → Configuration, header + breadcrumb titles. +- Sections: Delivery (master switch, SMS, Email, branch breakdown, send time), Recipients (≤ 5; name / mobile with the Sri Lankan phone rule + input limiter / email; active switch), Preview (SMS bubble + sandboxed email iframe for any day), Send test now (confirm dialog; blocked while there are unsaved changes), Send log (cards on phones, table on desktop, pagination). Draft + sticky Save/Discard bar; every validation error also toasts. +- `tsc` + `eslint` clean for the new files; not yet clicked through in a browser. diff --git a/Frontend/erp-system/app/dashboard/settings/configuration/page.tsx b/Frontend/erp-system/app/dashboard/settings/configuration/page.tsx index 6b99e0f..c455dc7 100644 --- a/Frontend/erp-system/app/dashboard/settings/configuration/page.tsx +++ b/Frontend/erp-system/app/dashboard/settings/configuration/page.tsx @@ -1,5 +1,5 @@ import Link from "next/link" -import { BookOpen, Building2, ClipboardList, LayoutTemplate, MapPin, PackageCheck, ShieldCheck, ShoppingCart, SlidersHorizontal } from "lucide-react" +import { BookOpen, Building2, ClipboardList, LayoutTemplate, MapPin, PackageCheck, Send, ShieldCheck, ShoppingCart, SlidersHorizontal } from "lucide-react" const cards = [ { @@ -56,6 +56,12 @@ const cards = [ icon: LayoutTemplate, description: "Logo, footer, terms, and template style for printed invoices", }, + { + title: "Day-End Owner Report", + href: "/dashboard/settings/owner-report", + icon: Send, + description: "Send the day's revenue to the owner by SMS and email", + }, ] export default function ConfigurationPage() { diff --git a/Frontend/erp-system/app/dashboard/settings/owner-report/page.tsx b/Frontend/erp-system/app/dashboard/settings/owner-report/page.tsx new file mode 100644 index 0000000..11d9b35 --- /dev/null +++ b/Frontend/erp-system/app/dashboard/settings/owner-report/page.tsx @@ -0,0 +1,524 @@ +"use client" + +import { useEffect, useState } from "react" +import { Clock, Eye, Mail, MessageSquare, Plus, Send, Trash2, Users } from "lucide-react" + +import { ownerReportApi } from "@/lib/api/owner-report" +import { errorMessage } from "@/lib/error-map" +import { cn } from "@/lib/utils" +import { limitPhoneInput, phoneError } from "@/lib/validations/phone" +import { PaginationMeta } from "@/types/common" +import { OwnerReportConfig, OwnerReportDispatch, OwnerReportPreview, OwnerReportRecipientInput } from "@/types/owner-report" + +import { Badge } from "@/components/ui/badge" +import { Button } from "@/components/ui/button" +import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog" +import { FieldError } from "@/components/ui/field" +import { Input } from "@/components/ui/input" +import { Label } from "@/components/ui/label" +import { PaginationFooter } from "@/components/ui/pagination-footer" +import { Skeleton } from "@/components/ui/skeleton" +import { Switch } from "@/components/ui/switch" +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table" +import { toast } from "@/components/ui/toast" + +const MAX_RECIPIENTS = 5 +const EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/ +const PANEL = "flex flex-col gap-4 rounded-2xl border bg-card p-4 shadow-sm sm:p-5" + +/** Editable copy of the settings — saved together with one PUT (the recipients are a list). */ +type Draft = { + enabled: boolean + smsEnabled: boolean + emailEnabled: boolean + sendAtLocalTime: string + includeBranchBreakdown: boolean + recipients: (OwnerReportRecipientInput & { key: number })[] +} + +function toDraft(c: OwnerReportConfig): Draft { + return { + enabled: c.enabled, + smsEnabled: c.smsEnabled, + emailEnabled: c.emailEnabled, + sendAtLocalTime: c.sendAtLocalTime, + includeBranchBreakdown: c.includeBranchBreakdown, + recipients: c.recipients.map((r) => ({ ...r, key: r.recipientId })), + } +} + +function localToday(): string { + const d = new Date() + return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}` +} + +/** Per-recipient field errors, keyed `${key}-name|mobile|email`. */ +function validate(draft: Draft): Record { + const errors: Record = {} + for (const r of draft.recipients) { + if (!r.name.trim()) errors[`${r.key}-name`] = "Enter a name" + const mobile = r.mobile?.trim() ?? "" + const email = r.email?.trim() ?? "" + if (!mobile && !email) errors[`${r.key}-mobile`] = "Add a mobile number, an email or both" + const pe = mobile ? phoneError(mobile) : null + if (pe) errors[`${r.key}-mobile`] = pe + if (email && !EMAIL_PATTERN.test(email)) errors[`${r.key}-email`] = "Enter a valid email address" + } + if (draft.enabled && !draft.recipients.some((r) => r.isActive)) errors.recipients = "Add at least one active recipient before switching the report on" + if (draft.enabled && !draft.smsEnabled && !draft.emailEnabled) errors.channels = "Switch on SMS, email or both" + return errors +} + +/** + * Day-End Owner Report — sends the day's revenue and details to the owner by SMS and email at a + * set time (docs/DAY_END_OWNER_REPORT_PLAN.md). Messages go out through the auth service's own + * SMTP / SMS settings, so nothing here holds mail credentials. + */ +export default function OwnerReportSettingsPage() { + const [config, setConfig] = useState(null) + const [etag, setEtag] = useState(null) + const [draft, setDraft] = useState(null) + const [loadError, setLoadError] = useState(null) + const [errors, setErrors] = useState>({}) + const [saving, setSaving] = useState(false) + const [nextKey, setNextKey] = useState(-1) + + const [previewDate, setPreviewDate] = useState(localToday()) + const [preview, setPreview] = useState(null) + const [previewLoading, setPreviewLoading] = useState(false) + + const [testOpen, setTestOpen] = useState(false) + const [testing, setTesting] = useState(false) + + const [log, setLog] = useState(null) + const [logPagination, setLogPagination] = useState(null) + const [logPage, setLogPage] = useState(1) + const [logPageSize, setLogPageSize] = useState(10) + const [logReload, setLogReload] = useState(0) + + function load() { + ownerReportApi + .get() + .then((res) => { + setConfig(res.data) + setEtag(res.etag) + setDraft(toDraft(res.data)) + setErrors({}) + }) + .catch((err) => { + setLoadError(errorMessage(err)) + toast.error("Could not load the owner report settings", errorMessage(err)) + }) + } + + useEffect(load, []) + + useEffect(() => { + ownerReportApi + .dispatches({ page: logPage, pageSize: logPageSize }) + .then((res) => { + setLog(res.items) + setLogPagination(res.pagination) + }) + .catch((err) => { + setLog([]) + toast.error("Could not load the send log", errorMessage(err)) + }) + }, [logPage, logPageSize, logReload]) + + const dirty = config !== null && draft !== null && JSON.stringify(toDraft(config)) !== JSON.stringify(draft) + + function patch(next: Partial) { + setDraft((d) => (d ? { ...d, ...next } : d)) + } + + function patchRecipient(key: number, next: Partial) { + setDraft((d) => (d ? { ...d, recipients: d.recipients.map((r) => (r.key === key ? { ...r, ...next } : r)) } : d)) + setErrors((e) => { + const copy = { ...e } + for (const field of Object.keys(next)) delete copy[`${key}-${field}`] + delete copy.recipients + return copy + }) + } + + function addRecipient() { + if (!draft || draft.recipients.length >= MAX_RECIPIENTS) return + patch({ recipients: [...draft.recipients, { key: nextKey, recipientId: null, name: draft.recipients.length === 0 ? "Owner" : "", mobile: "", email: "", isActive: true }] }) + setNextKey((k) => k - 1) + } + + async function save() { + if (!draft) return + const nextErrors = validate(draft) + setErrors(nextErrors) + const messages = [...new Set(Object.values(nextErrors))] + if (messages.length > 0) { + toast.error("Please fix the highlighted fields", messages.length === 1 ? messages[0] : `${messages[0]} (and ${messages.length - 1} more)`) + return + } + setSaving(true) + try { + const res = await ownerReportApi.update( + { + enabled: draft.enabled, + smsEnabled: draft.smsEnabled, + emailEnabled: draft.emailEnabled, + sendAtLocalTime: draft.sendAtLocalTime, + includeBranchBreakdown: draft.includeBranchBreakdown, + recipients: draft.recipients.map((r) => ({ + recipientId: r.recipientId, + name: r.name.trim(), + mobile: r.mobile?.trim() || null, + email: r.email?.trim() || null, + isActive: r.isActive, + })), + }, + etag ?? "", + ) + setConfig(res.data) + setEtag(res.etag) + setDraft(toDraft(res.data)) + toast.success("Owner report saved", res.data.enabled ? `It will be sent daily at ${res.data.sendAtLocalTime}.` : "The daily report is switched off.") + } catch (err) { + toast.error("Could not save the owner report settings", errorMessage(err)) + } finally { + setSaving(false) + } + } + + async function loadPreview() { + setPreviewLoading(true) + try { + setPreview(await ownerReportApi.preview(previewDate)) + } catch (err) { + toast.error("Could not build the preview", errorMessage(err)) + } finally { + setPreviewLoading(false) + } + } + + async function sendTest() { + setTesting(true) + try { + const results = await ownerReportApi.sendTest(previewDate) + const failed = results.filter((r) => !r.ok) + if (failed.length === 0) toast.success("Test sent", `${results.length} message${results.length === 1 ? "" : "s"} sent.`) + else toast.error(`${failed.length} of ${results.length} messages failed`, failed.map((f) => `${f.destination}: ${f.error ?? "failed"}`).join(" · ")) + setTestOpen(false) + setLogPage(1) + setLogReload((n) => n + 1) + } catch (err) { + toast.error("Could not send the test", errorMessage(err)) + } finally { + setTesting(false) + } + } + + return ( +
+
+

Day-End Owner Report

+

+ Send the day's revenue and details to the owner by SMS and email, automatically every evening. +

+
+ + {loadError &&
{loadError}
} + {!loadError && !draft && } + + {draft && config && ( + <> + {/* Delivery */} +
+
+ +

Delivery

+
+ + patch({ enabled: v })} + label="Send the daily report" + help="Off: nothing is sent automatically (you can still send a test)." + /> +
+ patch({ smsEnabled: v })} label="SMS" help="A short revenue summary." icon={MessageSquare} /> + patch({ emailEnabled: v })} label="Email" help="The full details: summary, cash, top sales and expenses." icon={Mail} /> +
+ {errors.channels && } + patch({ includeBranchBreakdown: v })} + label="Include a per-branch breakdown" + help="Adds one row per branch to the email when branches are switched on." + /> +
+ + patch({ sendAtLocalTime: e.target.value })} + className="h-11 text-base" + /> +

Time zone: {config.timeZone}. If the server is down then, it sends as soon as it is back the same day.

+
+
+ + {/* Recipients */} +
+
+
+ +

Recipients

+ ({draft.recipients.length}/{MAX_RECIPIENTS}) +
+ +
+ {errors.recipients && } + + {draft.recipients.length === 0 ? ( +

+ No recipients yet — add the owner's mobile number and email. +

+ ) : ( +
    + {draft.recipients.map((r) => ( +
  • +
    + + patchRecipient(r.key, { name: e.target.value })} placeholder="Owner" aria-invalid={!!errors[`${r.key}-name`]} className="h-11 text-base sm:h-10" /> + +
    +
    + + patchRecipient(r.key, { mobile: limitPhoneInput(e.target.value) })} + placeholder="0771234567" + aria-invalid={!!errors[`${r.key}-mobile`]} + className="h-11 text-base sm:h-10" + /> + +
    +
    + + patchRecipient(r.key, { email: e.target.value })} + placeholder="owner@company.com" + aria-invalid={!!errors[`${r.key}-email`]} + className="h-11 text-base sm:h-10" + /> + +
    +
    + + +
    +
  • + ))} +
+ )} +
+ + {/* Preview & test */} +
+
+ +

Preview & test

+
+
+
+ + setPreviewDate(e.target.value)} className="h-11 text-base sm:h-10 sm:w-44" /> +
+ + +
+ + {preview && ( +
+
+

SMS

+
{preview.smsText}
+

{preview.smsText.length} characters

+
+
+

Email — {preview.emailSubject}

+ {/* Sandboxed: the HTML is our own template, but it never gets scripts or same-origin access. */} +