From 11df543b671beb2a83c1a1807263ccc92dac1e71 Mon Sep 17 00:00:00 2001 From: Alain Kimbu Date: Mon, 17 Aug 2026 15:09:18 +0100 Subject: [PATCH 1/3] Implement 360 Hub and Multiple User Sessions --- Store.API/Controllers/EmployeesController.cs | 8 + Store.API/Controllers/UsersController.cs | 25 + Store.API/Program.cs | 1 + .../Configurations/UserConfiguration.cs | 11 +- ...47_SupportMultipleUserSessions.Designer.cs | 5713 +++++++++++++++++ ...60817130947_SupportMultipleUserSessions.cs | 123 + .../Migrations/StoreDbContextModelSnapshot.cs | 28 +- .../Services/AuthenticationService.cs | 181 +- Store.DbServices/Services/EmployeeService.cs | 32 + Store.DbServices/Services/UserService.cs | 62 + Store.DbServices/Store.DbServices.csproj | 1 + Store.Models/DTOs/Auth/UserSessionDto.cs | 13 + Store.Models/DTOs/Employees/Employee360Dto.cs | 13 + Store.Models/DTOs/Users/User360Dto.cs | 14 + Store.Models/Entities/User.cs | 2 +- Store.Models/Entities/UserToken.cs | 6 + .../Services/IAuthenticationService.cs | 1 + .../Interfaces/Services/IEmployeeService.cs | 1 + .../Interfaces/Services/IUserService.cs | 3 + Store.UI/Pages/Employees.cshtml | 418 +- Store.UI/Pages/Employees.cshtml.cs | 60 +- Store.UI/Pages/Users.cshtml | 211 +- Store.UI/Pages/Users.cshtml.cs | 36 + Store.UI/Services/ApiAuthenticationService.cs | 6 + Store.UI/Services/ApiEmployeeService.cs | 5 + Store.UI/Services/ApiUserService.cs | 16 + 26 files changed, 6806 insertions(+), 184 deletions(-) create mode 100644 Store.DbServices/Migrations/20260817130947_SupportMultipleUserSessions.Designer.cs create mode 100644 Store.DbServices/Migrations/20260817130947_SupportMultipleUserSessions.cs create mode 100644 Store.Models/DTOs/Auth/UserSessionDto.cs create mode 100644 Store.Models/DTOs/Employees/Employee360Dto.cs create mode 100644 Store.Models/DTOs/Users/User360Dto.cs diff --git a/Store.API/Controllers/EmployeesController.cs b/Store.API/Controllers/EmployeesController.cs index bd79283..a280151 100644 --- a/Store.API/Controllers/EmployeesController.cs +++ b/Store.API/Controllers/EmployeesController.cs @@ -30,6 +30,14 @@ public async Task GetById(Guid id, CancellationToken ct) return Ok(ApiResponse.Ok(employee)); } + [HttpGet("{id:guid}/360")] + public async Task Get360ById(Guid id, CancellationToken ct) + { + var employee360 = await _employeeService.Get360ByIdAsync(id, ct); + if (employee360 is null) return NotFound(ApiResponse.Fail("Employee not found.")); + return Ok(ApiResponse.Ok(employee360)); + } + [HttpPost] [Authorize(Roles = "Admin,Manager")] public async Task Create([FromBody] CreateEmployeeRequest request, CancellationToken ct) diff --git a/Store.API/Controllers/UsersController.cs b/Store.API/Controllers/UsersController.cs index f67a394..3d1ab2b 100644 --- a/Store.API/Controllers/UsersController.cs +++ b/Store.API/Controllers/UsersController.cs @@ -45,6 +45,18 @@ public async Task GetById(Guid id, CancellationToken ct) return Ok(ApiResponse.Ok(user)); } + [HttpGet("{id:guid}/360")] + public async Task Get360ById(Guid id, CancellationToken ct) + { + var user360 = await _userService.Get360ByIdAsync(id, ct); + if (user360 is null) + { + return NotFound(ApiErrorResponse.From("not_found", "User not found.", traceId: HttpContext.TraceIdentifier)); + } + + return Ok(ApiResponse.Ok(user360)); + } + [HttpPost] [Authorize(Roles = "Admin")] public async Task Create([FromBody] CreateUserRequest request, CancellationToken ct) @@ -225,6 +237,19 @@ public async Task RevokeAllSessions(CancellationToken ct) return Ok(ApiResponse.Ok(null!, "All sessions revoked successfully.")); } + [HttpPost("{id}/sessions/revoke")] + [Authorize(Roles = "Admin,Manager")] + public async Task RevokeUserSessions(Guid id, CancellationToken ct) + { + var success = await _dispatcher.SendAsync(new RevokeAllSessionsCommand(id), ct); + if (!success) + { + return BadRequest(ApiErrorResponse.From("revoke_failed", "Failed to revoke sessions.", traceId: HttpContext.TraceIdentifier)); + } + + return Ok(ApiResponse.Ok(null!, "All sessions revoked successfully.")); + } + // --- Contact Change Endpoints --- [HttpPost("profile/contact-change")] public async Task RequestContactChange([FromBody] CreateContactChangeDto request, CancellationToken ct) diff --git a/Store.API/Program.cs b/Store.API/Program.cs index 1760391..62d4312 100644 --- a/Store.API/Program.cs +++ b/Store.API/Program.cs @@ -26,6 +26,7 @@ builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddMemoryCache(); +builder.Services.AddHttpContextAccessor(); builder.Services.AddFido2(options => { diff --git a/Store.DbServices/Configurations/UserConfiguration.cs b/Store.DbServices/Configurations/UserConfiguration.cs index 45582b2..c17609a 100644 --- a/Store.DbServices/Configurations/UserConfiguration.cs +++ b/Store.DbServices/Configurations/UserConfiguration.cs @@ -30,10 +30,10 @@ public void Configure(EntityTypeBuilder builder) .HasForeignKey(p => p.UserId) .OnDelete(DeleteBehavior.Cascade); - // One-to-one: User → Token - builder.HasOne(u => u.UserToken) + // One-to-many: User → Tokens + builder.HasMany(u => u.UserTokens) .WithOne(t => t.User) - .HasForeignKey(t => t.UserId) + .HasForeignKey(t => t.UserId) .OnDelete(DeleteBehavior.Cascade); // Many-to-one: User → Role @@ -66,11 +66,14 @@ public class UserTokenConfiguration : IEntityTypeConfiguration public void Configure(EntityTypeBuilder builder) { builder.HasKey(t => t.UserTokenId); - builder.HasIndex(t => t.UserId).IsUnique(); + builder.HasIndex(t => t.UserId); builder.Property(t => t.Token).IsRequired().HasMaxLength(2000); builder.Property(t => t.RefreshTokenHash).IsRequired().HasMaxLength(256); builder.Property(t => t.ExpiryDate).IsRequired(); builder.Property(t => t.RefreshTokenExpiryDate).IsRequired(); + builder.Property(t => t.IpAddress).HasMaxLength(45); + builder.Property(t => t.UserAgent).HasMaxLength(500); + builder.Property(t => t.DeviceName).HasMaxLength(100); } } diff --git a/Store.DbServices/Migrations/20260817130947_SupportMultipleUserSessions.Designer.cs b/Store.DbServices/Migrations/20260817130947_SupportMultipleUserSessions.Designer.cs new file mode 100644 index 0000000..06fbd33 --- /dev/null +++ b/Store.DbServices/Migrations/20260817130947_SupportMultipleUserSessions.Designer.cs @@ -0,0 +1,5713 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Store.DbServices.Context; + +#nullable disable + +namespace Store.DbServices.Migrations +{ + [DbContext(typeof(StoreDbContext))] + [Migration("20260817130947_SupportMultipleUserSessions")] + partial class SupportMultipleUserSessions + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.4") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder); + + modelBuilder.Entity("Store.Models.Entities.AuditLog", b => + { + b.Property("AuditLogId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("audit_log_id"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("AuditLogId")); + + b.Property("Action") + .IsRequired() + .HasColumnType("longtext") + .HasColumnName("action"); + + b.Property("DateCreated") + .HasColumnType("datetime(6)") + .HasColumnName("date_created"); + + b.Property("Details") + .HasColumnType("longtext") + .HasColumnName("details"); + + b.Property("IpAddress") + .HasColumnType("longtext") + .HasColumnName("ip_address"); + + b.Property("LastModified") + .HasColumnType("datetime(6)") + .HasColumnName("last_modified"); + + b.Property("UserAgent") + .HasColumnType("longtext") + .HasColumnName("user_agent"); + + b.Property("UserId") + .HasColumnType("char(36)") + .HasColumnName("user_id"); + + b.HasKey("AuditLogId") + .HasName("pk_audit_log"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_audit_log_user_id"); + + b.ToTable("audit_log"); + }); + + modelBuilder.Entity("Store.Models.Entities.Batch", b => + { + b.Property("BatchId") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)") + .HasColumnName("batch_id"); + + b.Property("BatchNumber") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)") + .HasColumnName("batch_number"); + + b.Property("CostPrice") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)") + .HasColumnName("cost_price"); + + b.Property("DateCreated") + .HasColumnType("datetime(6)") + .HasColumnName("date_created"); + + b.Property("ExpiryDate") + .HasColumnType("datetime(6)") + .HasColumnName("expiry_date"); + + b.Property("ItemId") + .HasColumnType("char(36)") + .HasColumnName("item_id"); + + b.Property("LastModified") + .HasColumnType("datetime(6)") + .HasColumnName("last_modified"); + + b.Property("Notes") + .HasMaxLength(500) + .HasColumnType("varchar(500)") + .HasColumnName("notes"); + + b.Property("Quantity") + .HasColumnType("int") + .HasColumnName("quantity"); + + b.Property("ReceivedDate") + .HasColumnType("datetime(6)") + .HasColumnName("received_date"); + + b.HasKey("BatchId") + .HasName("pk_batch"); + + b.HasIndex("ItemId") + .HasDatabaseName("ix_batch_item_id"); + + b.ToTable("batch"); + }); + + modelBuilder.Entity("Store.Models.Entities.Branch", b => + { + b.Property("BranchId") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("branch_id"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("BranchId")); + + b.Property("Address") + .HasColumnType("longtext") + .HasColumnName("address"); + + b.Property("Code") + .IsRequired() + .HasColumnType("longtext") + .HasColumnName("code"); + + b.Property("DateCreated") + .HasColumnType("datetime(6)") + .HasColumnName("date_created"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)") + .HasColumnName("is_active"); + + b.Property("LastModified") + .HasColumnType("datetime(6)") + .HasColumnName("last_modified"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext") + .HasColumnName("name"); + + b.HasKey("BranchId") + .HasName("pk_branch"); + + b.ToTable("branch"); + }); + + modelBuilder.Entity("Store.Models.Entities.BundleRule", b => + { + b.Property("BundleRuleId") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("bundle_rule_id"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("BundleRuleId")); + + b.Property("DateCreated") + .HasColumnType("datetime(6)") + .HasColumnName("date_created"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)") + .HasColumnName("is_active"); + + b.Property("LastModified") + .HasColumnType("datetime(6)") + .HasColumnName("last_modified"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext") + .HasColumnName("name"); + + b.Property("RewardDiscountPercent") + .HasColumnType("decimal(65,30)") + .HasColumnName("reward_discount_percent"); + + b.Property("RewardItemId") + .HasColumnType("char(36)") + .HasColumnName("reward_item_id"); + + b.Property("RewardQuantity") + .HasColumnType("int") + .HasColumnName("reward_quantity"); + + b.Property("TriggerItemId") + .HasColumnType("char(36)") + .HasColumnName("trigger_item_id"); + + b.Property("TriggerQuantity") + .HasColumnType("int") + .HasColumnName("trigger_quantity"); + + b.Property("ValidFrom") + .HasColumnType("datetime(6)") + .HasColumnName("valid_from"); + + b.Property("ValidTo") + .HasColumnType("datetime(6)") + .HasColumnName("valid_to"); + + b.HasKey("BundleRuleId") + .HasName("pk_bundle_rule"); + + b.HasIndex("RewardItemId") + .HasDatabaseName("ix_bundle_rule_reward_item_id"); + + b.HasIndex("TriggerItemId") + .HasDatabaseName("ix_bundle_rule_trigger_item_id"); + + b.ToTable("bundle_rule"); + }); + + modelBuilder.Entity("Store.Models.Entities.CashVarianceRecord", b => + { + b.Property("CashVarianceRecordId") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("cash_variance_record_id"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("CashVarianceRecordId")); + + b.Property("ActualAmount") + .HasColumnType("decimal(65,30)") + .HasColumnName("actual_amount"); + + b.Property("CashierShiftId") + .HasColumnType("char(36)") + .HasColumnName("cashier_shift_id"); + + b.Property("DateCreated") + .HasColumnType("datetime(6)") + .HasColumnName("date_created"); + + b.Property("ExpectedAmount") + .HasColumnType("decimal(65,30)") + .HasColumnName("expected_amount"); + + b.Property("LastModified") + .HasColumnType("datetime(6)") + .HasColumnName("last_modified"); + + b.Property("Notes") + .HasMaxLength(2000) + .HasColumnType("varchar(2000)") + .HasColumnName("notes"); + + b.Property("ReasonCode") + .HasMaxLength(100) + .HasColumnType("varchar(100)") + .HasColumnName("reason_code"); + + b.Property("RecordedByUserId") + .HasColumnType("char(36)") + .HasColumnName("recorded_by_user_id"); + + b.Property("ReviewNotes") + .HasMaxLength(2000) + .HasColumnType("varchar(2000)") + .HasColumnName("review_notes"); + + b.Property("ReviewedAt") + .HasColumnType("datetime(6)") + .HasColumnName("reviewed_at"); + + b.Property("ReviewedByUserId") + .HasColumnType("char(36)") + .HasColumnName("reviewed_by_user_id"); + + b.Property("Status") + .HasColumnType("int") + .HasColumnName("status"); + + b.HasKey("CashVarianceRecordId") + .HasName("pk_cash_variance_record"); + + b.HasIndex("CashierShiftId") + .HasDatabaseName("ix_cash_variance_record_cashier_shift_id"); + + b.HasIndex("RecordedByUserId") + .HasDatabaseName("ix_cash_variance_record_recorded_by_user_id"); + + b.HasIndex("ReviewedByUserId") + .HasDatabaseName("ix_cash_variance_record_reviewed_by_user_id"); + + b.HasIndex("Status", "DateCreated") + .HasDatabaseName("ix_cash_variance_record_status_date_created"); + + b.ToTable("cash_variance_record"); + }); + + modelBuilder.Entity("Store.Models.Entities.CashierShift", b => + { + b.Property("CashierShiftId") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)") + .HasColumnName("cashier_shift_id"); + + b.Property("ClosedAtUtc") + .HasColumnType("datetime(6)") + .HasColumnName("closed_at_utc"); + + b.Property("ClosedByUserId") + .HasColumnType("char(36)") + .HasColumnName("closed_by_user_id"); + + b.Property("ClosingFloat") + .HasColumnType("decimal(65,30)") + .HasColumnName("closing_float"); + + b.Property("DateCreated") + .HasColumnType("datetime(6)") + .HasColumnName("date_created"); + + b.Property("ExpectedClosingAmount") + .HasColumnType("decimal(65,30)") + .HasColumnName("expected_closing_amount"); + + b.Property("LastModified") + .HasColumnType("datetime(6)") + .HasColumnName("last_modified"); + + b.Property("Notes") + .HasColumnType("longtext") + .HasColumnName("notes"); + + b.Property("OpenedAtUtc") + .HasColumnType("datetime(6)") + .HasColumnName("opened_at_utc"); + + b.Property("OpenedByUserId") + .HasColumnType("char(36)") + .HasColumnName("opened_by_user_id"); + + b.Property("OpeningFloat") + .HasColumnType("decimal(65,30)") + .HasColumnName("opening_float"); + + b.Property("Status") + .HasColumnType("int") + .HasColumnName("status"); + + b.Property("VarianceAmount") + .HasColumnType("decimal(65,30)") + .HasColumnName("variance_amount"); + + b.HasKey("CashierShiftId") + .HasName("pk_cashier_shift"); + + b.HasIndex("ClosedByUserId") + .HasDatabaseName("ix_cashier_shift_closed_by_user_id"); + + b.HasIndex("OpenedByUserId") + .HasDatabaseName("ix_cashier_shift_opened_by_user_id"); + + b.ToTable("cashier_shift"); + }); + + modelBuilder.Entity("Store.Models.Entities.Category", b => + { + b.Property("CategoryId") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("category_id"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("CategoryId")); + + b.Property("DateCreated") + .HasColumnType("datetime(6)") + .HasColumnName("date_created"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("varchar(500)") + .HasColumnName("description"); + + b.Property("FullImageUrl") + .HasColumnType("longtext") + .HasColumnName("full_image_url"); + + b.Property("LastModified") + .HasColumnType("datetime(6)") + .HasColumnName("last_modified"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("varchar(150)") + .HasColumnName("name"); + + b.Property("ThumbnailUrl") + .HasMaxLength(500) + .HasColumnType("varchar(500)") + .HasColumnName("thumbnail_url"); + + b.HasKey("CategoryId") + .HasName("pk_category"); + + b.HasIndex("Name") + .IsUnique() + .HasDatabaseName("ix_category_name"); + + b.ToTable("category"); + }); + + modelBuilder.Entity("Store.Models.Entities.ChangeLog", b => + { + b.Property("ChangeLogId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("change_log_id"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("ChangeLogId")); + + b.Property("Action") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("varchar(20)") + .HasColumnName("action"); + + b.Property("DateCreated") + .HasColumnType("datetime(6)") + .HasColumnName("date_created"); + + b.Property("EntityId") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)") + .HasColumnName("entity_id"); + + b.Property("EntityName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)") + .HasColumnName("entity_name"); + + b.Property("IpAddress") + .HasMaxLength(50) + .HasColumnType("varchar(50)") + .HasColumnName("ip_address"); + + b.Property("LastModified") + .HasColumnType("datetime(6)") + .HasColumnName("last_modified"); + + b.Property("NewValues") + .HasColumnType("longtext") + .HasColumnName("new_values"); + + b.Property("OldValues") + .HasColumnType("longtext") + .HasColumnName("old_values"); + + b.Property("UserId") + .HasColumnType("char(36)") + .HasColumnName("user_id"); + + b.HasKey("ChangeLogId") + .HasName("pk_change_log"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_change_log_user_id"); + + b.ToTable("change_log"); + }); + + modelBuilder.Entity("Store.Models.Entities.City", b => + { + b.Property("CityId") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("city_id"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("CityId")); + + b.Property("DateCreated") + .HasColumnType("datetime(6)") + .HasColumnName("date_created"); + + b.Property("LastModified") + .HasColumnType("datetime(6)") + .HasColumnName("last_modified"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)") + .HasColumnName("name"); + + b.Property("RegionId") + .HasColumnType("int") + .HasColumnName("region_id"); + + b.HasKey("CityId") + .HasName("pk_city"); + + b.HasIndex("RegionId") + .HasDatabaseName("ix_city_region_id"); + + b.ToTable("city"); + }); + + modelBuilder.Entity("Store.Models.Entities.Contacts.ContactChangeRequest", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)") + .HasColumnName("id"); + + b.Property("ApprovedAt") + .HasColumnType("datetime(6)") + .HasColumnName("approved_at"); + + b.Property("ApprovedById") + .HasColumnType("char(36)") + .HasColumnName("approved_by_id"); + + b.Property("DateCreated") + .HasColumnType("datetime(6)") + .HasColumnName("date_created"); + + b.Property("LastModified") + .HasColumnType("datetime(6)") + .HasColumnName("last_modified"); + + b.Property("NewEmail") + .HasColumnType("longtext") + .HasColumnName("new_email"); + + b.Property("NewPhone") + .HasColumnType("longtext") + .HasColumnName("new_phone"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)") + .HasColumnName("status"); + + b.Property("UserId") + .HasColumnType("char(36)") + .HasColumnName("user_id"); + + b.Property("VerificationToken") + .HasColumnType("longtext") + .HasColumnName("verification_token"); + + b.Property("VerifiedAt") + .HasColumnType("datetime(6)") + .HasColumnName("verified_at"); + + b.HasKey("Id") + .HasName("pk_contact_change_request"); + + b.HasIndex("ApprovedById") + .HasDatabaseName("ix_contact_change_request_approved_by_id"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_contact_change_request_user_id"); + + b.ToTable("contact_change_request", (string)null); + }); + + modelBuilder.Entity("Store.Models.Entities.Contacts.CustomerEmail", b => + { + b.Property("CustomerEmailId") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("customer_email_id"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("CustomerEmailId")); + + b.Property("CustomerId") + .HasColumnType("char(36)") + .HasColumnName("customer_id"); + + b.Property("DateCreated") + .HasColumnType("datetime(6)") + .HasColumnName("date_created"); + + b.Property("EmailId") + .HasColumnType("int") + .HasColumnName("email_id"); + + b.Property("IsPrimary") + .HasColumnType("tinyint(1)") + .HasColumnName("is_primary"); + + b.Property("LastModified") + .HasColumnType("datetime(6)") + .HasColumnName("last_modified"); + + b.HasKey("CustomerEmailId") + .HasName("pk_customer_email"); + + b.HasIndex("CustomerId") + .HasDatabaseName("ix_customer_email_customer_id"); + + b.HasIndex("EmailId") + .HasDatabaseName("ix_customer_email_email_id"); + + b.ToTable("customer_email"); + }); + + modelBuilder.Entity("Store.Models.Entities.Contacts.CustomerPhone", b => + { + b.Property("CustomerPhoneId") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("customer_phone_id"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("CustomerPhoneId")); + + b.Property("CustomerId") + .HasColumnType("char(36)") + .HasColumnName("customer_id"); + + b.Property("DateCreated") + .HasColumnType("datetime(6)") + .HasColumnName("date_created"); + + b.Property("IsPrimary") + .HasColumnType("tinyint(1)") + .HasColumnName("is_primary"); + + b.Property("LastModified") + .HasColumnType("datetime(6)") + .HasColumnName("last_modified"); + + b.Property("PhoneId") + .HasColumnType("int") + .HasColumnName("phone_id"); + + b.HasKey("CustomerPhoneId") + .HasName("pk_customer_phone"); + + b.HasIndex("CustomerId") + .HasDatabaseName("ix_customer_phone_customer_id"); + + b.HasIndex("PhoneId") + .HasDatabaseName("ix_customer_phone_phone_id"); + + b.ToTable("customer_phone"); + }); + + modelBuilder.Entity("Store.Models.Entities.Contacts.Email", b => + { + b.Property("EmailId") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("email_id"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("EmailId")); + + b.Property("Address") + .IsRequired() + .HasMaxLength(254) + .HasColumnType("varchar(254)") + .HasColumnName("address"); + + b.Property("DateCreated") + .HasColumnType("datetime(6)") + .HasColumnName("date_created"); + + b.Property("IsVerified") + .HasColumnType("tinyint(1)") + .HasColumnName("is_verified"); + + b.Property("LastModified") + .HasColumnType("datetime(6)") + .HasColumnName("last_modified"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("varchar(20)") + .HasColumnName("type"); + + b.HasKey("EmailId") + .HasName("pk_email"); + + b.HasIndex("Address") + .IsUnique() + .HasDatabaseName("ix_email_address"); + + b.ToTable("email"); + }); + + modelBuilder.Entity("Store.Models.Entities.Contacts.EmployeeEmail", b => + { + b.Property("EmployeeEmailId") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("employee_email_id"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("EmployeeEmailId")); + + b.Property("DateCreated") + .HasColumnType("datetime(6)") + .HasColumnName("date_created"); + + b.Property("EmailId") + .HasColumnType("int") + .HasColumnName("email_id"); + + b.Property("EmployeeId") + .HasColumnType("char(36)") + .HasColumnName("employee_id"); + + b.Property("IsPrimary") + .HasColumnType("tinyint(1)") + .HasColumnName("is_primary"); + + b.Property("LastModified") + .HasColumnType("datetime(6)") + .HasColumnName("last_modified"); + + b.HasKey("EmployeeEmailId") + .HasName("pk_employee_email"); + + b.HasIndex("EmailId") + .HasDatabaseName("ix_employee_email_email_id"); + + b.HasIndex("EmployeeId") + .HasDatabaseName("ix_employee_email_employee_id"); + + b.ToTable("employee_email"); + }); + + modelBuilder.Entity("Store.Models.Entities.Contacts.EmployeePhone", b => + { + b.Property("EmployeePhoneId") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("employee_phone_id"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("EmployeePhoneId")); + + b.Property("DateCreated") + .HasColumnType("datetime(6)") + .HasColumnName("date_created"); + + b.Property("EmployeeId") + .HasColumnType("char(36)") + .HasColumnName("employee_id"); + + b.Property("IsPrimary") + .HasColumnType("tinyint(1)") + .HasColumnName("is_primary"); + + b.Property("LastModified") + .HasColumnType("datetime(6)") + .HasColumnName("last_modified"); + + b.Property("PhoneId") + .HasColumnType("int") + .HasColumnName("phone_id"); + + b.HasKey("EmployeePhoneId") + .HasName("pk_employee_phone"); + + b.HasIndex("EmployeeId") + .HasDatabaseName("ix_employee_phone_employee_id"); + + b.HasIndex("PhoneId") + .HasDatabaseName("ix_employee_phone_phone_id"); + + b.ToTable("employee_phone"); + }); + + modelBuilder.Entity("Store.Models.Entities.Contacts.ManufacturerEmail", b => + { + b.Property("ManufacturerEmailId") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("manufacturer_email_id"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("ManufacturerEmailId")); + + b.Property("DateCreated") + .HasColumnType("datetime(6)") + .HasColumnName("date_created"); + + b.Property("EmailId") + .HasColumnType("int") + .HasColumnName("email_id"); + + b.Property("IsPrimary") + .HasColumnType("tinyint(1)") + .HasColumnName("is_primary"); + + b.Property("LastModified") + .HasColumnType("datetime(6)") + .HasColumnName("last_modified"); + + b.Property("ManufacturerId") + .HasColumnType("char(36)") + .HasColumnName("manufacturer_id"); + + b.HasKey("ManufacturerEmailId") + .HasName("pk_manufacturer_email"); + + b.HasIndex("EmailId") + .HasDatabaseName("ix_manufacturer_email_email_id"); + + b.HasIndex("ManufacturerId") + .HasDatabaseName("ix_manufacturer_email_manufacturer_id"); + + b.ToTable("manufacturer_email"); + }); + + modelBuilder.Entity("Store.Models.Entities.Contacts.ManufacturerPhone", b => + { + b.Property("ManufacturerPhoneId") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("manufacturer_phone_id"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("ManufacturerPhoneId")); + + b.Property("DateCreated") + .HasColumnType("datetime(6)") + .HasColumnName("date_created"); + + b.Property("IsPrimary") + .HasColumnType("tinyint(1)") + .HasColumnName("is_primary"); + + b.Property("LastModified") + .HasColumnType("datetime(6)") + .HasColumnName("last_modified"); + + b.Property("ManufacturerId") + .HasColumnType("char(36)") + .HasColumnName("manufacturer_id"); + + b.Property("PhoneId") + .HasColumnType("int") + .HasColumnName("phone_id"); + + b.HasKey("ManufacturerPhoneId") + .HasName("pk_manufacturer_phone"); + + b.HasIndex("ManufacturerId") + .HasDatabaseName("ix_manufacturer_phone_manufacturer_id"); + + b.HasIndex("PhoneId") + .HasDatabaseName("ix_manufacturer_phone_phone_id"); + + b.ToTable("manufacturer_phone"); + }); + + modelBuilder.Entity("Store.Models.Entities.Contacts.Phone", b => + { + b.Property("PhoneId") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("phone_id"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("PhoneId")); + + b.Property("CountryId") + .HasColumnType("int") + .HasColumnName("country_id"); + + b.Property("DateCreated") + .HasColumnType("datetime(6)") + .HasColumnName("date_created"); + + b.Property("IsVerified") + .HasColumnType("tinyint(1)") + .HasColumnName("is_verified"); + + b.Property("LastModified") + .HasColumnType("datetime(6)") + .HasColumnName("last_modified"); + + b.Property("Number") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("varchar(30)") + .HasColumnName("number"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("varchar(20)") + .HasColumnName("type"); + + b.HasKey("PhoneId") + .HasName("pk_phone"); + + b.HasIndex("CountryId", "Number") + .IsUnique() + .HasDatabaseName("ix_phone_country_id_number"); + + b.ToTable("phone"); + }); + + modelBuilder.Entity("Store.Models.Entities.Contacts.SupplierEmail", b => + { + b.Property("SupplierEmailId") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("supplier_email_id"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("SupplierEmailId")); + + b.Property("DateCreated") + .HasColumnType("datetime(6)") + .HasColumnName("date_created"); + + b.Property("EmailId") + .HasColumnType("int") + .HasColumnName("email_id"); + + b.Property("IsPrimary") + .HasColumnType("tinyint(1)") + .HasColumnName("is_primary"); + + b.Property("LastModified") + .HasColumnType("datetime(6)") + .HasColumnName("last_modified"); + + b.Property("SupplierId") + .HasColumnType("char(36)") + .HasColumnName("supplier_id"); + + b.HasKey("SupplierEmailId") + .HasName("pk_supplier_email"); + + b.HasIndex("EmailId") + .HasDatabaseName("ix_supplier_email_email_id"); + + b.HasIndex("SupplierId") + .HasDatabaseName("ix_supplier_email_supplier_id"); + + b.ToTable("supplier_email"); + }); + + modelBuilder.Entity("Store.Models.Entities.Contacts.SupplierPhone", b => + { + b.Property("SupplierPhoneId") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("supplier_phone_id"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("SupplierPhoneId")); + + b.Property("DateCreated") + .HasColumnType("datetime(6)") + .HasColumnName("date_created"); + + b.Property("IsPrimary") + .HasColumnType("tinyint(1)") + .HasColumnName("is_primary"); + + b.Property("LastModified") + .HasColumnType("datetime(6)") + .HasColumnName("last_modified"); + + b.Property("PhoneId") + .HasColumnType("int") + .HasColumnName("phone_id"); + + b.Property("SupplierId") + .HasColumnType("char(36)") + .HasColumnName("supplier_id"); + + b.HasKey("SupplierPhoneId") + .HasName("pk_supplier_phone"); + + b.HasIndex("PhoneId") + .HasDatabaseName("ix_supplier_phone_phone_id"); + + b.HasIndex("SupplierId") + .HasDatabaseName("ix_supplier_phone_supplier_id"); + + b.ToTable("supplier_phone"); + }); + + modelBuilder.Entity("Store.Models.Entities.Contacts.UserEmail", b => + { + b.Property("UserEmailId") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("user_email_id"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("UserEmailId")); + + b.Property("DateCreated") + .HasColumnType("datetime(6)") + .HasColumnName("date_created"); + + b.Property("EmailId") + .HasColumnType("int") + .HasColumnName("email_id"); + + b.Property("IsPrimary") + .HasColumnType("tinyint(1)") + .HasColumnName("is_primary"); + + b.Property("LastModified") + .HasColumnType("datetime(6)") + .HasColumnName("last_modified"); + + b.Property("UserId") + .HasColumnType("char(36)") + .HasColumnName("user_id"); + + b.HasKey("UserEmailId") + .HasName("pk_user_email"); + + b.HasIndex("EmailId") + .HasDatabaseName("ix_user_email_email_id"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_user_email_user_id"); + + b.ToTable("user_email"); + }); + + modelBuilder.Entity("Store.Models.Entities.Contacts.UserPhone", b => + { + b.Property("UserPhoneId") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("user_phone_id"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("UserPhoneId")); + + b.Property("DateCreated") + .HasColumnType("datetime(6)") + .HasColumnName("date_created"); + + b.Property("IsPrimary") + .HasColumnType("tinyint(1)") + .HasColumnName("is_primary"); + + b.Property("LastModified") + .HasColumnType("datetime(6)") + .HasColumnName("last_modified"); + + b.Property("PhoneId") + .HasColumnType("int") + .HasColumnName("phone_id"); + + b.Property("UserId") + .HasColumnType("char(36)") + .HasColumnName("user_id"); + + b.HasKey("UserPhoneId") + .HasName("pk_user_phone"); + + b.HasIndex("PhoneId") + .HasDatabaseName("ix_user_phone_phone_id"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_user_phone_user_id"); + + b.ToTable("user_phone"); + }); + + modelBuilder.Entity("Store.Models.Entities.Country", b => + { + b.Property("CountryId") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("country_id"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("CountryId")); + + b.Property("DateCreated") + .HasColumnType("datetime(6)") + .HasColumnName("date_created"); + + b.Property("IsoCode") + .HasMaxLength(3) + .HasColumnType("varchar(3)") + .HasColumnName("iso_code"); + + b.Property("LastModified") + .HasColumnType("datetime(6)") + .HasColumnName("last_modified"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)") + .HasColumnName("name"); + + b.Property("PhoneCode") + .HasMaxLength(10) + .HasColumnType("varchar(10)") + .HasColumnName("phone_code"); + + b.HasKey("CountryId") + .HasName("pk_country"); + + b.HasIndex("IsoCode") + .IsUnique() + .HasDatabaseName("ix_country_iso_code") + .HasFilter("iso_code IS NOT NULL"); + + b.ToTable("country"); + }); + + modelBuilder.Entity("Store.Models.Entities.Currency", b => + { + b.Property("CurrencyId") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("currency_id"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("CurrencyId")); + + b.Property("Code") + .IsRequired() + .HasColumnType("longtext") + .HasColumnName("code"); + + b.Property("DateCreated") + .HasColumnType("datetime(6)") + .HasColumnName("date_created"); + + b.Property("LastModified") + .HasColumnType("datetime(6)") + .HasColumnName("last_modified"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext") + .HasColumnName("name"); + + b.Property("Symbol") + .IsRequired() + .HasColumnType("longtext") + .HasColumnName("symbol"); + + b.HasKey("CurrencyId") + .HasName("pk_currency"); + + b.ToTable("currency"); + }); + + modelBuilder.Entity("Store.Models.Entities.Customer", b => + { + b.Property("CustomerId") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)") + .HasColumnName("customer_id"); + + b.Property("DateCreated") + .HasColumnType("datetime(6)") + .HasColumnName("date_created"); + + b.Property("DateOfBirth") + .HasColumnType("datetime(6)") + .HasColumnName("date_of_birth"); + + b.Property("FirstName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)") + .HasColumnName("first_name"); + + b.Property("FullImageUrl") + .HasColumnType("longtext") + .HasColumnName("full_image_url"); + + b.Property("Gender") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("varchar(20)") + .HasColumnName("gender"); + + b.Property("LastModified") + .HasColumnType("datetime(6)") + .HasColumnName("last_modified"); + + b.Property("LastName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)") + .HasColumnName("last_name"); + + b.Property("MiddleName") + .HasMaxLength(100) + .HasColumnType("varchar(100)") + .HasColumnName("middle_name"); + + b.Property("Notes") + .HasMaxLength(1000) + .HasColumnType("varchar(1000)") + .HasColumnName("notes"); + + b.Property("Segment") + .HasColumnType("int") + .HasColumnName("segment"); + + b.Property("ThumbnailUrl") + .HasMaxLength(500) + .HasColumnType("varchar(500)") + .HasColumnName("thumbnail_url"); + + b.HasKey("CustomerId") + .HasName("pk_customer"); + + b.ToTable("customer"); + }); + + modelBuilder.Entity("Store.Models.Entities.CustomerLocation", b => + { + b.Property("CustomerLocationId") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("customer_location_id"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("CustomerLocationId")); + + b.Property("CustomerId") + .HasColumnType("char(36)") + .HasColumnName("customer_id"); + + b.Property("DateCreated") + .HasColumnType("datetime(6)") + .HasColumnName("date_created"); + + b.Property("IsPrimary") + .HasColumnType("tinyint(1)") + .HasColumnName("is_primary"); + + b.Property("LastModified") + .HasColumnType("datetime(6)") + .HasColumnName("last_modified"); + + b.Property("LocationId") + .HasColumnType("int") + .HasColumnName("location_id"); + + b.HasKey("CustomerLocationId") + .HasName("pk_customer_location"); + + b.HasIndex("CustomerId") + .HasDatabaseName("ix_customer_location_customer_id"); + + b.HasIndex("LocationId") + .HasDatabaseName("ix_customer_location_location_id"); + + b.ToTable("customer_location"); + }); + + modelBuilder.Entity("Store.Models.Entities.CustomerLoyaltyAccount", b => + { + b.Property("LoyaltyAccountId") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("loyalty_account_id"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("LoyaltyAccountId")); + + b.Property("CustomerId") + .HasColumnType("char(36)") + .HasColumnName("customer_id"); + + b.Property("DateCreated") + .HasColumnType("datetime(6)") + .HasColumnName("date_created"); + + b.Property("LastModified") + .HasColumnType("datetime(6)") + .HasColumnName("last_modified"); + + b.Property("Points") + .HasColumnType("int") + .HasColumnName("points"); + + b.Property("Tier") + .HasColumnType("int") + .HasColumnName("tier"); + + b.HasKey("LoyaltyAccountId") + .HasName("pk_customer_loyalty_account"); + + b.HasIndex("CustomerId") + .IsUnique() + .HasDatabaseName("ix_customer_loyalty_account_customer_id"); + + b.ToTable("customer_loyalty_account"); + }); + + modelBuilder.Entity("Store.Models.Entities.CustomerPrivilege", b => + { + b.Property("CustomerPrivilegeId") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("customer_privilege_id"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("CustomerPrivilegeId")); + + b.Property("CustomerId") + .HasColumnType("char(36)") + .HasColumnName("customer_id"); + + b.Property("DateCreated") + .HasColumnType("datetime(6)") + .HasColumnName("date_created"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)") + .HasColumnName("is_active"); + + b.Property("LastModified") + .HasColumnType("datetime(6)") + .HasColumnName("last_modified"); + + b.Property("PrivilegeId") + .HasColumnType("int") + .HasColumnName("privilege_id"); + + b.Property("Type") + .HasColumnType("int") + .HasColumnName("type"); + + b.HasKey("CustomerPrivilegeId") + .HasName("pk_customer_privilege"); + + b.HasIndex("CustomerId") + .HasDatabaseName("ix_customer_privilege_customer_id"); + + b.HasIndex("PrivilegeId") + .HasDatabaseName("ix_customer_privilege_privilege_id"); + + b.ToTable("customer_privilege"); + }); + + modelBuilder.Entity("Store.Models.Entities.CustomerPrivilegeAction", b => + { + b.Property("CustomerPrivilegeActionId") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("customer_privilege_action_id"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("CustomerPrivilegeActionId")); + + b.Property("Action") + .IsRequired() + .HasColumnType("longtext") + .HasColumnName("action"); + + b.Property("CustomerPrivilegeId") + .HasColumnType("int") + .HasColumnName("customer_privilege_id"); + + b.Property("DateCreated") + .HasColumnType("datetime(6)") + .HasColumnName("date_created"); + + b.Property("LastModified") + .HasColumnType("datetime(6)") + .HasColumnName("last_modified"); + + b.Property("Notes") + .HasColumnType("longtext") + .HasColumnName("notes"); + + b.Property("PerformedByUserId") + .HasColumnType("char(36)") + .HasColumnName("performed_by_user_id"); + + b.HasKey("CustomerPrivilegeActionId") + .HasName("pk_customer_privilege_action"); + + b.HasIndex("CustomerPrivilegeId") + .HasDatabaseName("ix_customer_privilege_action_customer_privilege_id"); + + b.HasIndex("PerformedByUserId") + .HasDatabaseName("ix_customer_privilege_action_performed_by_user_id"); + + b.ToTable("customer_privilege_action"); + }); + + modelBuilder.Entity("Store.Models.Entities.CustomerSegmentPrice", b => + { + b.Property("CustomerSegmentPriceId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("customer_segment_price_id"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("CustomerSegmentPriceId")); + + b.Property("DateCreated") + .HasColumnType("datetime(6)") + .HasColumnName("date_created"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)") + .HasColumnName("is_active"); + + b.Property("ItemId") + .HasColumnType("char(36)") + .HasColumnName("item_id"); + + b.Property("LastModified") + .HasColumnType("datetime(6)") + .HasColumnName("last_modified"); + + b.Property("PriceOverride") + .HasColumnType("decimal(65,30)") + .HasColumnName("price_override"); + + b.Property("Segment") + .HasColumnType("int") + .HasColumnName("segment"); + + b.Property("ValidFrom") + .HasColumnType("datetime(6)") + .HasColumnName("valid_from"); + + b.Property("ValidTo") + .HasColumnType("datetime(6)") + .HasColumnName("valid_to"); + + b.HasKey("CustomerSegmentPriceId") + .HasName("pk_customer_segment_price"); + + b.HasIndex("ItemId", "Segment", "IsActive") + .HasDatabaseName("ix_customer_segment_price_item_id_segment_is_active"); + + b.ToTable("customer_segment_price"); + }); + + modelBuilder.Entity("Store.Models.Entities.Department", b => + { + b.Property("DepartmentId") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("department_id"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("DepartmentId")); + + b.Property("DateCreated") + .HasColumnType("datetime(6)") + .HasColumnName("date_created"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("varchar(500)") + .HasColumnName("description"); + + b.Property("LastModified") + .HasColumnType("datetime(6)") + .HasColumnName("last_modified"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("varchar(150)") + .HasColumnName("name"); + + b.HasKey("DepartmentId") + .HasName("pk_department"); + + b.HasIndex("Name") + .IsUnique() + .HasDatabaseName("ix_department_name"); + + b.ToTable("department"); + }); + + modelBuilder.Entity("Store.Models.Entities.Discount", b => + { + b.Property("DiscountId") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("discount_id"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("DiscountId")); + + b.Property("CategoryId") + .HasColumnType("int") + .HasColumnName("category_id"); + + b.Property("CouponCode") + .HasMaxLength(50) + .HasColumnType("varchar(50)") + .HasColumnName("coupon_code"); + + b.Property("DateCreated") + .HasColumnType("datetime(6)") + .HasColumnName("date_created"); + + b.Property("DiscountType") + .HasColumnType("int") + .HasColumnName("discount_type"); + + b.Property("FixedAmount") + .HasColumnType("decimal(65,30)") + .HasColumnName("fixed_amount"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)") + .HasColumnName("is_active"); + + b.Property("ItemId") + .HasColumnType("char(36)") + .HasColumnName("item_id"); + + b.Property("LastModified") + .HasColumnType("datetime(6)") + .HasColumnName("last_modified"); + + b.Property("ManagedByUserId") + .HasColumnType("char(36)") + .HasColumnName("managed_by_user_id"); + + b.Property("MaxUses") + .HasColumnType("int") + .HasColumnName("max_uses"); + + b.Property("MinQuantity") + .HasColumnType("int") + .HasColumnName("min_quantity"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("varchar(150)") + .HasColumnName("name"); + + b.Property("Percentage") + .HasPrecision(5, 2) + .HasColumnType("decimal(5,2)") + .HasColumnName("percentage"); + + b.Property("TargetSegment") + .HasColumnType("int") + .HasColumnName("target_segment"); + + b.Property("UsedCount") + .HasColumnType("int") + .HasColumnName("used_count"); + + b.Property("ValidFrom") + .HasColumnType("datetime(6)") + .HasColumnName("valid_from"); + + b.Property("ValidTo") + .HasColumnType("datetime(6)") + .HasColumnName("valid_to"); + + b.HasKey("DiscountId") + .HasName("pk_discount"); + + b.HasIndex("CategoryId") + .HasDatabaseName("ix_discount_category_id"); + + b.HasIndex("CouponCode") + .IsUnique() + .HasDatabaseName("ix_discount_coupon_code") + .HasFilter("coupon_code IS NOT NULL"); + + b.HasIndex("ItemId") + .IsUnique() + .HasDatabaseName("ix_discount_item_id"); + + b.HasIndex("ManagedByUserId") + .HasDatabaseName("ix_discount_managed_by_user_id"); + + b.HasIndex("IsActive", "ValidFrom", "ValidTo") + .HasDatabaseName("ix_discount_is_active_valid_from_valid_to"); + + b.ToTable("discount"); + }); + + modelBuilder.Entity("Store.Models.Entities.DiscountOverrideRequest", b => + { + b.Property("DiscountOverrideRequestId") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("discount_override_request_id"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("DiscountOverrideRequestId")); + + b.Property("DateCreated") + .HasColumnType("datetime(6)") + .HasColumnName("date_created"); + + b.Property("InvoiceId") + .HasColumnType("char(36)") + .HasColumnName("invoice_id"); + + b.Property("ItemId") + .HasColumnType("char(36)") + .HasColumnName("item_id"); + + b.Property("Justification") + .HasMaxLength(1000) + .HasColumnType("varchar(1000)") + .HasColumnName("justification"); + + b.Property("LastModified") + .HasColumnType("datetime(6)") + .HasColumnName("last_modified"); + + b.Property("OverrideType") + .HasColumnType("int") + .HasColumnName("override_type"); + + b.Property("OverrideValue") + .HasColumnType("decimal(65,30)") + .HasColumnName("override_value"); + + b.Property("RequestedByUserId") + .HasColumnType("char(36)") + .HasColumnName("requested_by_user_id"); + + b.Property("ReviewNotes") + .HasMaxLength(1000) + .HasColumnType("varchar(1000)") + .HasColumnName("review_notes"); + + b.Property("ReviewedAt") + .HasColumnType("datetime(6)") + .HasColumnName("reviewed_at"); + + b.Property("ReviewedByUserId") + .HasColumnType("char(36)") + .HasColumnName("reviewed_by_user_id"); + + b.Property("Status") + .HasColumnType("int") + .HasColumnName("status"); + + b.HasKey("DiscountOverrideRequestId") + .HasName("pk_discount_override_request"); + + b.HasIndex("InvoiceId") + .HasDatabaseName("ix_discount_override_request_invoice_id"); + + b.HasIndex("ItemId") + .HasDatabaseName("ix_discount_override_request_item_id"); + + b.HasIndex("RequestedByUserId") + .HasDatabaseName("ix_discount_override_request_requested_by_user_id"); + + b.HasIndex("ReviewedByUserId") + .HasDatabaseName("ix_discount_override_request_reviewed_by_user_id"); + + b.HasIndex("Status", "DateCreated") + .HasDatabaseName("ix_discount_override_request_status_date_created"); + + b.ToTable("discount_override_request"); + }); + + modelBuilder.Entity("Store.Models.Entities.Document", b => + { + b.Property("DocumentId") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("document_id"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("DocumentId")); + + b.Property("DateCreated") + .HasColumnType("datetime(6)") + .HasColumnName("date_created"); + + b.Property("EntityId") + .HasColumnType("longtext") + .HasColumnName("entity_id"); + + b.Property("EntityName") + .HasColumnType("longtext") + .HasColumnName("entity_name"); + + b.Property("FileName") + .IsRequired() + .HasColumnType("longtext") + .HasColumnName("file_name"); + + b.Property("FilePath") + .IsRequired() + .HasColumnType("longtext") + .HasColumnName("file_path"); + + b.Property("FileSizeBytes") + .HasColumnType("bigint") + .HasColumnName("file_size_bytes"); + + b.Property("LastModified") + .HasColumnType("datetime(6)") + .HasColumnName("last_modified"); + + b.Property("MimeType") + .HasColumnType("longtext") + .HasColumnName("mime_type"); + + b.Property("UploadedByUserId") + .HasColumnType("longtext") + .HasColumnName("uploaded_by_user_id"); + + b.HasKey("DocumentId") + .HasName("pk_document"); + + b.ToTable("document"); + }); + + modelBuilder.Entity("Store.Models.Entities.Employee", b => + { + b.Property("EmployeeId") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)") + .HasColumnName("employee_id"); + + b.Property("DateCreated") + .HasColumnType("datetime(6)") + .HasColumnName("date_created"); + + b.Property("DateEmployed") + .HasColumnType("datetime(6)") + .HasColumnName("date_employed"); + + b.Property("DateOfBirth") + .HasColumnType("datetime(6)") + .HasColumnName("date_of_birth"); + + b.Property("DepartmentId") + .HasColumnType("int") + .HasColumnName("department_id"); + + b.Property("FirstName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)") + .HasColumnName("first_name"); + + b.Property("FullImageUrl") + .HasColumnType("longtext") + .HasColumnName("full_image_url"); + + b.Property("Gender") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(20) + .HasColumnType("varchar(20)") + .HasDefaultValue("NotSpecified") + .HasColumnName("gender"); + + b.Property("LastModified") + .HasColumnType("datetime(6)") + .HasColumnName("last_modified"); + + b.Property("LastName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)") + .HasColumnName("last_name"); + + b.Property("MiddleName") + .HasMaxLength(100) + .HasColumnType("varchar(100)") + .HasColumnName("middle_name"); + + b.Property("NidNumber") + .HasMaxLength(50) + .HasColumnType("varchar(50)") + .HasColumnName("nid_number"); + + b.Property("PlaceOfBirth") + .HasMaxLength(200) + .HasColumnType("varchar(200)") + .HasColumnName("place_of_birth"); + + b.Property("SalaryId") + .HasColumnType("int") + .HasColumnName("salary_id"); + + b.Property("Status") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(50) + .HasColumnType("varchar(50)") + .HasDefaultValue("Pending") + .HasColumnName("status"); + + b.Property("ThumbnailUrl") + .HasMaxLength(500) + .HasColumnType("varchar(500)") + .HasColumnName("thumbnail_url"); + + b.HasKey("EmployeeId") + .HasName("pk_employee"); + + b.HasIndex("DepartmentId") + .HasDatabaseName("ix_employee_department_id"); + + b.HasIndex("SalaryId") + .HasDatabaseName("ix_employee_salary_id"); + + b.ToTable("employee"); + }); + + modelBuilder.Entity("Store.Models.Entities.EmployeeLocation", b => + { + b.Property("EmployeeLocationId") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("employee_location_id"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("EmployeeLocationId")); + + b.Property("DateCreated") + .HasColumnType("datetime(6)") + .HasColumnName("date_created"); + + b.Property("EmployeeId") + .HasColumnType("char(36)") + .HasColumnName("employee_id"); + + b.Property("IsPrimary") + .HasColumnType("tinyint(1)") + .HasColumnName("is_primary"); + + b.Property("LastModified") + .HasColumnType("datetime(6)") + .HasColumnName("last_modified"); + + b.Property("LocationId") + .HasColumnType("int") + .HasColumnName("location_id"); + + b.HasKey("EmployeeLocationId") + .HasName("pk_employee_location"); + + b.HasIndex("EmployeeId") + .HasDatabaseName("ix_employee_location_employee_id"); + + b.HasIndex("LocationId") + .HasDatabaseName("ix_employee_location_location_id"); + + b.ToTable("employee_location"); + }); + + modelBuilder.Entity("Store.Models.Entities.EmployeePrivilege", b => + { + b.Property("EmployeePrivilegeId") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("employee_privilege_id"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("EmployeePrivilegeId")); + + b.Property("DateCreated") + .HasColumnType("datetime(6)") + .HasColumnName("date_created"); + + b.Property("EmployeeId") + .HasColumnType("char(36)") + .HasColumnName("employee_id"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)") + .HasColumnName("is_active"); + + b.Property("LastModified") + .HasColumnType("datetime(6)") + .HasColumnName("last_modified"); + + b.Property("PrivilegeId") + .HasColumnType("int") + .HasColumnName("privilege_id"); + + b.Property("Type") + .HasColumnType("int") + .HasColumnName("type"); + + b.HasKey("EmployeePrivilegeId") + .HasName("pk_employee_privilege"); + + b.HasIndex("EmployeeId") + .HasDatabaseName("ix_employee_privilege_employee_id"); + + b.HasIndex("PrivilegeId") + .HasDatabaseName("ix_employee_privilege_privilege_id"); + + b.ToTable("employee_privilege"); + }); + + modelBuilder.Entity("Store.Models.Entities.EmployeePrivilegeAction", b => + { + b.Property("EmployeePrivilegeActionId") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("employee_privilege_action_id"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("EmployeePrivilegeActionId")); + + b.Property("Action") + .IsRequired() + .HasColumnType("longtext") + .HasColumnName("action"); + + b.Property("DateCreated") + .HasColumnType("datetime(6)") + .HasColumnName("date_created"); + + b.Property("EmployeePrivilegeId") + .HasColumnType("int") + .HasColumnName("employee_privilege_id"); + + b.Property("LastModified") + .HasColumnType("datetime(6)") + .HasColumnName("last_modified"); + + b.Property("Notes") + .HasColumnType("longtext") + .HasColumnName("notes"); + + b.Property("PerformedByUserId") + .HasColumnType("char(36)") + .HasColumnName("performed_by_user_id"); + + b.HasKey("EmployeePrivilegeActionId") + .HasName("pk_employee_privilege_action"); + + b.HasIndex("EmployeePrivilegeId") + .HasDatabaseName("ix_employee_privilege_action_employee_privilege_id"); + + b.HasIndex("PerformedByUserId") + .HasDatabaseName("ix_employee_privilege_action_performed_by_user_id"); + + b.ToTable("employee_privilege_action"); + }); + + modelBuilder.Entity("Store.Models.Entities.FidoCredential", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("id"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("AaGuid") + .HasColumnType("char(36)") + .HasColumnName("aa_guid"); + + b.Property("CredType") + .IsRequired() + .HasColumnType("longtext") + .HasColumnName("cred_type"); + + b.Property("CredentialId") + .IsRequired() + .HasColumnType("longblob") + .HasColumnName("credential_id"); + + b.Property("DateCreated") + .HasColumnType("datetime(6)") + .HasColumnName("date_created"); + + b.Property("LastModified") + .HasColumnType("datetime(6)") + .HasColumnName("last_modified"); + + b.Property("PublicKey") + .IsRequired() + .HasColumnType("longblob") + .HasColumnName("public_key"); + + b.Property("RegDate") + .HasColumnType("datetime(6)") + .HasColumnName("reg_date"); + + b.Property("SignatureCounter") + .HasColumnType("int unsigned") + .HasColumnName("signature_counter"); + + b.Property("UserHandle") + .IsRequired() + .HasColumnType("longblob") + .HasColumnName("user_handle"); + + b.Property("UserId") + .HasColumnType("char(36)") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_fido_credential"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_fido_credential_user_id"); + + b.ToTable("fido_credential"); + }); + + modelBuilder.Entity("Store.Models.Entities.Invoice", b => + { + b.Property("InvoiceId") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)") + .HasColumnName("invoice_id"); + + b.Property("AmountTendered") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)") + .HasColumnName("amount_tendered"); + + b.Property("BranchId") + .HasColumnType("int") + .HasColumnName("branch_id"); + + b.Property("ChangeGiven") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)") + .HasColumnName("change_given"); + + b.Property("CustomerId") + .HasColumnType("char(36)") + .HasColumnName("customer_id"); + + b.Property("DateCreated") + .HasColumnType("datetime(6)") + .HasColumnName("date_created"); + + b.Property("IsPaid") + .HasColumnType("tinyint(1)") + .HasColumnName("is_paid"); + + b.Property("LastModified") + .HasColumnType("datetime(6)") + .HasColumnName("last_modified"); + + b.Property("Notes") + .HasMaxLength(1000) + .HasColumnType("varchar(1000)") + .HasColumnName("notes"); + + b.Property("PaymentType") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(30) + .HasColumnType("varchar(30)") + .HasDefaultValue("Cash") + .HasColumnName("payment_type"); + + b.Property("TotalAmount") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)") + .HasColumnName("total_amount"); + + b.Property("UserId") + .HasColumnType("char(36)") + .HasColumnName("user_id"); + + b.HasKey("InvoiceId") + .HasName("pk_invoice"); + + b.HasIndex("BranchId") + .HasDatabaseName("ix_invoice_branch_id"); + + b.HasIndex("CustomerId") + .HasDatabaseName("ix_invoice_customer_id"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_invoice_user_id"); + + b.ToTable("invoice"); + }); + + modelBuilder.Entity("Store.Models.Entities.InvoiceTender", b => + { + b.Property("InvoiceTenderId") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("invoice_tender_id"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("InvoiceTenderId")); + + b.Property("Amount") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)") + .HasColumnName("amount"); + + b.Property("DateCreated") + .HasColumnType("datetime(6)") + .HasColumnName("date_created"); + + b.Property("InvoiceId") + .HasColumnType("char(36)") + .HasColumnName("invoice_id"); + + b.Property("LastModified") + .HasColumnType("datetime(6)") + .HasColumnName("last_modified"); + + b.Property("PaymentType") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("varchar(30)") + .HasColumnName("payment_type"); + + b.Property("Reference") + .HasMaxLength(200) + .HasColumnType("varchar(200)") + .HasColumnName("reference"); + + b.HasKey("InvoiceTenderId") + .HasName("pk_invoice_tender"); + + b.HasIndex("InvoiceId") + .HasDatabaseName("ix_invoice_tender_invoice_id"); + + b.ToTable("invoice_tender"); + }); + + modelBuilder.Entity("Store.Models.Entities.Item", b => + { + b.Property("ItemId") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)") + .HasColumnName("item_id"); + + b.Property("Barcode") + .HasMaxLength(100) + .HasColumnType("varchar(100)") + .HasColumnName("barcode"); + + b.Property("CategoryId") + .HasColumnType("int") + .HasColumnName("category_id"); + + b.Property("CostPrice") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)") + .HasColumnName("cost_price"); + + b.Property("DateCreated") + .HasColumnType("datetime(6)") + .HasColumnName("date_created"); + + b.Property("Description") + .HasMaxLength(2000) + .HasColumnType("varchar(2000)") + .HasColumnName("description"); + + b.Property("FullImageUrl") + .HasColumnType("longtext") + .HasColumnName("full_image_url"); + + b.Property("InStock") + .HasColumnType("int") + .HasColumnName("in_stock"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)") + .HasColumnName("is_active"); + + b.Property("LastModified") + .HasColumnType("datetime(6)") + .HasColumnName("last_modified"); + + b.Property("ManufacturerId") + .HasColumnType("char(36)") + .HasColumnName("manufacturer_id"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)") + .HasColumnName("name"); + + b.Property("ReorderLevel") + .HasColumnType("int") + .HasColumnName("reorder_level"); + + b.Property("TaxProfileId") + .HasColumnType("int") + .HasColumnName("tax_profile_id"); + + b.Property("ThumbnailUrl") + .HasMaxLength(500) + .HasColumnType("varchar(500)") + .HasColumnName("thumbnail_url"); + + b.Property("Type") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(20) + .HasColumnType("varchar(20)") + .HasDefaultValue("Product") + .HasColumnName("type"); + + b.Property("UnitId") + .HasColumnType("int") + .HasColumnName("unit_id"); + + b.Property("UnitPrice") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)") + .HasColumnName("unit_price"); + + b.HasKey("ItemId") + .HasName("pk_item"); + + b.HasIndex("Barcode") + .IsUnique() + .HasDatabaseName("ix_item_barcode") + .HasFilter("barcode IS NOT NULL"); + + b.HasIndex("CategoryId") + .HasDatabaseName("ix_item_category_id"); + + b.HasIndex("ManufacturerId") + .HasDatabaseName("ix_item_manufacturer_id"); + + b.HasIndex("TaxProfileId") + .HasDatabaseName("ix_item_tax_profile_id"); + + b.HasIndex("UnitId") + .HasDatabaseName("ix_item_unit_id"); + + b.ToTable("item"); + }); + + modelBuilder.Entity("Store.Models.Entities.ItemCategory", b => + { + b.Property("ItemCategoryId") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("item_category_id"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("ItemCategoryId")); + + b.Property("CategoryId") + .HasColumnType("int") + .HasColumnName("category_id"); + + b.Property("DateCreated") + .HasColumnType("datetime(6)") + .HasColumnName("date_created"); + + b.Property("ItemId") + .HasColumnType("char(36)") + .HasColumnName("item_id"); + + b.Property("LastModified") + .HasColumnType("datetime(6)") + .HasColumnName("last_modified"); + + b.HasKey("ItemCategoryId") + .HasName("pk_item_category"); + + b.HasIndex("CategoryId") + .HasDatabaseName("ix_item_category_category_id"); + + b.HasIndex("ItemId") + .HasDatabaseName("ix_item_category_item_id"); + + b.ToTable("item_category"); + }); + + modelBuilder.Entity("Store.Models.Entities.ItemCode", b => + { + b.Property("ItemCodeId") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("item_code_id"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("ItemCodeId")); + + b.Property("Code") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)") + .HasColumnName("code"); + + b.Property("CodeType") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)") + .HasColumnName("code_type"); + + b.Property("DateCreated") + .HasColumnType("datetime(6)") + .HasColumnName("date_created"); + + b.Property("ItemId") + .HasColumnType("char(36)") + .HasColumnName("item_id"); + + b.Property("LastModified") + .HasColumnType("datetime(6)") + .HasColumnName("last_modified"); + + b.HasKey("ItemCodeId") + .HasName("pk_item_code"); + + b.HasIndex("Code") + .IsUnique() + .HasDatabaseName("ix_item_code_code"); + + b.HasIndex("ItemId") + .HasDatabaseName("ix_item_code_item_id"); + + b.ToTable("item_code"); + }); + + modelBuilder.Entity("Store.Models.Entities.ItemExpiry", b => + { + b.Property("ItemExpiryId") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("item_expiry_id"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("ItemExpiryId")); + + b.Property("DateCreated") + .HasColumnType("datetime(6)") + .HasColumnName("date_created"); + + b.Property("DaysWarningBefore") + .HasColumnType("int") + .HasColumnName("days_warning_before"); + + b.Property("ExpiryDate") + .HasColumnType("datetime(6)") + .HasColumnName("expiry_date"); + + b.Property("ItemId") + .HasColumnType("char(36)") + .HasColumnName("item_id"); + + b.Property("LastModified") + .HasColumnType("datetime(6)") + .HasColumnName("last_modified"); + + b.HasKey("ItemExpiryId") + .HasName("pk_item_expiry"); + + b.HasIndex("ItemId") + .IsUnique() + .HasDatabaseName("ix_item_expiry_item_id"); + + b.ToTable("item_expiry"); + }); + + modelBuilder.Entity("Store.Models.Entities.ItemsOrder", b => + { + b.Property("ItemsOrderId") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)") + .HasColumnName("items_order_id"); + + b.Property("CreatedByUserId") + .HasColumnType("char(36)") + .HasColumnName("created_by_user_id"); + + b.Property("DateCreated") + .HasColumnType("datetime(6)") + .HasColumnName("date_created"); + + b.Property("ExpectedDeliveryDate") + .HasColumnType("datetime(6)") + .HasColumnName("expected_delivery_date"); + + b.Property("LastModified") + .HasColumnType("datetime(6)") + .HasColumnName("last_modified"); + + b.Property("Notes") + .HasMaxLength(1000) + .HasColumnType("varchar(1000)") + .HasColumnName("notes"); + + b.Property("OrderNumber") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)") + .HasColumnName("order_number"); + + b.Property("Status") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(30) + .HasColumnType("varchar(30)") + .HasDefaultValue("Draft") + .HasColumnName("status"); + + b.Property("SupplierId") + .HasColumnType("char(36)") + .HasColumnName("supplier_id"); + + b.Property("TotalAmount") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)") + .HasColumnName("total_amount"); + + b.HasKey("ItemsOrderId") + .HasName("pk_items_order"); + + b.HasIndex("CreatedByUserId") + .HasDatabaseName("ix_items_order_created_by_user_id"); + + b.HasIndex("OrderNumber") + .IsUnique() + .HasDatabaseName("ix_items_order_order_number"); + + b.HasIndex("SupplierId") + .HasDatabaseName("ix_items_order_supplier_id"); + + b.ToTable("items_order"); + }); + + modelBuilder.Entity("Store.Models.Entities.Language", b => + { + b.Property("LanguageId") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("language_id"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("LanguageId")); + + b.Property("Code") + .IsRequired() + .HasColumnType("longtext") + .HasColumnName("code"); + + b.Property("DateCreated") + .HasColumnType("datetime(6)") + .HasColumnName("date_created"); + + b.Property("LastModified") + .HasColumnType("datetime(6)") + .HasColumnName("last_modified"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext") + .HasColumnName("name"); + + b.HasKey("LanguageId") + .HasName("pk_language"); + + b.ToTable("language"); + }); + + modelBuilder.Entity("Store.Models.Entities.Location", b => + { + b.Property("LocationId") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("location_id"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("LocationId")); + + b.Property("CityId") + .HasColumnType("int") + .HasColumnName("city_id"); + + b.Property("DateCreated") + .HasColumnType("datetime(6)") + .HasColumnName("date_created"); + + b.Property("LastModified") + .HasColumnType("datetime(6)") + .HasColumnName("last_modified"); + + b.Property("Latitude") + .HasMaxLength(20) + .HasColumnType("varchar(20)") + .HasColumnName("latitude"); + + b.Property("Longitude") + .HasMaxLength(20) + .HasColumnType("varchar(20)") + .HasColumnName("longitude"); + + b.Property("PostalCode") + .HasMaxLength(20) + .HasColumnType("varchar(20)") + .HasColumnName("postal_code"); + + b.Property("StreetAddress") + .HasMaxLength(300) + .HasColumnType("varchar(300)") + .HasColumnName("street_address"); + + b.HasKey("LocationId") + .HasName("pk_location"); + + b.HasIndex("CityId") + .HasDatabaseName("ix_location_city_id"); + + b.ToTable("location"); + }); + + modelBuilder.Entity("Store.Models.Entities.LoyaltyCampaign", b => + { + b.Property("LoyaltyCampaignId") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("loyalty_campaign_id"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("LoyaltyCampaignId")); + + b.Property("BonusPoints") + .HasColumnType("int") + .HasColumnName("bonus_points"); + + b.Property("CampaignType") + .HasColumnType("int") + .HasColumnName("campaign_type"); + + b.Property("DateCreated") + .HasColumnType("datetime(6)") + .HasColumnName("date_created"); + + b.Property("Description") + .HasMaxLength(1000) + .HasColumnType("varchar(1000)") + .HasColumnName("description"); + + b.Property("EndDate") + .HasColumnType("datetime(6)") + .HasColumnName("end_date"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)") + .HasColumnName("is_active"); + + b.Property("LastModified") + .HasColumnType("datetime(6)") + .HasColumnName("last_modified"); + + b.Property("MultiplierFactor") + .HasColumnType("decimal(65,30)") + .HasColumnName("multiplier_factor"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)") + .HasColumnName("name"); + + b.Property("StartDate") + .HasColumnType("datetime(6)") + .HasColumnName("start_date"); + + b.Property("TargetSegment") + .HasColumnType("int") + .HasColumnName("target_segment"); + + b.HasKey("LoyaltyCampaignId") + .HasName("pk_loyalty_campaign"); + + b.HasIndex("IsActive", "StartDate", "EndDate") + .HasDatabaseName("ix_loyalty_campaign_is_active_start_date_end_date"); + + b.ToTable("loyalty_campaign"); + }); + + modelBuilder.Entity("Store.Models.Entities.LoyaltyTransaction", b => + { + b.Property("LoyaltyTransactionId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("loyalty_transaction_id"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("LoyaltyTransactionId")); + + b.Property("DateCreated") + .HasColumnType("datetime(6)") + .HasColumnName("date_created"); + + b.Property("InvoiceId") + .HasColumnType("char(36)") + .HasColumnName("invoice_id"); + + b.Property("LastModified") + .HasColumnType("datetime(6)") + .HasColumnName("last_modified"); + + b.Property("LoyaltyAccountId") + .HasColumnType("int") + .HasColumnName("loyalty_account_id"); + + b.Property("Note") + .HasColumnType("longtext") + .HasColumnName("note"); + + b.Property("Points") + .HasColumnType("int") + .HasColumnName("points"); + + b.Property("TransactionType") + .HasColumnType("int") + .HasColumnName("transaction_type"); + + b.HasKey("LoyaltyTransactionId") + .HasName("pk_loyalty_transaction"); + + b.HasIndex("InvoiceId") + .HasDatabaseName("ix_loyalty_transaction_invoice_id"); + + b.HasIndex("LoyaltyAccountId") + .HasDatabaseName("ix_loyalty_transaction_loyalty_account_id"); + + b.ToTable("loyalty_transaction"); + }); + + modelBuilder.Entity("Store.Models.Entities.Manufacturer", b => + { + b.Property("ManufacturerId") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)") + .HasColumnName("manufacturer_id"); + + b.Property("DateCreated") + .HasColumnType("datetime(6)") + .HasColumnName("date_created"); + + b.Property("FullImageUrl") + .HasColumnType("longtext") + .HasColumnName("full_image_url"); + + b.Property("LastModified") + .HasColumnType("datetime(6)") + .HasColumnName("last_modified"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)") + .HasColumnName("name"); + + b.Property("Notes") + .HasMaxLength(1000) + .HasColumnType("varchar(1000)") + .HasColumnName("notes"); + + b.Property("RegistrationNumber") + .HasMaxLength(100) + .HasColumnType("varchar(100)") + .HasColumnName("registration_number"); + + b.Property("ThumbnailUrl") + .HasMaxLength(500) + .HasColumnType("varchar(500)") + .HasColumnName("thumbnail_url"); + + b.Property("Website") + .HasMaxLength(300) + .HasColumnType("varchar(300)") + .HasColumnName("website"); + + b.HasKey("ManufacturerId") + .HasName("pk_manufacturer"); + + b.ToTable("manufacturer"); + }); + + modelBuilder.Entity("Store.Models.Entities.ManufacturerLocation", b => + { + b.Property("ManufacturerLocationId") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("manufacturer_location_id"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("ManufacturerLocationId")); + + b.Property("DateCreated") + .HasColumnType("datetime(6)") + .HasColumnName("date_created"); + + b.Property("IsPrimary") + .HasColumnType("tinyint(1)") + .HasColumnName("is_primary"); + + b.Property("LastModified") + .HasColumnType("datetime(6)") + .HasColumnName("last_modified"); + + b.Property("LocationId") + .HasColumnType("int") + .HasColumnName("location_id"); + + b.Property("ManufacturerId") + .HasColumnType("char(36)") + .HasColumnName("manufacturer_id"); + + b.HasKey("ManufacturerLocationId") + .HasName("pk_manufacturer_location"); + + b.HasIndex("LocationId") + .HasDatabaseName("ix_manufacturer_location_location_id"); + + b.HasIndex("ManufacturerId") + .HasDatabaseName("ix_manufacturer_location_manufacturer_id"); + + b.ToTable("manufacturer_location"); + }); + + modelBuilder.Entity("Store.Models.Entities.MobileMoneyTransaction", b => + { + b.Property("MobileMoneyTransactionId") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)") + .HasColumnName("mobile_money_transaction_id"); + + b.Property("Amount") + .HasColumnType("decimal(65,30)") + .HasColumnName("amount"); + + b.Property("CallbackPayload") + .HasColumnType("longtext") + .HasColumnName("callback_payload"); + + b.Property("CompletedAtUtc") + .HasColumnType("datetime(6)") + .HasColumnName("completed_at_utc"); + + b.Property("DateCreated") + .HasColumnType("datetime(6)") + .HasColumnName("date_created"); + + b.Property("InvoiceId") + .HasColumnType("char(36)") + .HasColumnName("invoice_id"); + + b.Property("LastModified") + .HasColumnType("datetime(6)") + .HasColumnName("last_modified"); + + b.Property("PhoneNumber") + .IsRequired() + .HasColumnType("longtext") + .HasColumnName("phone_number"); + + b.Property("Provider") + .HasColumnType("int") + .HasColumnName("provider"); + + b.Property("ProviderTransactionId") + .HasColumnType("longtext") + .HasColumnName("provider_transaction_id"); + + b.Property("Status") + .HasColumnType("int") + .HasColumnName("status"); + + b.HasKey("MobileMoneyTransactionId") + .HasName("pk_mobile_money_transaction"); + + b.HasIndex("InvoiceId") + .HasDatabaseName("ix_mobile_money_transaction_invoice_id"); + + b.ToTable("mobile_money_transaction"); + }); + + modelBuilder.Entity("Store.Models.Entities.Notification", b => + { + b.Property("NotificationId") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("notification_id"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("NotificationId")); + + b.Property("DateCreated") + .HasColumnType("datetime(6)") + .HasColumnName("date_created"); + + b.Property("IsRead") + .HasColumnType("tinyint(1)") + .HasColumnName("is_read"); + + b.Property("LastModified") + .HasColumnType("datetime(6)") + .HasColumnName("last_modified"); + + b.Property("Message") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("varchar(2000)") + .HasColumnName("message"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)") + .HasColumnName("title"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("varchar(20)") + .HasColumnName("type"); + + b.Property("UserId") + .HasColumnType("char(36)") + .HasColumnName("user_id"); + + b.HasKey("NotificationId") + .HasName("pk_notification"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_notification_user_id"); + + b.ToTable("notification"); + }); + + modelBuilder.Entity("Store.Models.Entities.OrderItem", b => + { + b.Property("OrderItemId") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("order_item_id"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("OrderItemId")); + + b.Property("DateCreated") + .HasColumnType("datetime(6)") + .HasColumnName("date_created"); + + b.Property("ItemId") + .HasColumnType("char(36)") + .HasColumnName("item_id"); + + b.Property("ItemName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)") + .HasColumnName("item_name"); + + b.Property("ItemsOrderId") + .HasColumnType("char(36)") + .HasColumnName("items_order_id"); + + b.Property("LastModified") + .HasColumnType("datetime(6)") + .HasColumnName("last_modified"); + + b.Property("LineTotal") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)") + .HasColumnName("line_total"); + + b.Property("QuantityOrdered") + .HasColumnType("int") + .HasColumnName("quantity_ordered"); + + b.Property("QuantityReceived") + .HasColumnType("int") + .HasColumnName("quantity_received"); + + b.Property("UnitCost") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)") + .HasColumnName("unit_cost"); + + b.HasKey("OrderItemId") + .HasName("pk_order_item"); + + b.HasIndex("ItemId") + .HasDatabaseName("ix_order_item_item_id"); + + b.HasIndex("ItemsOrderId") + .HasDatabaseName("ix_order_item_items_order_id"); + + b.ToTable("order_item"); + }); + + modelBuilder.Entity("Store.Models.Entities.Otp", b => + { + b.Property("OtpId") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("otp_id"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("OtpId")); + + b.Property("Code") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("varchar(10)") + .HasColumnName("code"); + + b.Property("DateCreated") + .HasColumnType("datetime(6)") + .HasColumnName("date_created"); + + b.Property("ExpiresAt") + .HasColumnType("datetime(6)") + .HasColumnName("expires_at"); + + b.Property("IsUsed") + .HasColumnType("tinyint(1)") + .HasColumnName("is_used"); + + b.Property("LastModified") + .HasColumnType("datetime(6)") + .HasColumnName("last_modified"); + + b.Property("Purpose") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("varchar(30)") + .HasColumnName("purpose"); + + b.Property("UserId") + .HasColumnType("char(36)") + .HasColumnName("user_id"); + + b.HasKey("OtpId") + .HasName("pk_otp"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_otp_user_id"); + + b.ToTable("otp"); + }); + + modelBuilder.Entity("Store.Models.Entities.PasswordResetToken", b => + { + b.Property("PasswordResetTokenId") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("password_reset_token_id"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("PasswordResetTokenId")); + + b.Property("DateCreated") + .HasColumnType("datetime(6)") + .HasColumnName("date_created"); + + b.Property("ExpiryDate") + .HasColumnType("datetime(6)") + .HasColumnName("expiry_date"); + + b.Property("IsUsed") + .HasColumnType("tinyint(1)") + .HasColumnName("is_used"); + + b.Property("LastModified") + .HasColumnType("datetime(6)") + .HasColumnName("last_modified"); + + b.Property("TokenHash") + .IsRequired() + .HasColumnType("longtext") + .HasColumnName("token_hash"); + + b.Property("UserId") + .HasColumnType("char(36)") + .HasColumnName("user_id"); + + b.HasKey("PasswordResetTokenId") + .HasName("pk_password_reset_token"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_password_reset_token_user_id"); + + b.ToTable("password_reset_token"); + }); + + modelBuilder.Entity("Store.Models.Entities.Privilege", b => + { + b.Property("PrivilegeId") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("privilege_id"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("PrivilegeId")); + + b.Property("DateCreated") + .HasColumnType("datetime(6)") + .HasColumnName("date_created"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("varchar(500)") + .HasColumnName("description"); + + b.Property("LastModified") + .HasColumnType("datetime(6)") + .HasColumnName("last_modified"); + + b.Property("Module") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)") + .HasColumnName("module"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)") + .HasColumnName("name"); + + b.HasKey("PrivilegeId") + .HasName("pk_privilege"); + + b.ToTable("privilege"); + }); + + modelBuilder.Entity("Store.Models.Entities.PurchaseOrder", b => + { + b.Property("PurchaseOrderId") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("purchase_order_id"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("PurchaseOrderId")); + + b.Property("ApprovedAt") + .HasColumnType("datetime(6)") + .HasColumnName("approved_at"); + + b.Property("ApprovedByUserId") + .HasColumnType("char(36)") + .HasColumnName("approved_by_user_id"); + + b.Property("BranchId") + .HasColumnType("int") + .HasColumnName("branch_id"); + + b.Property("DateCreated") + .HasColumnType("datetime(6)") + .HasColumnName("date_created"); + + b.Property("ExpectedDeliveryDate") + .HasColumnType("datetime(6)") + .HasColumnName("expected_delivery_date"); + + b.Property("LastModified") + .HasColumnType("datetime(6)") + .HasColumnName("last_modified"); + + b.Property("Notes") + .HasMaxLength(2000) + .HasColumnType("varchar(2000)") + .HasColumnName("notes"); + + b.Property("ReceivedAt") + .HasColumnType("datetime(6)") + .HasColumnName("received_at"); + + b.Property("ReferenceNumber") + .HasMaxLength(100) + .HasColumnType("varchar(100)") + .HasColumnName("reference_number"); + + b.Property("RequestedByUserId") + .HasColumnType("char(36)") + .HasColumnName("requested_by_user_id"); + + b.Property("Status") + .HasColumnType("int") + .HasColumnName("status"); + + b.Property("SupplierId") + .HasColumnType("char(36)") + .HasColumnName("supplier_id"); + + b.HasKey("PurchaseOrderId") + .HasName("pk_purchase_order"); + + b.HasIndex("ApprovedByUserId") + .HasDatabaseName("ix_purchase_order_approved_by_user_id"); + + b.HasIndex("BranchId") + .HasDatabaseName("ix_purchase_order_branch_id"); + + b.HasIndex("ReferenceNumber") + .IsUnique() + .HasDatabaseName("ix_purchase_order_reference_number") + .HasFilter("reference_number IS NOT NULL"); + + b.HasIndex("RequestedByUserId") + .HasDatabaseName("ix_purchase_order_requested_by_user_id"); + + b.HasIndex("SupplierId") + .HasDatabaseName("ix_purchase_order_supplier_id"); + + b.HasIndex("Status", "DateCreated") + .HasDatabaseName("ix_purchase_order_status_date_created"); + + b.ToTable("purchase_order"); + }); + + modelBuilder.Entity("Store.Models.Entities.PurchaseOrderItem", b => + { + b.Property("PurchaseOrderItemId") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("purchase_order_item_id"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("PurchaseOrderItemId")); + + b.Property("DateCreated") + .HasColumnType("datetime(6)") + .HasColumnName("date_created"); + + b.Property("ItemId") + .HasColumnType("char(36)") + .HasColumnName("item_id"); + + b.Property("LastModified") + .HasColumnType("datetime(6)") + .HasColumnName("last_modified"); + + b.Property("Notes") + .HasMaxLength(500) + .HasColumnType("varchar(500)") + .HasColumnName("notes"); + + b.Property("OrderedQuantity") + .HasColumnType("int") + .HasColumnName("ordered_quantity"); + + b.Property("PurchaseOrderId") + .HasColumnType("int") + .HasColumnName("purchase_order_id"); + + b.Property("ReceivedQuantity") + .HasColumnType("int") + .HasColumnName("received_quantity"); + + b.Property("UnitCost") + .HasColumnType("decimal(65,30)") + .HasColumnName("unit_cost"); + + b.HasKey("PurchaseOrderItemId") + .HasName("pk_purchase_order_item"); + + b.HasIndex("ItemId") + .HasDatabaseName("ix_purchase_order_item_item_id"); + + b.HasIndex("PurchaseOrderId") + .HasDatabaseName("ix_purchase_order_item_purchase_order_id"); + + b.ToTable("purchase_order_item"); + }); + + modelBuilder.Entity("Store.Models.Entities.Region", b => + { + b.Property("RegionId") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("region_id"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("RegionId")); + + b.Property("CountryId") + .HasColumnType("int") + .HasColumnName("country_id"); + + b.Property("DateCreated") + .HasColumnType("datetime(6)") + .HasColumnName("date_created"); + + b.Property("LastModified") + .HasColumnType("datetime(6)") + .HasColumnName("last_modified"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)") + .HasColumnName("name"); + + b.HasKey("RegionId") + .HasName("pk_region"); + + b.HasIndex("CountryId") + .HasDatabaseName("ix_region_country_id"); + + b.ToTable("region"); + }); + + modelBuilder.Entity("Store.Models.Entities.Role", b => + { + b.Property("RoleId") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("role_id"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("RoleId")); + + b.Property("DateCreated") + .HasColumnType("datetime(6)") + .HasColumnName("date_created"); + + b.Property("Description") + .HasMaxLength(300) + .HasColumnType("varchar(300)") + .HasColumnName("description"); + + b.Property("LastModified") + .HasColumnType("datetime(6)") + .HasColumnName("last_modified"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)") + .HasColumnName("name"); + + b.HasKey("RoleId") + .HasName("pk_role"); + + b.HasIndex("Name") + .IsUnique() + .HasDatabaseName("ix_role_name"); + + b.ToTable("role"); + + b.HasData( + new + { + RoleId = 1, + DateCreated = new DateTime(2024, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Full system access", + LastModified = new DateTime(2024, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Name = "Admin" + }, + new + { + RoleId = 2, + DateCreated = new DateTime(2024, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Management level access", + LastModified = new DateTime(2024, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Name = "Manager" + }, + new + { + RoleId = 3, + DateCreated = new DateTime(2024, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Standard user access", + LastModified = new DateTime(2024, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), + Name = "User" + }); + }); + + modelBuilder.Entity("Store.Models.Entities.RolePermission", b => + { + b.Property("RolePermissionId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("role_permission_id"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("RolePermissionId")); + + b.Property("DateCreated") + .HasColumnType("datetime(6)") + .HasColumnName("date_created"); + + b.Property("IsAllowed") + .HasColumnType("tinyint(1)") + .HasColumnName("is_allowed"); + + b.Property("LastModified") + .HasColumnType("datetime(6)") + .HasColumnName("last_modified"); + + b.Property("PermissionKey") + .IsRequired() + .HasColumnType("varchar(255)") + .HasColumnName("permission_key"); + + b.Property("RoleId") + .HasColumnType("int") + .HasColumnName("role_id"); + + b.HasKey("RolePermissionId") + .HasName("pk_role_permission"); + + b.HasIndex("RoleId", "PermissionKey") + .IsUnique() + .HasDatabaseName("ix_role_permission_role_id_permission_key"); + + b.ToTable("role_permission"); + }); + + modelBuilder.Entity("Store.Models.Entities.Salary", b => + { + b.Property("SalaryId") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("salary_id"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("SalaryId")); + + b.Property("AllowanceAmount") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)") + .HasColumnName("allowance_amount"); + + b.Property("BasicAmount") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)") + .HasColumnName("basic_amount"); + + b.Property("DateCreated") + .HasColumnType("datetime(6)") + .HasColumnName("date_created"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("varchar(500)") + .HasColumnName("description"); + + b.Property("Grade") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)") + .HasColumnName("grade"); + + b.Property("LastModified") + .HasColumnType("datetime(6)") + .HasColumnName("last_modified"); + + b.HasKey("SalaryId") + .HasName("pk_salary"); + + b.ToTable("salary"); + }); + + modelBuilder.Entity("Store.Models.Entities.Sale", b => + { + b.Property("SaleId") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)") + .HasColumnName("sale_id"); + + b.Property("DateCreated") + .HasColumnType("datetime(6)") + .HasColumnName("date_created"); + + b.Property("DiscountAmount") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)") + .HasColumnName("discount_amount"); + + b.Property("InvoiceId") + .HasColumnType("char(36)") + .HasColumnName("invoice_id"); + + b.Property("ItemId") + .HasColumnType("char(36)") + .HasColumnName("item_id"); + + b.Property("ItemName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)") + .HasColumnName("item_name"); + + b.Property("LastModified") + .HasColumnType("datetime(6)") + .HasColumnName("last_modified"); + + b.Property("LineTotal") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)") + .HasColumnName("line_total"); + + b.Property("Quantity") + .HasColumnType("int") + .HasColumnName("quantity"); + + b.Property("UnitAbbreviation") + .HasMaxLength(20) + .HasColumnType("varchar(20)") + .HasColumnName("unit_abbreviation"); + + b.Property("UnitPrice") + .HasPrecision(18, 4) + .HasColumnType("decimal(18,4)") + .HasColumnName("unit_price"); + + b.Property("UserId") + .HasColumnType("char(36)") + .HasColumnName("user_id"); + + b.HasKey("SaleId") + .HasName("pk_sale"); + + b.HasIndex("InvoiceId") + .HasDatabaseName("ix_sale_invoice_id"); + + b.HasIndex("ItemId") + .HasDatabaseName("ix_sale_item_id"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_sale_user_id"); + + b.ToTable("sale"); + }); + + modelBuilder.Entity("Store.Models.Entities.StockMovement", b => + { + b.Property("StockMovementId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("stock_movement_id"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("StockMovementId")); + + b.Property("DateCreated") + .HasColumnType("datetime(6)") + .HasColumnName("date_created"); + + b.Property("InvoiceId") + .HasColumnType("char(36)") + .HasColumnName("invoice_id"); + + b.Property("ItemId") + .HasColumnType("char(36)") + .HasColumnName("item_id"); + + b.Property("ItemsOrderId") + .HasColumnType("char(36)") + .HasColumnName("items_order_id"); + + b.Property("LastModified") + .HasColumnType("datetime(6)") + .HasColumnName("last_modified"); + + b.Property("MovementType") + .HasColumnType("int") + .HasColumnName("movement_type"); + + b.Property("PerformedByUserId") + .HasColumnType("char(36)") + .HasColumnName("performed_by_user_id"); + + b.Property("QuantityDelta") + .HasColumnType("int") + .HasColumnName("quantity_delta"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("longtext") + .HasColumnName("reason"); + + b.Property("ReferenceCode") + .HasColumnType("longtext") + .HasColumnName("reference_code"); + + b.Property("StockAfter") + .HasColumnType("int") + .HasColumnName("stock_after"); + + b.Property("StockBefore") + .HasColumnType("int") + .HasColumnName("stock_before"); + + b.Property("UnitCost") + .HasColumnType("decimal(65,30)") + .HasColumnName("unit_cost"); + + b.Property("UnitPrice") + .HasColumnType("decimal(65,30)") + .HasColumnName("unit_price"); + + b.HasKey("StockMovementId") + .HasName("pk_stock_movement"); + + b.HasIndex("InvoiceId") + .HasDatabaseName("ix_stock_movement_invoice_id"); + + b.HasIndex("ItemsOrderId") + .HasDatabaseName("ix_stock_movement_items_order_id"); + + b.HasIndex("PerformedByUserId") + .HasDatabaseName("ix_stock_movement_performed_by_user_id"); + + b.HasIndex("ItemId", "DateCreated") + .HasDatabaseName("ix_stock_movement_item_id_date_created"); + + b.ToTable("stock_movement"); + }); + + modelBuilder.Entity("Store.Models.Entities.StockTransfer", b => + { + b.Property("StockTransferId") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("stock_transfer_id"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("StockTransferId")); + + b.Property("ApprovedAt") + .HasColumnType("datetime(6)") + .HasColumnName("approved_at"); + + b.Property("ApprovedByUserId") + .HasColumnType("char(36)") + .HasColumnName("approved_by_user_id"); + + b.Property("DateCreated") + .HasColumnType("datetime(6)") + .HasColumnName("date_created"); + + b.Property("DispatchedAt") + .HasColumnType("datetime(6)") + .HasColumnName("dispatched_at"); + + b.Property("DispatchedByUserId") + .HasColumnType("char(36)") + .HasColumnName("dispatched_by_user_id"); + + b.Property("FromBranchId") + .HasColumnType("int") + .HasColumnName("from_branch_id"); + + b.Property("LastModified") + .HasColumnType("datetime(6)") + .HasColumnName("last_modified"); + + b.Property("Notes") + .HasMaxLength(1000) + .HasColumnType("varchar(1000)") + .HasColumnName("notes"); + + b.Property("ReceivedAt") + .HasColumnType("datetime(6)") + .HasColumnName("received_at"); + + b.Property("ReceivedByUserId") + .HasColumnType("char(36)") + .HasColumnName("received_by_user_id"); + + b.Property("RejectionReason") + .HasMaxLength(1000) + .HasColumnType("varchar(1000)") + .HasColumnName("rejection_reason"); + + b.Property("RequestedByUserId") + .HasColumnType("char(36)") + .HasColumnName("requested_by_user_id"); + + b.Property("Status") + .HasColumnType("int") + .HasColumnName("status"); + + b.Property("ToBranchId") + .HasColumnType("int") + .HasColumnName("to_branch_id"); + + b.HasKey("StockTransferId") + .HasName("pk_stock_transfer"); + + b.HasIndex("FromBranchId") + .HasDatabaseName("ix_stock_transfer_from_branch_id"); + + b.HasIndex("RequestedByUserId") + .HasDatabaseName("ix_stock_transfer_requested_by_user_id"); + + b.HasIndex("ToBranchId") + .HasDatabaseName("ix_stock_transfer_to_branch_id"); + + b.HasIndex("Status", "DateCreated") + .HasDatabaseName("ix_stock_transfer_status_date_created"); + + b.ToTable("stock_transfer"); + }); + + modelBuilder.Entity("Store.Models.Entities.StockTransferItem", b => + { + b.Property("StockTransferItemId") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("stock_transfer_item_id"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("StockTransferItemId")); + + b.Property("DateCreated") + .HasColumnType("datetime(6)") + .HasColumnName("date_created"); + + b.Property("DispatchedQuantity") + .HasColumnType("int") + .HasColumnName("dispatched_quantity"); + + b.Property("ItemId") + .HasColumnType("char(36)") + .HasColumnName("item_id"); + + b.Property("LastModified") + .HasColumnType("datetime(6)") + .HasColumnName("last_modified"); + + b.Property("Notes") + .HasMaxLength(500) + .HasColumnType("varchar(500)") + .HasColumnName("notes"); + + b.Property("ReceivedQuantity") + .HasColumnType("int") + .HasColumnName("received_quantity"); + + b.Property("RequestedQuantity") + .HasColumnType("int") + .HasColumnName("requested_quantity"); + + b.Property("StockTransferId") + .HasColumnType("int") + .HasColumnName("stock_transfer_id"); + + b.HasKey("StockTransferItemId") + .HasName("pk_stock_transfer_item"); + + b.HasIndex("ItemId") + .HasDatabaseName("ix_stock_transfer_item_item_id"); + + b.HasIndex("StockTransferId") + .HasDatabaseName("ix_stock_transfer_item_stock_transfer_id"); + + b.ToTable("stock_transfer_item"); + }); + + modelBuilder.Entity("Store.Models.Entities.Supplier", b => + { + b.Property("SupplierId") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)") + .HasColumnName("supplier_id"); + + b.Property("DateCreated") + .HasColumnType("datetime(6)") + .HasColumnName("date_created"); + + b.Property("FullImageUrl") + .HasColumnType("longtext") + .HasColumnName("full_image_url"); + + b.Property("LastModified") + .HasColumnType("datetime(6)") + .HasColumnName("last_modified"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)") + .HasColumnName("name"); + + b.Property("Notes") + .HasMaxLength(1000) + .HasColumnType("varchar(1000)") + .HasColumnName("notes"); + + b.Property("RegistrationNumber") + .HasMaxLength(100) + .HasColumnType("varchar(100)") + .HasColumnName("registration_number"); + + b.Property("ThumbnailUrl") + .HasMaxLength(500) + .HasColumnType("varchar(500)") + .HasColumnName("thumbnail_url"); + + b.HasKey("SupplierId") + .HasName("pk_supplier"); + + b.ToTable("supplier"); + }); + + modelBuilder.Entity("Store.Models.Entities.SupplierLocation", b => + { + b.Property("SupplierLocationId") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("supplier_location_id"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("SupplierLocationId")); + + b.Property("DateCreated") + .HasColumnType("datetime(6)") + .HasColumnName("date_created"); + + b.Property("IsPrimary") + .HasColumnType("tinyint(1)") + .HasColumnName("is_primary"); + + b.Property("LastModified") + .HasColumnType("datetime(6)") + .HasColumnName("last_modified"); + + b.Property("LocationId") + .HasColumnType("int") + .HasColumnName("location_id"); + + b.Property("SupplierId") + .HasColumnType("char(36)") + .HasColumnName("supplier_id"); + + b.HasKey("SupplierLocationId") + .HasName("pk_supplier_location"); + + b.HasIndex("LocationId") + .HasDatabaseName("ix_supplier_location_location_id"); + + b.HasIndex("SupplierId") + .HasDatabaseName("ix_supplier_location_supplier_id"); + + b.ToTable("supplier_location"); + }); + + modelBuilder.Entity("Store.Models.Entities.SystemSetting", b => + { + b.Property("SettingKey") + .HasMaxLength(128) + .HasColumnType("varchar(128)") + .HasColumnName("setting_key"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("varchar(256)") + .HasColumnName("description"); + + b.Property("LastModified") + .HasColumnType("datetime(6)") + .HasColumnName("last_modified"); + + b.Property("SettingValue") + .HasColumnType("longtext") + .HasColumnName("setting_value"); + + b.HasKey("SettingKey") + .HasName("pk_system_setting"); + + b.ToTable("system_setting"); + + b.HasData( + new + { + SettingKey = "Auth:PasswordRecoveryMethod", + Description = "Determines allowed password recovery methods (OTP, TempPassword, Both)", + LastModified = new DateTime(2026, 8, 17, 13, 9, 45, 172, DateTimeKind.Utc).AddTicks(9277), + SettingValue = "Both" + }); + }); + + modelBuilder.Entity("Store.Models.Entities.TaxProfile", b => + { + b.Property("TaxProfileId") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("tax_profile_id"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("TaxProfileId")); + + b.Property("ApplicationType") + .HasColumnType("int") + .HasColumnName("application_type"); + + b.Property("DateCreated") + .HasColumnType("datetime(6)") + .HasColumnName("date_created"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)") + .HasColumnName("is_active"); + + b.Property("LastModified") + .HasColumnType("datetime(6)") + .HasColumnName("last_modified"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext") + .HasColumnName("name"); + + b.Property("RatePercent") + .HasColumnType("decimal(65,30)") + .HasColumnName("rate_percent"); + + b.HasKey("TaxProfileId") + .HasName("pk_tax_profile"); + + b.ToTable("tax_profile"); + }); + + modelBuilder.Entity("Store.Models.Entities.Unit", b => + { + b.Property("UnitId") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("unit_id"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("UnitId")); + + b.Property("Abbreviation") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("varchar(20)") + .HasColumnName("abbreviation"); + + b.Property("DateCreated") + .HasColumnType("datetime(6)") + .HasColumnName("date_created"); + + b.Property("Description") + .HasMaxLength(300) + .HasColumnType("varchar(300)") + .HasColumnName("description"); + + b.Property("LastModified") + .HasColumnType("datetime(6)") + .HasColumnName("last_modified"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)") + .HasColumnName("name"); + + b.HasKey("UnitId") + .HasName("pk_unit"); + + b.HasIndex("Abbreviation") + .IsUnique() + .HasDatabaseName("ix_unit_abbreviation"); + + b.ToTable("unit"); + }); + + modelBuilder.Entity("Store.Models.Entities.User", b => + { + b.Property("UserId") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)") + .HasColumnName("user_id"); + + b.Property("DateCreated") + .HasColumnType("datetime(6)") + .HasColumnName("date_created"); + + b.Property("EmployeeId") + .HasColumnType("char(36)") + .HasColumnName("employee_id"); + + b.Property("FailedLoginAttempts") + .HasColumnType("int") + .HasColumnName("failed_login_attempts"); + + b.Property("FullImageUrl") + .HasColumnType("longtext") + .HasColumnName("full_image_url"); + + b.Property("LastModified") + .HasColumnType("datetime(6)") + .HasColumnName("last_modified"); + + b.Property("LockoutEnd") + .HasColumnType("datetime(6)") + .HasColumnName("lockout_end"); + + b.Property("RoleId") + .HasColumnType("int") + .HasColumnName("role_id"); + + b.Property("SecurityStamp") + .HasColumnType("char(36)") + .HasColumnName("security_stamp"); + + b.Property("Status") + .IsRequired() + .ValueGeneratedOnAdd() + .HasMaxLength(50) + .HasColumnType("varchar(50)") + .HasDefaultValue("NotVerified") + .HasColumnName("status"); + + b.Property("ThumbnailUrl") + .HasMaxLength(500) + .HasColumnType("varchar(500)") + .HasColumnName("thumbnail_url"); + + b.Property("TwoFactorEnabled") + .HasColumnType("tinyint(1)") + .HasColumnName("two_factor_enabled"); + + b.Property("TwoFactorSecret") + .HasColumnType("longtext") + .HasColumnName("two_factor_secret"); + + b.Property("Username") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)") + .HasColumnName("username"); + + b.HasKey("UserId") + .HasName("pk_user"); + + b.HasIndex("EmployeeId") + .HasDatabaseName("ix_user_employee_id"); + + b.HasIndex("RoleId") + .HasDatabaseName("ix_user_role_id"); + + b.HasIndex("Username") + .IsUnique() + .HasDatabaseName("ix_user_username"); + + b.ToTable("user"); + }); + + modelBuilder.Entity("Store.Models.Entities.UserBranchRole", b => + { + b.Property("UserBranchRoleId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("user_branch_role_id"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("UserBranchRoleId")); + + b.Property("BranchId") + .HasColumnType("int") + .HasColumnName("branch_id"); + + b.Property("DateCreated") + .HasColumnType("datetime(6)") + .HasColumnName("date_created"); + + b.Property("LastModified") + .HasColumnType("datetime(6)") + .HasColumnName("last_modified"); + + b.Property("RoleId") + .HasColumnType("int") + .HasColumnName("role_id"); + + b.Property("UserId") + .HasColumnType("char(36)") + .HasColumnName("user_id"); + + b.HasKey("UserBranchRoleId") + .HasName("pk_user_branch_role"); + + b.HasIndex("BranchId") + .HasDatabaseName("ix_user_branch_role_branch_id"); + + b.HasIndex("RoleId") + .HasDatabaseName("ix_user_branch_role_role_id"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_user_branch_role_user_id"); + + b.ToTable("user_branch_role"); + }); + + modelBuilder.Entity("Store.Models.Entities.UserPassword", b => + { + b.Property("UserPasswordId") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("user_password_id"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("UserPasswordId")); + + b.Property("DateCreated") + .HasColumnType("datetime(6)") + .HasColumnName("date_created"); + + b.Property("ForcePasswordChange") + .HasColumnType("tinyint(1)") + .HasColumnName("force_password_change"); + + b.Property("LastModified") + .HasColumnType("datetime(6)") + .HasColumnName("last_modified"); + + b.Property("PasswordHash") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("varchar(256)") + .HasColumnName("password_hash"); + + b.Property("TempPasswordExpiresAt") + .HasColumnType("datetime(6)") + .HasColumnName("temp_password_expires_at"); + + b.Property("UserId") + .HasColumnType("char(36)") + .HasColumnName("user_id"); + + b.HasKey("UserPasswordId") + .HasName("pk_user_password"); + + b.HasIndex("UserId") + .IsUnique() + .HasDatabaseName("ix_user_password_user_id"); + + b.ToTable("user_password"); + }); + + modelBuilder.Entity("Store.Models.Entities.UserPrivilege", b => + { + b.Property("UserPrivilegeId") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("user_privilege_id"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("UserPrivilegeId")); + + b.Property("DateCreated") + .HasColumnType("datetime(6)") + .HasColumnName("date_created"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)") + .HasColumnName("is_active"); + + b.Property("LastModified") + .HasColumnType("datetime(6)") + .HasColumnName("last_modified"); + + b.Property("PrivilegeId") + .HasColumnType("int") + .HasColumnName("privilege_id"); + + b.Property("Type") + .HasColumnType("int") + .HasColumnName("type"); + + b.Property("UserId") + .HasColumnType("char(36)") + .HasColumnName("user_id"); + + b.HasKey("UserPrivilegeId") + .HasName("pk_user_privilege"); + + b.HasIndex("PrivilegeId") + .HasDatabaseName("ix_user_privilege_privilege_id"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_user_privilege_user_id"); + + b.ToTable("user_privilege"); + }); + + modelBuilder.Entity("Store.Models.Entities.UserPrivilegeAction", b => + { + b.Property("UserPrivilegeActionId") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("user_privilege_action_id"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("UserPrivilegeActionId")); + + b.Property("Action") + .IsRequired() + .HasColumnType("longtext") + .HasColumnName("action"); + + b.Property("DateCreated") + .HasColumnType("datetime(6)") + .HasColumnName("date_created"); + + b.Property("LastModified") + .HasColumnType("datetime(6)") + .HasColumnName("last_modified"); + + b.Property("Notes") + .HasColumnType("longtext") + .HasColumnName("notes"); + + b.Property("PerformedByUserId") + .HasColumnType("char(36)") + .HasColumnName("performed_by_user_id"); + + b.Property("UserPrivilegeId") + .HasColumnType("int") + .HasColumnName("user_privilege_id"); + + b.HasKey("UserPrivilegeActionId") + .HasName("pk_user_privilege_action"); + + b.HasIndex("PerformedByUserId") + .HasDatabaseName("ix_user_privilege_action_performed_by_user_id"); + + b.HasIndex("UserPrivilegeId") + .HasDatabaseName("ix_user_privilege_action_user_privilege_id"); + + b.ToTable("user_privilege_action"); + }); + + modelBuilder.Entity("Store.Models.Entities.UserToken", b => + { + b.Property("UserTokenId") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("user_token_id"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("UserTokenId")); + + b.Property("DateCreated") + .HasColumnType("datetime(6)") + .HasColumnName("date_created"); + + b.Property("DeviceName") + .HasMaxLength(100) + .HasColumnType("varchar(100)") + .HasColumnName("device_name"); + + b.Property("ExpiryDate") + .HasColumnType("datetime(6)") + .HasColumnName("expiry_date"); + + b.Property("IpAddress") + .HasMaxLength(45) + .HasColumnType("varchar(45)") + .HasColumnName("ip_address"); + + b.Property("IsRevoked") + .HasColumnType("tinyint(1)") + .HasColumnName("is_revoked"); + + b.Property("LastActive") + .HasColumnType("datetime(6)") + .HasColumnName("last_active"); + + b.Property("LastModified") + .HasColumnType("datetime(6)") + .HasColumnName("last_modified"); + + b.Property("RefreshTokenExpiryDate") + .HasColumnType("datetime(6)") + .HasColumnName("refresh_token_expiry_date"); + + b.Property("RefreshTokenHash") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("varchar(256)") + .HasColumnName("refresh_token_hash"); + + b.Property("Token") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("varchar(2000)") + .HasColumnName("token"); + + b.Property("UserAgent") + .HasMaxLength(500) + .HasColumnType("varchar(500)") + .HasColumnName("user_agent"); + + b.Property("UserId") + .HasColumnType("char(36)") + .HasColumnName("user_id"); + + b.HasKey("UserTokenId") + .HasName("pk_user_token"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_user_token_user_id"); + + b.ToTable("user_token"); + }); + + modelBuilder.Entity("Store.Models.Entities.WastageEntry", b => + { + b.Property("WastageEntryId") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("wastage_entry_id"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("WastageEntryId")); + + b.Property("DateCreated") + .HasColumnType("datetime(6)") + .HasColumnName("date_created"); + + b.Property("ItemId") + .HasColumnType("char(36)") + .HasColumnName("item_id"); + + b.Property("LastModified") + .HasColumnType("datetime(6)") + .HasColumnName("last_modified"); + + b.Property("Notes") + .HasMaxLength(1000) + .HasColumnType("varchar(1000)") + .HasColumnName("notes"); + + b.Property("Quantity") + .HasColumnType("int") + .HasColumnName("quantity"); + + b.Property("RecordedByUserId") + .HasColumnType("char(36)") + .HasColumnName("recorded_by_user_id"); + + b.Property("ReferenceCode") + .HasMaxLength(100) + .HasColumnType("varchar(100)") + .HasColumnName("reference_code"); + + b.Property("WastageType") + .HasColumnType("int") + .HasColumnName("wastage_type"); + + b.HasKey("WastageEntryId") + .HasName("pk_wastage_entry"); + + b.HasIndex("RecordedByUserId") + .HasDatabaseName("ix_wastage_entry_recorded_by_user_id"); + + b.HasIndex("ItemId", "DateCreated") + .HasDatabaseName("ix_wastage_entry_item_id_date_created"); + + b.ToTable("wastage_entry"); + }); + + modelBuilder.Entity("Store.Models.Entities.AuditLog", b => + { + b.HasOne("Store.Models.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_audit_log_users_user_id"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Store.Models.Entities.Batch", b => + { + b.HasOne("Store.Models.Entities.Item", "Item") + .WithMany("Batches") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_batch_items_item_id"); + + b.Navigation("Item"); + }); + + modelBuilder.Entity("Store.Models.Entities.BundleRule", b => + { + b.HasOne("Store.Models.Entities.Item", "RewardItem") + .WithMany() + .HasForeignKey("RewardItemId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_bundle_rule_items_reward_item_id"); + + b.HasOne("Store.Models.Entities.Item", "TriggerItem") + .WithMany() + .HasForeignKey("TriggerItemId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_bundle_rule_items_trigger_item_id"); + + b.Navigation("RewardItem"); + + b.Navigation("TriggerItem"); + }); + + modelBuilder.Entity("Store.Models.Entities.CashVarianceRecord", b => + { + b.HasOne("Store.Models.Entities.CashierShift", "CashierShift") + .WithMany() + .HasForeignKey("CashierShiftId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_cash_variance_record_cashier_shifts_cashier_shift_id"); + + b.HasOne("Store.Models.Entities.User", "RecordedByUser") + .WithMany() + .HasForeignKey("RecordedByUserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_cash_variance_record_users_recorded_by_user_id"); + + b.HasOne("Store.Models.Entities.User", "ReviewedByUser") + .WithMany() + .HasForeignKey("ReviewedByUserId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_cash_variance_record_users_reviewed_by_user_id"); + + b.Navigation("CashierShift"); + + b.Navigation("RecordedByUser"); + + b.Navigation("ReviewedByUser"); + }); + + modelBuilder.Entity("Store.Models.Entities.CashierShift", b => + { + b.HasOne("Store.Models.Entities.User", "ClosedByUser") + .WithMany("ClosedShifts") + .HasForeignKey("ClosedByUserId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_cashier_shift_users_closed_by_user_id"); + + b.HasOne("Store.Models.Entities.User", "OpenedByUser") + .WithMany("OpenedShifts") + .HasForeignKey("OpenedByUserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_cashier_shift_users_opened_by_user_id"); + + b.Navigation("ClosedByUser"); + + b.Navigation("OpenedByUser"); + }); + + modelBuilder.Entity("Store.Models.Entities.ChangeLog", b => + { + b.HasOne("Store.Models.Entities.User", "User") + .WithMany("ChangeLogs") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_change_log_users_user_id"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Store.Models.Entities.City", b => + { + b.HasOne("Store.Models.Entities.Region", "Region") + .WithMany("Cities") + .HasForeignKey("RegionId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_city_regions_region_id"); + + b.Navigation("Region"); + }); + + modelBuilder.Entity("Store.Models.Entities.Contacts.ContactChangeRequest", b => + { + b.HasOne("Store.Models.Entities.User", "ApprovedBy") + .WithMany("ApprovedContactChanges") + .HasForeignKey("ApprovedById") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_contact_change_request_users_approved_by_id"); + + b.HasOne("Store.Models.Entities.User", "User") + .WithMany("ContactChangeRequests") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_contact_change_request_users_user_id"); + + b.Navigation("ApprovedBy"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Store.Models.Entities.Contacts.CustomerEmail", b => + { + b.HasOne("Store.Models.Entities.Customer", "Customer") + .WithMany("Emails") + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_customer_email_customers_customer_id"); + + b.HasOne("Store.Models.Entities.Contacts.Email", "Email") + .WithMany("CustomerEmails") + .HasForeignKey("EmailId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_customer_email_emails_email_id"); + + b.Navigation("Customer"); + + b.Navigation("Email"); + }); + + modelBuilder.Entity("Store.Models.Entities.Contacts.CustomerPhone", b => + { + b.HasOne("Store.Models.Entities.Customer", "Customer") + .WithMany("Phones") + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_customer_phone_customers_customer_id"); + + b.HasOne("Store.Models.Entities.Contacts.Phone", "Phone") + .WithMany("CustomerPhones") + .HasForeignKey("PhoneId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_customer_phone_phones_phone_id"); + + b.Navigation("Customer"); + + b.Navigation("Phone"); + }); + + modelBuilder.Entity("Store.Models.Entities.Contacts.EmployeeEmail", b => + { + b.HasOne("Store.Models.Entities.Contacts.Email", "Email") + .WithMany("EmployeeEmails") + .HasForeignKey("EmailId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_employee_email_email_email_id"); + + b.HasOne("Store.Models.Entities.Employee", "Employee") + .WithMany("Emails") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_employee_email_employees_employee_id"); + + b.Navigation("Email"); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("Store.Models.Entities.Contacts.EmployeePhone", b => + { + b.HasOne("Store.Models.Entities.Employee", "Employee") + .WithMany("Phones") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_employee_phone_employees_employee_id"); + + b.HasOne("Store.Models.Entities.Contacts.Phone", "Phone") + .WithMany("EmployeePhones") + .HasForeignKey("PhoneId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_employee_phone_phones_phone_id"); + + b.Navigation("Employee"); + + b.Navigation("Phone"); + }); + + modelBuilder.Entity("Store.Models.Entities.Contacts.ManufacturerEmail", b => + { + b.HasOne("Store.Models.Entities.Contacts.Email", "Email") + .WithMany("ManufacturerEmails") + .HasForeignKey("EmailId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_manufacturer_email_email_email_id"); + + b.HasOne("Store.Models.Entities.Manufacturer", "Manufacturer") + .WithMany("Emails") + .HasForeignKey("ManufacturerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_manufacturer_email_manufacturers_manufacturer_id"); + + b.Navigation("Email"); + + b.Navigation("Manufacturer"); + }); + + modelBuilder.Entity("Store.Models.Entities.Contacts.ManufacturerPhone", b => + { + b.HasOne("Store.Models.Entities.Manufacturer", "Manufacturer") + .WithMany("Phones") + .HasForeignKey("ManufacturerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_manufacturer_phone_manufacturers_manufacturer_id"); + + b.HasOne("Store.Models.Entities.Contacts.Phone", "Phone") + .WithMany("ManufacturerPhones") + .HasForeignKey("PhoneId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_manufacturer_phone_phones_phone_id"); + + b.Navigation("Manufacturer"); + + b.Navigation("Phone"); + }); + + modelBuilder.Entity("Store.Models.Entities.Contacts.SupplierEmail", b => + { + b.HasOne("Store.Models.Entities.Contacts.Email", "Email") + .WithMany("SupplierEmails") + .HasForeignKey("EmailId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_supplier_email_email_email_id"); + + b.HasOne("Store.Models.Entities.Supplier", "Supplier") + .WithMany("Emails") + .HasForeignKey("SupplierId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_supplier_email_suppliers_supplier_id"); + + b.Navigation("Email"); + + b.Navigation("Supplier"); + }); + + modelBuilder.Entity("Store.Models.Entities.Contacts.SupplierPhone", b => + { + b.HasOne("Store.Models.Entities.Contacts.Phone", "Phone") + .WithMany("SupplierPhones") + .HasForeignKey("PhoneId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_supplier_phone_phone_phone_id"); + + b.HasOne("Store.Models.Entities.Supplier", "Supplier") + .WithMany("Phones") + .HasForeignKey("SupplierId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_supplier_phone_suppliers_supplier_id"); + + b.Navigation("Phone"); + + b.Navigation("Supplier"); + }); + + modelBuilder.Entity("Store.Models.Entities.Contacts.UserEmail", b => + { + b.HasOne("Store.Models.Entities.Contacts.Email", "Email") + .WithMany("UserEmails") + .HasForeignKey("EmailId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_user_email_email_email_id"); + + b.HasOne("Store.Models.Entities.User", "User") + .WithMany("Emails") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_user_email_users_user_id"); + + b.Navigation("Email"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Store.Models.Entities.Contacts.UserPhone", b => + { + b.HasOne("Store.Models.Entities.Contacts.Phone", "Phone") + .WithMany("UserPhones") + .HasForeignKey("PhoneId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_user_phone_phone_phone_id"); + + b.HasOne("Store.Models.Entities.User", "User") + .WithMany("Phones") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_user_phone_users_user_id"); + + b.Navigation("Phone"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Store.Models.Entities.CustomerLocation", b => + { + b.HasOne("Store.Models.Entities.Customer", "Customer") + .WithMany("Locations") + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_customer_location_customer_customer_id"); + + b.HasOne("Store.Models.Entities.Location", "Location") + .WithMany("CustomerLocations") + .HasForeignKey("LocationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_customer_location_locations_location_id"); + + b.Navigation("Customer"); + + b.Navigation("Location"); + }); + + modelBuilder.Entity("Store.Models.Entities.CustomerLoyaltyAccount", b => + { + b.HasOne("Store.Models.Entities.Customer", "Customer") + .WithOne("LoyaltyAccount") + .HasForeignKey("Store.Models.Entities.CustomerLoyaltyAccount", "CustomerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_customer_loyalty_account_customer_customer_id"); + + b.Navigation("Customer"); + }); + + modelBuilder.Entity("Store.Models.Entities.CustomerPrivilege", b => + { + b.HasOne("Store.Models.Entities.Customer", "Customer") + .WithMany("Privileges") + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_customer_privilege_customer_customer_id"); + + b.HasOne("Store.Models.Entities.Privilege", "Privilege") + .WithMany("CustomerPrivileges") + .HasForeignKey("PrivilegeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_customer_privilege_privileges_privilege_id"); + + b.Navigation("Customer"); + + b.Navigation("Privilege"); + }); + + modelBuilder.Entity("Store.Models.Entities.CustomerPrivilegeAction", b => + { + b.HasOne("Store.Models.Entities.CustomerPrivilege", "CustomerPrivilege") + .WithMany("Actions") + .HasForeignKey("CustomerPrivilegeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_customer_privilege_action_customer_privilege_customer_privil~"); + + b.HasOne("Store.Models.Entities.User", "PerformedByUser") + .WithMany("CustomerPrivilegeActions") + .HasForeignKey("PerformedByUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_customer_privilege_action_users_performed_by_user_id"); + + b.Navigation("CustomerPrivilege"); + + b.Navigation("PerformedByUser"); + }); + + modelBuilder.Entity("Store.Models.Entities.CustomerSegmentPrice", b => + { + b.HasOne("Store.Models.Entities.Item", "Item") + .WithMany("SegmentPrices") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_customer_segment_price_items_item_id"); + + b.Navigation("Item"); + }); + + modelBuilder.Entity("Store.Models.Entities.Discount", b => + { + b.HasOne("Store.Models.Entities.Category", "Category") + .WithMany() + .HasForeignKey("CategoryId") + .HasConstraintName("fk_discount_category_category_id"); + + b.HasOne("Store.Models.Entities.Item", "Item") + .WithOne("Discount") + .HasForeignKey("Store.Models.Entities.Discount", "ItemId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_discount_items_item_id"); + + b.HasOne("Store.Models.Entities.User", "ManagedByUser") + .WithMany("ManagedDiscounts") + .HasForeignKey("ManagedByUserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_discount_users_managed_by_user_id"); + + b.Navigation("Category"); + + b.Navigation("Item"); + + b.Navigation("ManagedByUser"); + }); + + modelBuilder.Entity("Store.Models.Entities.DiscountOverrideRequest", b => + { + b.HasOne("Store.Models.Entities.Invoice", "Invoice") + .WithMany() + .HasForeignKey("InvoiceId") + .HasConstraintName("fk_discount_override_request_invoices_invoice_id"); + + b.HasOne("Store.Models.Entities.Item", "Item") + .WithMany() + .HasForeignKey("ItemId") + .HasConstraintName("fk_discount_override_request_items_item_id"); + + b.HasOne("Store.Models.Entities.User", "RequestedByUser") + .WithMany() + .HasForeignKey("RequestedByUserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_discount_override_request_users_requested_by_user_id"); + + b.HasOne("Store.Models.Entities.User", "ReviewedByUser") + .WithMany() + .HasForeignKey("ReviewedByUserId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_discount_override_request_users_reviewed_by_user_id"); + + b.Navigation("Invoice"); + + b.Navigation("Item"); + + b.Navigation("RequestedByUser"); + + b.Navigation("ReviewedByUser"); + }); + + modelBuilder.Entity("Store.Models.Entities.Employee", b => + { + b.HasOne("Store.Models.Entities.Department", "Department") + .WithMany("Employees") + .HasForeignKey("DepartmentId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_employee_department_department_id"); + + b.HasOne("Store.Models.Entities.Salary", "Salary") + .WithMany("Employees") + .HasForeignKey("SalaryId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_employee_salaries_salary_id"); + + b.Navigation("Department"); + + b.Navigation("Salary"); + }); + + modelBuilder.Entity("Store.Models.Entities.EmployeeLocation", b => + { + b.HasOne("Store.Models.Entities.Employee", "Employee") + .WithMany("Locations") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_employee_location_employee_employee_id"); + + b.HasOne("Store.Models.Entities.Location", "Location") + .WithMany("EmployeeLocations") + .HasForeignKey("LocationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_employee_location_locations_location_id"); + + b.Navigation("Employee"); + + b.Navigation("Location"); + }); + + modelBuilder.Entity("Store.Models.Entities.EmployeePrivilege", b => + { + b.HasOne("Store.Models.Entities.Employee", "Employee") + .WithMany("Privileges") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_employee_privilege_employee_employee_id"); + + b.HasOne("Store.Models.Entities.Privilege", "Privilege") + .WithMany("EmployeePrivileges") + .HasForeignKey("PrivilegeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_employee_privilege_privileges_privilege_id"); + + b.Navigation("Employee"); + + b.Navigation("Privilege"); + }); + + modelBuilder.Entity("Store.Models.Entities.EmployeePrivilegeAction", b => + { + b.HasOne("Store.Models.Entities.EmployeePrivilege", "EmployeePrivilege") + .WithMany("Actions") + .HasForeignKey("EmployeePrivilegeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_employee_privilege_action_employee_privilege_employee_privil~"); + + b.HasOne("Store.Models.Entities.User", "PerformedByUser") + .WithMany("EmployeePrivilegeActions") + .HasForeignKey("PerformedByUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_employee_privilege_action_users_performed_by_user_id"); + + b.Navigation("EmployeePrivilege"); + + b.Navigation("PerformedByUser"); + }); + + modelBuilder.Entity("Store.Models.Entities.FidoCredential", b => + { + b.HasOne("Store.Models.Entities.User", "User") + .WithMany("FidoCredentials") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_fido_credential_users_user_id"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Store.Models.Entities.Invoice", b => + { + b.HasOne("Store.Models.Entities.Branch", "Branch") + .WithMany("Invoices") + .HasForeignKey("BranchId") + .HasConstraintName("fk_invoice_branch_branch_id"); + + b.HasOne("Store.Models.Entities.Customer", "Customer") + .WithMany("Invoices") + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_invoice_customer_customer_id"); + + b.HasOne("Store.Models.Entities.User", "User") + .WithMany("Invoices") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_invoice_users_user_id"); + + b.Navigation("Branch"); + + b.Navigation("Customer"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Store.Models.Entities.InvoiceTender", b => + { + b.HasOne("Store.Models.Entities.Invoice", "Invoice") + .WithMany("Tenders") + .HasForeignKey("InvoiceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_invoice_tender_invoice_invoice_id"); + + b.Navigation("Invoice"); + }); + + modelBuilder.Entity("Store.Models.Entities.Item", b => + { + b.HasOne("Store.Models.Entities.Category", "Category") + .WithMany("Items") + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_item_category_category_id"); + + b.HasOne("Store.Models.Entities.Manufacturer", "Manufacturer") + .WithMany("Items") + .HasForeignKey("ManufacturerId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_item_manufacturers_manufacturer_id"); + + b.HasOne("Store.Models.Entities.TaxProfile", "TaxProfile") + .WithMany("Items") + .HasForeignKey("TaxProfileId") + .HasConstraintName("fk_item_tax_profiles_tax_profile_id"); + + b.HasOne("Store.Models.Entities.Unit", "Unit") + .WithMany("Items") + .HasForeignKey("UnitId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_item_units_unit_id"); + + b.Navigation("Category"); + + b.Navigation("Manufacturer"); + + b.Navigation("TaxProfile"); + + b.Navigation("Unit"); + }); + + modelBuilder.Entity("Store.Models.Entities.ItemCategory", b => + { + b.HasOne("Store.Models.Entities.Category", "Category") + .WithMany("ItemCategories") + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_item_category_category_category_id"); + + b.HasOne("Store.Models.Entities.Item", "Item") + .WithMany("ItemCategories") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_item_category_item_item_id"); + + b.Navigation("Category"); + + b.Navigation("Item"); + }); + + modelBuilder.Entity("Store.Models.Entities.ItemCode", b => + { + b.HasOne("Store.Models.Entities.Item", "Item") + .WithMany("ItemCodes") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_item_code_item_item_id"); + + b.Navigation("Item"); + }); + + modelBuilder.Entity("Store.Models.Entities.ItemExpiry", b => + { + b.HasOne("Store.Models.Entities.Item", "Item") + .WithOne("ItemExpiry") + .HasForeignKey("Store.Models.Entities.ItemExpiry", "ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_item_expiry_item_item_id"); + + b.Navigation("Item"); + }); + + modelBuilder.Entity("Store.Models.Entities.ItemsOrder", b => + { + b.HasOne("Store.Models.Entities.User", "CreatedByUser") + .WithMany("Orders") + .HasForeignKey("CreatedByUserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_items_order_users_created_by_user_id"); + + b.HasOne("Store.Models.Entities.Supplier", "Supplier") + .WithMany("Orders") + .HasForeignKey("SupplierId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_items_order_suppliers_supplier_id"); + + b.Navigation("CreatedByUser"); + + b.Navigation("Supplier"); + }); + + modelBuilder.Entity("Store.Models.Entities.Location", b => + { + b.HasOne("Store.Models.Entities.City", "City") + .WithMany("Locations") + .HasForeignKey("CityId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_location_city_city_id"); + + b.Navigation("City"); + }); + + modelBuilder.Entity("Store.Models.Entities.LoyaltyTransaction", b => + { + b.HasOne("Store.Models.Entities.Invoice", "Invoice") + .WithMany("LoyaltyTransactions") + .HasForeignKey("InvoiceId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_loyalty_transaction_invoice_invoice_id"); + + b.HasOne("Store.Models.Entities.CustomerLoyaltyAccount", "LoyaltyAccount") + .WithMany("Transactions") + .HasForeignKey("LoyaltyAccountId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_loyalty_transaction_customer_loyalty_account_loyalty_account~"); + + b.Navigation("Invoice"); + + b.Navigation("LoyaltyAccount"); + }); + + modelBuilder.Entity("Store.Models.Entities.ManufacturerLocation", b => + { + b.HasOne("Store.Models.Entities.Location", "Location") + .WithMany("ManufacturerLocations") + .HasForeignKey("LocationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_manufacturer_location_location_location_id"); + + b.HasOne("Store.Models.Entities.Manufacturer", "Manufacturer") + .WithMany("Locations") + .HasForeignKey("ManufacturerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_manufacturer_location_manufacturer_manufacturer_id"); + + b.Navigation("Location"); + + b.Navigation("Manufacturer"); + }); + + modelBuilder.Entity("Store.Models.Entities.MobileMoneyTransaction", b => + { + b.HasOne("Store.Models.Entities.Invoice", "Invoice") + .WithMany() + .HasForeignKey("InvoiceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_mobile_money_transaction_invoice_invoice_id"); + + b.Navigation("Invoice"); + }); + + modelBuilder.Entity("Store.Models.Entities.Notification", b => + { + b.HasOne("Store.Models.Entities.User", "User") + .WithMany("Notifications") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_notification_users_user_id"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Store.Models.Entities.OrderItem", b => + { + b.HasOne("Store.Models.Entities.Item", "Item") + .WithMany("OrderItems") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_order_item_item_item_id"); + + b.HasOne("Store.Models.Entities.ItemsOrder", "ItemsOrder") + .WithMany("Items") + .HasForeignKey("ItemsOrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_order_item_items_order_items_order_id"); + + b.Navigation("Item"); + + b.Navigation("ItemsOrder"); + }); + + modelBuilder.Entity("Store.Models.Entities.Otp", b => + { + b.HasOne("Store.Models.Entities.User", "User") + .WithMany("Otps") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_otp_users_user_id"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Store.Models.Entities.PasswordResetToken", b => + { + b.HasOne("Store.Models.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_password_reset_token_users_user_id"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Store.Models.Entities.PurchaseOrder", b => + { + b.HasOne("Store.Models.Entities.User", "ApprovedByUser") + .WithMany() + .HasForeignKey("ApprovedByUserId") + .OnDelete(DeleteBehavior.Restrict) + .HasConstraintName("fk_purchase_order_users_approved_by_user_id"); + + b.HasOne("Store.Models.Entities.Branch", "Branch") + .WithMany() + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_purchase_order_branch_branch_id"); + + b.HasOne("Store.Models.Entities.User", "RequestedByUser") + .WithMany() + .HasForeignKey("RequestedByUserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_purchase_order_users_requested_by_user_id"); + + b.HasOne("Store.Models.Entities.Supplier", "Supplier") + .WithMany() + .HasForeignKey("SupplierId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_purchase_order_suppliers_supplier_id"); + + b.Navigation("ApprovedByUser"); + + b.Navigation("Branch"); + + b.Navigation("RequestedByUser"); + + b.Navigation("Supplier"); + }); + + modelBuilder.Entity("Store.Models.Entities.PurchaseOrderItem", b => + { + b.HasOne("Store.Models.Entities.Item", "Item") + .WithMany() + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_purchase_order_item_item_item_id"); + + b.HasOne("Store.Models.Entities.PurchaseOrder", "PurchaseOrder") + .WithMany("Items") + .HasForeignKey("PurchaseOrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_purchase_order_item_purchase_order_purchase_order_id"); + + b.Navigation("Item"); + + b.Navigation("PurchaseOrder"); + }); + + modelBuilder.Entity("Store.Models.Entities.Region", b => + { + b.HasOne("Store.Models.Entities.Country", "Country") + .WithMany("Regions") + .HasForeignKey("CountryId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_region_country_country_id"); + + b.Navigation("Country"); + }); + + modelBuilder.Entity("Store.Models.Entities.RolePermission", b => + { + b.HasOne("Store.Models.Entities.Role", "Role") + .WithMany("Permissions") + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_role_permission_role_role_id"); + + b.Navigation("Role"); + }); + + modelBuilder.Entity("Store.Models.Entities.Sale", b => + { + b.HasOne("Store.Models.Entities.Invoice", "Invoice") + .WithMany("Sales") + .HasForeignKey("InvoiceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_sale_invoice_invoice_id"); + + b.HasOne("Store.Models.Entities.Item", "Item") + .WithMany("Sales") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_sale_item_item_id"); + + b.HasOne("Store.Models.Entities.User", "User") + .WithMany("Sales") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_sale_users_user_id"); + + b.Navigation("Invoice"); + + b.Navigation("Item"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Store.Models.Entities.StockMovement", b => + { + b.HasOne("Store.Models.Entities.Invoice", "Invoice") + .WithMany("StockMovements") + .HasForeignKey("InvoiceId") + .HasConstraintName("fk_stock_movement_invoice_invoice_id"); + + b.HasOne("Store.Models.Entities.Item", "Item") + .WithMany("StockMovements") + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_stock_movement_item_item_id"); + + b.HasOne("Store.Models.Entities.ItemsOrder", "ItemsOrder") + .WithMany("StockMovements") + .HasForeignKey("ItemsOrderId") + .HasConstraintName("fk_stock_movement_items_order_items_order_id"); + + b.HasOne("Store.Models.Entities.User", "PerformedByUser") + .WithMany("StockMovements") + .HasForeignKey("PerformedByUserId") + .HasConstraintName("fk_stock_movement_users_performed_by_user_id"); + + b.Navigation("Invoice"); + + b.Navigation("Item"); + + b.Navigation("ItemsOrder"); + + b.Navigation("PerformedByUser"); + }); + + modelBuilder.Entity("Store.Models.Entities.StockTransfer", b => + { + b.HasOne("Store.Models.Entities.Branch", "FromBranch") + .WithMany() + .HasForeignKey("FromBranchId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_stock_transfer_branch_from_branch_id"); + + b.HasOne("Store.Models.Entities.User", "RequestedByUser") + .WithMany() + .HasForeignKey("RequestedByUserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_stock_transfer_users_requested_by_user_id"); + + b.HasOne("Store.Models.Entities.Branch", "ToBranch") + .WithMany() + .HasForeignKey("ToBranchId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_stock_transfer_branch_to_branch_id"); + + b.Navigation("FromBranch"); + + b.Navigation("RequestedByUser"); + + b.Navigation("ToBranch"); + }); + + modelBuilder.Entity("Store.Models.Entities.StockTransferItem", b => + { + b.HasOne("Store.Models.Entities.Item", "Item") + .WithMany() + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_stock_transfer_item_item_item_id"); + + b.HasOne("Store.Models.Entities.StockTransfer", "Transfer") + .WithMany("Items") + .HasForeignKey("StockTransferId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_stock_transfer_item_stock_transfer_stock_transfer_id"); + + b.Navigation("Item"); + + b.Navigation("Transfer"); + }); + + modelBuilder.Entity("Store.Models.Entities.SupplierLocation", b => + { + b.HasOne("Store.Models.Entities.Location", "Location") + .WithMany("SupplierLocations") + .HasForeignKey("LocationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_supplier_location_location_location_id"); + + b.HasOne("Store.Models.Entities.Supplier", "Supplier") + .WithMany("Locations") + .HasForeignKey("SupplierId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_supplier_location_supplier_supplier_id"); + + b.Navigation("Location"); + + b.Navigation("Supplier"); + }); + + modelBuilder.Entity("Store.Models.Entities.User", b => + { + b.HasOne("Store.Models.Entities.Employee", "Employee") + .WithMany("Users") + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_user_employee_employee_id"); + + b.HasOne("Store.Models.Entities.Role", "Role") + .WithMany("Users") + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_user_role_role_id"); + + b.Navigation("Employee"); + + b.Navigation("Role"); + }); + + modelBuilder.Entity("Store.Models.Entities.UserBranchRole", b => + { + b.HasOne("Store.Models.Entities.Branch", "Branch") + .WithMany("UserBranchRoles") + .HasForeignKey("BranchId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_user_branch_role_branch_branch_id"); + + b.HasOne("Store.Models.Entities.Role", "Role") + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_user_branch_role_role_role_id"); + + b.HasOne("Store.Models.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_user_branch_role_user_user_id"); + + b.Navigation("Branch"); + + b.Navigation("Role"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Store.Models.Entities.UserPassword", b => + { + b.HasOne("Store.Models.Entities.User", "User") + .WithOne("Password") + .HasForeignKey("Store.Models.Entities.UserPassword", "UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_user_password_user_user_id"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Store.Models.Entities.UserPrivilege", b => + { + b.HasOne("Store.Models.Entities.Privilege", "Privilege") + .WithMany("UserPrivileges") + .HasForeignKey("PrivilegeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_user_privilege_privilege_privilege_id"); + + b.HasOne("Store.Models.Entities.User", "User") + .WithMany("Privileges") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_user_privilege_user_user_id"); + + b.Navigation("Privilege"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Store.Models.Entities.UserPrivilegeAction", b => + { + b.HasOne("Store.Models.Entities.User", "PerformedByUser") + .WithMany("PrivilegeActions") + .HasForeignKey("PerformedByUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_user_privilege_action_user_performed_by_user_id"); + + b.HasOne("Store.Models.Entities.UserPrivilege", "UserPrivilege") + .WithMany("Actions") + .HasForeignKey("UserPrivilegeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_user_privilege_action_user_privilege_user_privilege_id"); + + b.Navigation("PerformedByUser"); + + b.Navigation("UserPrivilege"); + }); + + modelBuilder.Entity("Store.Models.Entities.UserToken", b => + { + b.HasOne("Store.Models.Entities.User", "User") + .WithMany("UserTokens") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_user_token_user_user_id"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Store.Models.Entities.WastageEntry", b => + { + b.HasOne("Store.Models.Entities.Item", "Item") + .WithMany() + .HasForeignKey("ItemId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_wastage_entry_item_item_id"); + + b.HasOne("Store.Models.Entities.User", "RecordedByUser") + .WithMany() + .HasForeignKey("RecordedByUserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired() + .HasConstraintName("fk_wastage_entry_user_recorded_by_user_id"); + + b.Navigation("Item"); + + b.Navigation("RecordedByUser"); + }); + + modelBuilder.Entity("Store.Models.Entities.Branch", b => + { + b.Navigation("Invoices"); + + b.Navigation("UserBranchRoles"); + }); + + modelBuilder.Entity("Store.Models.Entities.Category", b => + { + b.Navigation("ItemCategories"); + + b.Navigation("Items"); + }); + + modelBuilder.Entity("Store.Models.Entities.City", b => + { + b.Navigation("Locations"); + }); + + modelBuilder.Entity("Store.Models.Entities.Contacts.Email", b => + { + b.Navigation("CustomerEmails"); + + b.Navigation("EmployeeEmails"); + + b.Navigation("ManufacturerEmails"); + + b.Navigation("SupplierEmails"); + + b.Navigation("UserEmails"); + }); + + modelBuilder.Entity("Store.Models.Entities.Contacts.Phone", b => + { + b.Navigation("CustomerPhones"); + + b.Navigation("EmployeePhones"); + + b.Navigation("ManufacturerPhones"); + + b.Navigation("SupplierPhones"); + + b.Navigation("UserPhones"); + }); + + modelBuilder.Entity("Store.Models.Entities.Country", b => + { + b.Navigation("Regions"); + }); + + modelBuilder.Entity("Store.Models.Entities.Customer", b => + { + b.Navigation("Emails"); + + b.Navigation("Invoices"); + + b.Navigation("Locations"); + + b.Navigation("LoyaltyAccount"); + + b.Navigation("Phones"); + + b.Navigation("Privileges"); + }); + + modelBuilder.Entity("Store.Models.Entities.CustomerLoyaltyAccount", b => + { + b.Navigation("Transactions"); + }); + + modelBuilder.Entity("Store.Models.Entities.CustomerPrivilege", b => + { + b.Navigation("Actions"); + }); + + modelBuilder.Entity("Store.Models.Entities.Department", b => + { + b.Navigation("Employees"); + }); + + modelBuilder.Entity("Store.Models.Entities.Employee", b => + { + b.Navigation("Emails"); + + b.Navigation("Locations"); + + b.Navigation("Phones"); + + b.Navigation("Privileges"); + + b.Navigation("Users"); + }); + + modelBuilder.Entity("Store.Models.Entities.EmployeePrivilege", b => + { + b.Navigation("Actions"); + }); + + modelBuilder.Entity("Store.Models.Entities.Invoice", b => + { + b.Navigation("LoyaltyTransactions"); + + b.Navigation("Sales"); + + b.Navigation("StockMovements"); + + b.Navigation("Tenders"); + }); + + modelBuilder.Entity("Store.Models.Entities.Item", b => + { + b.Navigation("Batches"); + + b.Navigation("Discount"); + + b.Navigation("ItemCategories"); + + b.Navigation("ItemCodes"); + + b.Navigation("ItemExpiry"); + + b.Navigation("OrderItems"); + + b.Navigation("Sales"); + + b.Navigation("SegmentPrices"); + + b.Navigation("StockMovements"); + }); + + modelBuilder.Entity("Store.Models.Entities.ItemsOrder", b => + { + b.Navigation("Items"); + + b.Navigation("StockMovements"); + }); + + modelBuilder.Entity("Store.Models.Entities.Location", b => + { + b.Navigation("CustomerLocations"); + + b.Navigation("EmployeeLocations"); + + b.Navigation("ManufacturerLocations"); + + b.Navigation("SupplierLocations"); + }); + + modelBuilder.Entity("Store.Models.Entities.Manufacturer", b => + { + b.Navigation("Emails"); + + b.Navigation("Items"); + + b.Navigation("Locations"); + + b.Navigation("Phones"); + }); + + modelBuilder.Entity("Store.Models.Entities.Privilege", b => + { + b.Navigation("CustomerPrivileges"); + + b.Navigation("EmployeePrivileges"); + + b.Navigation("UserPrivileges"); + }); + + modelBuilder.Entity("Store.Models.Entities.PurchaseOrder", b => + { + b.Navigation("Items"); + }); + + modelBuilder.Entity("Store.Models.Entities.Region", b => + { + b.Navigation("Cities"); + }); + + modelBuilder.Entity("Store.Models.Entities.Role", b => + { + b.Navigation("Permissions"); + + b.Navigation("Users"); + }); + + modelBuilder.Entity("Store.Models.Entities.Salary", b => + { + b.Navigation("Employees"); + }); + + modelBuilder.Entity("Store.Models.Entities.StockTransfer", b => + { + b.Navigation("Items"); + }); + + modelBuilder.Entity("Store.Models.Entities.Supplier", b => + { + b.Navigation("Emails"); + + b.Navigation("Locations"); + + b.Navigation("Orders"); + + b.Navigation("Phones"); + }); + + modelBuilder.Entity("Store.Models.Entities.TaxProfile", b => + { + b.Navigation("Items"); + }); + + modelBuilder.Entity("Store.Models.Entities.Unit", b => + { + b.Navigation("Items"); + }); + + modelBuilder.Entity("Store.Models.Entities.User", b => + { + b.Navigation("ApprovedContactChanges"); + + b.Navigation("ChangeLogs"); + + b.Navigation("ClosedShifts"); + + b.Navigation("ContactChangeRequests"); + + b.Navigation("CustomerPrivilegeActions"); + + b.Navigation("Emails"); + + b.Navigation("EmployeePrivilegeActions"); + + b.Navigation("FidoCredentials"); + + b.Navigation("Invoices"); + + b.Navigation("ManagedDiscounts"); + + b.Navigation("Notifications"); + + b.Navigation("OpenedShifts"); + + b.Navigation("Orders"); + + b.Navigation("Otps"); + + b.Navigation("Password"); + + b.Navigation("Phones"); + + b.Navigation("PrivilegeActions"); + + b.Navigation("Privileges"); + + b.Navigation("Sales"); + + b.Navigation("StockMovements"); + + b.Navigation("UserTokens"); + }); + + modelBuilder.Entity("Store.Models.Entities.UserPrivilege", b => + { + b.Navigation("Actions"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Store.DbServices/Migrations/20260817130947_SupportMultipleUserSessions.cs b/Store.DbServices/Migrations/20260817130947_SupportMultipleUserSessions.cs new file mode 100644 index 0000000..9d58b1b --- /dev/null +++ b/Store.DbServices/Migrations/20260817130947_SupportMultipleUserSessions.cs @@ -0,0 +1,123 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Store.DbServices.Migrations +{ + /// + public partial class SupportMultipleUserSessions : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "fk_user_token_user_user_id", + table: "user_token"); + + migrationBuilder.DropIndex( + name: "ix_user_token_user_id", + table: "user_token"); + + migrationBuilder.AddColumn( + name: "device_name", + table: "user_token", + type: "varchar(100)", + maxLength: 100, + nullable: true) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.AddColumn( + name: "ip_address", + table: "user_token", + type: "varchar(45)", + maxLength: 45, + nullable: true) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.AddColumn( + name: "last_active", + table: "user_token", + type: "datetime(6)", + nullable: false, + defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified)); + + migrationBuilder.AddColumn( + name: "user_agent", + table: "user_token", + type: "varchar(500)", + maxLength: 500, + nullable: true) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.UpdateData( + table: "system_setting", + keyColumn: "setting_key", + keyValue: "Auth:PasswordRecoveryMethod", + column: "last_modified", + value: new DateTime(2026, 8, 17, 13, 9, 45, 172, DateTimeKind.Utc).AddTicks(9277)); + + migrationBuilder.CreateIndex( + name: "ix_user_token_user_id", + table: "user_token", + column: "user_id"); + + migrationBuilder.AddForeignKey( + name: "fk_user_token_user_user_id", + table: "user_token", + column: "user_id", + principalTable: "user", + principalColumn: "user_id", + onDelete: ReferentialAction.Cascade); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "fk_user_token_user_user_id", + table: "user_token"); + + migrationBuilder.DropIndex( + name: "ix_user_token_user_id", + table: "user_token"); + + migrationBuilder.DropColumn( + name: "device_name", + table: "user_token"); + + migrationBuilder.DropColumn( + name: "ip_address", + table: "user_token"); + + migrationBuilder.DropColumn( + name: "last_active", + table: "user_token"); + + migrationBuilder.DropColumn( + name: "user_agent", + table: "user_token"); + + migrationBuilder.UpdateData( + table: "system_setting", + keyColumn: "setting_key", + keyValue: "Auth:PasswordRecoveryMethod", + column: "last_modified", + value: new DateTime(2026, 8, 12, 23, 5, 0, 982, DateTimeKind.Utc).AddTicks(828)); + + migrationBuilder.CreateIndex( + name: "ix_user_token_user_id", + table: "user_token", + column: "user_id", + unique: true); + + migrationBuilder.AddForeignKey( + name: "fk_user_token_user_user_id", + table: "user_token", + column: "user_id", + principalTable: "user", + principalColumn: "user_id", + onDelete: ReferentialAction.Cascade); + } + } +} diff --git a/Store.DbServices/Migrations/StoreDbContextModelSnapshot.cs b/Store.DbServices/Migrations/StoreDbContextModelSnapshot.cs index 6f1db3a..5d1e783 100644 --- a/Store.DbServices/Migrations/StoreDbContextModelSnapshot.cs +++ b/Store.DbServices/Migrations/StoreDbContextModelSnapshot.cs @@ -3761,7 +3761,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) { SettingKey = "Auth:PasswordRecoveryMethod", Description = "Determines allowed password recovery methods (OTP, TempPassword, Both)", - LastModified = new DateTime(2026, 8, 12, 23, 5, 0, 982, DateTimeKind.Utc).AddTicks(828), + LastModified = new DateTime(2026, 8, 17, 13, 9, 45, 172, DateTimeKind.Utc).AddTicks(9277), SettingValue = "Both" }); }); @@ -4125,14 +4125,28 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("datetime(6)") .HasColumnName("date_created"); + b.Property("DeviceName") + .HasMaxLength(100) + .HasColumnType("varchar(100)") + .HasColumnName("device_name"); + b.Property("ExpiryDate") .HasColumnType("datetime(6)") .HasColumnName("expiry_date"); + b.Property("IpAddress") + .HasMaxLength(45) + .HasColumnType("varchar(45)") + .HasColumnName("ip_address"); + b.Property("IsRevoked") .HasColumnType("tinyint(1)") .HasColumnName("is_revoked"); + b.Property("LastActive") + .HasColumnType("datetime(6)") + .HasColumnName("last_active"); + b.Property("LastModified") .HasColumnType("datetime(6)") .HasColumnName("last_modified"); @@ -4153,6 +4167,11 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("varchar(2000)") .HasColumnName("token"); + b.Property("UserAgent") + .HasMaxLength(500) + .HasColumnType("varchar(500)") + .HasColumnName("user_agent"); + b.Property("UserId") .HasColumnType("char(36)") .HasColumnName("user_id"); @@ -4161,7 +4180,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasName("pk_user_token"); b.HasIndex("UserId") - .IsUnique() .HasDatabaseName("ix_user_token_user_id"); b.ToTable("user_token"); @@ -5391,8 +5409,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) modelBuilder.Entity("Store.Models.Entities.UserToken", b => { b.HasOne("Store.Models.Entities.User", "User") - .WithOne("UserToken") - .HasForeignKey("Store.Models.Entities.UserToken", "UserId") + .WithMany("UserTokens") + .HasForeignKey("UserId") .OnDelete(DeleteBehavior.Cascade) .IsRequired() .HasConstraintName("fk_user_token_user_user_id"); @@ -5679,7 +5697,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("StockMovements"); - b.Navigation("UserToken"); + b.Navigation("UserTokens"); }); modelBuilder.Entity("Store.Models.Entities.UserPrivilege", b => diff --git a/Store.DbServices/Services/AuthenticationService.cs b/Store.DbServices/Services/AuthenticationService.cs index 21e6c4f..705cfeb 100644 --- a/Store.DbServices/Services/AuthenticationService.cs +++ b/Store.DbServices/Services/AuthenticationService.cs @@ -20,18 +20,20 @@ public class AuthenticationService : IAuthenticationService { private readonly IUnitOfWork _uow; private readonly IConfiguration _config; + private readonly Microsoft.AspNetCore.Http.IHttpContextAccessor _httpContextAccessor; - public AuthenticationService(IUnitOfWork uow, IConfiguration config) + public AuthenticationService(IUnitOfWork uow, IConfiguration config, Microsoft.AspNetCore.Http.IHttpContextAccessor httpContextAccessor) { _uow = uow; _config = config; + _httpContextAccessor = httpContextAccessor; } public async Task LoginAsync(LoginRequest request, CancellationToken ct = default) { var user = await _uow.Repository().Query() .Include(u => u.Password) - .Include(u => u.UserToken) + .Include(u => u.UserTokens) .Include(u => u.Role) .FirstOrDefaultAsync(u => u.Username == request.Username.Trim(), ct); @@ -48,7 +50,7 @@ public AuthenticationService(IUnitOfWork uow, IConfiguration config) var user = await _uow.Repository().Query() .Include(u => u.Password) - .Include(u => u.UserToken) + .Include(u => u.UserTokens) .Include(u => u.Role) .FirstOrDefaultAsync(u => u.UserId == userEmail.UserId, ct); @@ -65,7 +67,7 @@ public AuthenticationService(IUnitOfWork uow, IConfiguration config) var user = await _uow.Repository().Query() .Include(u => u.Password) - .Include(u => u.UserToken) + .Include(u => u.UserTokens) .Include(u => u.Role) .FirstOrDefaultAsync(u => u.UserId == userPhone.UserId, ct); @@ -76,7 +78,7 @@ public AuthenticationService(IUnitOfWork uow, IConfiguration config) { var user = await _uow.Repository().Query() .Include(u => u.Role) - .Include(u => u.UserToken) + .Include(u => u.UserTokens) .Include(u => u.Password) .FirstOrDefaultAsync(u => u.UserId == userId, ct); @@ -121,29 +123,25 @@ public AuthenticationService(IUnitOfWork uow, IConfiguration config) var permissions = await GetPermissionClaimsAsync(user.RoleId, ct); var (token, refreshToken, expiry, refreshExpiry) = GenerateTokens(user, permissions); - if (user.UserToken is null) - { - var newToken = new UserToken - { - UserId = user.UserId, - Token = token, - RefreshTokenHash = HashRefreshToken(refreshToken), - ExpiryDate = expiry, - RefreshTokenExpiryDate = refreshExpiry, - IsRevoked = false - }; - await _uow.Repository().AddAsync(newToken, ct); - } - else - { - user.UserToken.Token = token; - user.UserToken.RefreshTokenHash = HashRefreshToken(refreshToken); - user.UserToken.ExpiryDate = expiry; - user.UserToken.RefreshTokenExpiryDate = refreshExpiry; - user.UserToken.IsRevoked = false; - _uow.Repository().Update(user.UserToken); - } - + var context = _httpContextAccessor.HttpContext; + var ipAddress = context?.Connection?.RemoteIpAddress?.ToString(); + var userAgent = context?.Request?.Headers["User-Agent"].ToString(); + + var newToken = new UserToken + { + UserId = user.UserId, + Token = token, + RefreshTokenHash = HashRefreshToken(refreshToken), + ExpiryDate = expiry, + RefreshTokenExpiryDate = refreshExpiry, + IsRevoked = false, + IpAddress = ipAddress, + UserAgent = userAgent, + DateCreated = DateTime.UtcNow, + LastActive = DateTime.UtcNow + }; + + user.UserTokens.Add(newToken); await _uow.SaveChangesAsync(ct); return new LoginResponse @@ -171,7 +169,7 @@ public AuthenticationService(IUnitOfWork uow, IConfiguration config) if (!Guid.TryParse(userIdClaim, out var userId)) return null; var userToken = await _uow.Repository().Query() - .FirstOrDefaultAsync(t => t.UserId == userId && !t.IsRevoked, ct); + .FirstOrDefaultAsync(t => t.UserId == userId && t.Token == request.Token && !t.IsRevoked, ct); if (userToken is null) return null; if (userToken.RefreshTokenExpiryDate < DateTime.UtcNow) return null; @@ -225,7 +223,7 @@ public AuthenticationService(IUnitOfWork uow, IConfiguration config) var user = await _uow.Repository().Query() .Include(u => u.Role) - .Include(u => u.UserToken) + .Include(u => u.UserTokens) .FirstOrDefaultAsync(u => u.UserId == userId, ct); if (user is null || user.Status != UserStatus.Active || !user.TwoFactorEnabled || string.IsNullOrWhiteSpace(user.TwoFactorSecret)) @@ -242,29 +240,25 @@ public AuthenticationService(IUnitOfWork uow, IConfiguration config) var permissions = await GetPermissionClaimsAsync(user.RoleId, ct); var (token, refreshToken, expiry, refreshExpiry) = GenerateTokens(user, permissions); - if (user.UserToken is null) - { - var newToken = new UserToken - { - UserId = user.UserId, - Token = token, - RefreshTokenHash = HashRefreshToken(refreshToken), - ExpiryDate = expiry, - RefreshTokenExpiryDate = refreshExpiry, - IsRevoked = false - }; - await _uow.Repository().AddAsync(newToken, ct); - } - else - { - user.UserToken.Token = token; - user.UserToken.RefreshTokenHash = HashRefreshToken(refreshToken); - user.UserToken.ExpiryDate = expiry; - user.UserToken.RefreshTokenExpiryDate = refreshExpiry; - user.UserToken.IsRevoked = false; - _uow.Repository().Update(user.UserToken); - } - + var context = _httpContextAccessor.HttpContext; + var ipAddress = context?.Connection?.RemoteIpAddress?.ToString(); + var userAgent = context?.Request?.Headers["User-Agent"].ToString(); + + var newToken = new UserToken + { + UserId = user.UserId, + Token = token, + RefreshTokenHash = HashRefreshToken(refreshToken), + ExpiryDate = expiry, + RefreshTokenExpiryDate = refreshExpiry, + IsRevoked = false, + IpAddress = ipAddress, + UserAgent = userAgent, + DateCreated = DateTime.UtcNow, + LastActive = DateTime.UtcNow + }; + + user.UserTokens.Add(newToken); await _uow.SaveChangesAsync(ct); return new LoginResponse @@ -284,6 +278,33 @@ public AuthenticationService(IUnitOfWork uow, IConfiguration config) } public async Task LogoutAsync(Guid userId, CancellationToken ct = default) + { + var authHeader = _httpContextAccessor.HttpContext?.Request?.Headers["Authorization"].FirstOrDefault(); + var token = authHeader?.StartsWith("Bearer ") == true ? authHeader.Substring("Bearer ".Length).Trim() : null; + + if (string.IsNullOrEmpty(token)) return false; + + var userToken = await _uow.Repository().Query() + .FirstOrDefaultAsync(t => t.UserId == userId && t.Token == token, ct); + + if (userToken is not null) + { + userToken.IsRevoked = true; + _uow.Repository().Update(userToken); + await _uow.SaveChangesAsync(ct); + return true; + } + + return false; + } + + /// + /// Revokes all active sessions for a specific user and forces them to re-authenticate everywhere. + /// + /// The unique identifier of the user. + /// The cancellation token. + /// True if successful, otherwise false. + public async Task RevokeAllSessionsAsync(Guid userId, CancellationToken ct = default) { var user = await _uow.Repository().Query() .FirstOrDefaultAsync(u => u.UserId == userId, ct); @@ -294,14 +315,15 @@ public async Task LogoutAsync(Guid userId, CancellationToken ct = default) user.SecurityStamp = Guid.NewGuid(); _uow.Repository().Update(user); - // Also revoke the active refresh token - var userToken = await _uow.Repository().Query() - .FirstOrDefaultAsync(t => t.UserId == userId, ct); + // Also revoke all active refresh tokens + var userTokens = await _uow.Repository().Query() + .Where(t => t.UserId == userId && !t.IsRevoked) + .ToListAsync(ct); - if (userToken is not null) + foreach (var t in userTokens) { - userToken.IsRevoked = true; - _uow.Repository().Update(userToken); + t.IsRevoked = true; + _uow.Repository().Update(t); } await _uow.SaveChangesAsync(ct); @@ -403,28 +425,25 @@ public async Task ResetPasswordAsync(ResetPasswordRequest request, Cancell var permissions = await GetPermissionClaimsAsync(user.RoleId, ct); var (token, refreshToken, expiry, refreshExpiry) = GenerateTokens(user, permissions); - if (user.UserToken is null) - { - var newToken = new UserToken - { - UserId = user.UserId, - Token = token, - RefreshTokenHash = HashRefreshToken(refreshToken), - ExpiryDate = expiry, - RefreshTokenExpiryDate = refreshExpiry, - IsRevoked = false - }; - await _uow.Repository().AddAsync(newToken, ct); - } - else - { - user.UserToken.Token = token; - user.UserToken.RefreshTokenHash = HashRefreshToken(refreshToken); - user.UserToken.ExpiryDate = expiry; - user.UserToken.RefreshTokenExpiryDate = refreshExpiry; - user.UserToken.IsRevoked = false; - _uow.Repository().Update(user.UserToken); - } + var context = _httpContextAccessor.HttpContext; + var ipAddress = context?.Connection?.RemoteIpAddress?.ToString(); + var userAgent = context?.Request?.Headers["User-Agent"].ToString(); + + var newToken = new UserToken + { + UserId = user.UserId, + Token = token, + RefreshTokenHash = HashRefreshToken(refreshToken), + ExpiryDate = expiry, + RefreshTokenExpiryDate = refreshExpiry, + IsRevoked = false, + IpAddress = ipAddress, + UserAgent = userAgent, + DateCreated = DateTime.UtcNow, + LastActive = DateTime.UtcNow + }; + + user.UserTokens.Add(newToken); await _uow.SaveChangesAsync(ct); diff --git a/Store.DbServices/Services/EmployeeService.cs b/Store.DbServices/Services/EmployeeService.cs index b3ffa05..2bf38bc 100644 --- a/Store.DbServices/Services/EmployeeService.cs +++ b/Store.DbServices/Services/EmployeeService.cs @@ -26,6 +26,38 @@ public class EmployeeService : IEmployeeService return emp is null ? null : MapToDto(emp); } + public async Task Get360ByIdAsync(Guid employeeId, CancellationToken ct = default) + { + var emp = await _uow.Repository().Query() + .Include(e => e.Department) + .Include(e => e.Salary) + .Include(e => e.Emails).ThenInclude(ee => ee.Email) + .Include(e => e.Phones).ThenInclude(ep => ep.Phone) + .Include(e => e.Users).ThenInclude(u => u.Role) + .AsNoTracking() + .FirstOrDefaultAsync(e => e.EmployeeId == employeeId, ct); + + if (emp is null) return null; + + var dto = new Employee360Dto + { + Profile = MapToDto(emp), + Emails = emp.Emails.Select(ee => ee.Email.Address).ToList(), + Phones = emp.Phones.Select(ep => ep.Phone.Number).ToList(), + Users = emp.Users.Select(u => new Store.Models.DTOs.Users.UserDto + { + UserId = u.UserId, + Username = u.Username, + RoleId = u.RoleId, + RoleName = u.Role?.Name, + Status = u.Status, + DateCreated = u.DateCreated + }).ToList() + }; + + return dto; + } + public async Task> GetAllAsync(PagedRequest request, CancellationToken ct = default) { var query = _uow.Repository().Query() diff --git a/Store.DbServices/Services/UserService.cs b/Store.DbServices/Services/UserService.cs index f9be40c..2fd8cfc 100644 --- a/Store.DbServices/Services/UserService.cs +++ b/Store.DbServices/Services/UserService.cs @@ -1,5 +1,6 @@ using OtpNet; using Store.Models.DTOs.Users; +using Store.Models.DTOs.Employees; using Store.Models.DTOs.Common; using Store.Models.Entities; using Store.DbServices.Context; @@ -37,6 +38,41 @@ public UserService(IUserAggregateRepository users, StoreDbContext db) return MapToDto(user); } + public async Task Get360ByIdAsync(Guid userId, CancellationToken ct = default) + { + var profile = await GetByIdAsync(userId, ct); + if (profile == null) return null; + + var dto = new User360Dto + { + Profile = profile, + RecentActivity = await GetRecentActivityAsync(userId, ct), + PendingContactChanges = await GetPendingContactChangesByUserIdAsync(userId, ct), + ActiveSessions = await GetActiveSessionsAsync(userId, ct) + }; + + if (profile.EmployeeId.HasValue) + { + dto.LinkedEmployee = await _db.Employees + .AsNoTracking() + .Where(e => e.EmployeeId == profile.EmployeeId.Value) + .Select(e => new EmployeeDto + { + EmployeeId = e.EmployeeId, + FirstName = e.FirstName, + MiddleName = e.MiddleName, + LastName = e.LastName, + Gender = e.Gender, + DateEmployed = e.DateEmployed, + Status = e.Status, + DateCreated = e.DateCreated + }) + .FirstOrDefaultAsync(ct); + } + + return dto; + } + public async Task> GetAllAsync(PagedRequest request, CancellationToken ct = default) { var (users, total) = await _users.GetPagedUsersWithRoleAsync(request, ct); @@ -254,11 +290,37 @@ public async Task> GetRecentActivityAsync(Guid }).ToList(); } + public async Task> GetActiveSessionsAsync(Guid userId, CancellationToken ct = default) + { + var sessions = await _db.UserTokens + .AsNoTracking() + .Where(t => t.UserId == userId && !t.IsRevoked && t.RefreshTokenExpiryDate > DateTime.UtcNow) + .OrderByDescending(t => t.LastActive) + .Select(t => new Store.Models.DTOs.Auth.UserSessionDto + { + SessionId = t.UserTokenId, + IpAddress = t.IpAddress, + UserAgent = t.UserAgent, + DeviceName = t.DeviceName, + DateCreated = t.DateCreated, + LastActive = t.LastActive, + IsRevoked = t.IsRevoked + }) + .ToListAsync(ct); + + return sessions; + } + public Task RevokeAllSessionsAsync(CancellationToken ct = default) { throw new NotImplementedException("Handled by AuthenticationService directly on the API side."); } + public Task RevokeAllSessionsAsync(Guid userId, CancellationToken ct = default) + { + throw new NotImplementedException("Handled by AuthenticationService directly on the API side."); + } + public async Task RequestContactChangeAsync(Guid userId, CreateContactChangeDto request, CancellationToken ct = default) { var existingRequest = await _db.ContactChangeRequests diff --git a/Store.DbServices/Store.DbServices.csproj b/Store.DbServices/Store.DbServices.csproj index 80c1e76..d4ddd6e 100644 --- a/Store.DbServices/Store.DbServices.csproj +++ b/Store.DbServices/Store.DbServices.csproj @@ -9,6 +9,7 @@ + diff --git a/Store.Models/DTOs/Auth/UserSessionDto.cs b/Store.Models/DTOs/Auth/UserSessionDto.cs new file mode 100644 index 0000000..4ec0225 --- /dev/null +++ b/Store.Models/DTOs/Auth/UserSessionDto.cs @@ -0,0 +1,13 @@ +namespace Store.Models.DTOs.Auth; + +public class UserSessionDto +{ + public int SessionId { get; set; } + public string? IpAddress { get; set; } + public string? UserAgent { get; set; } + public string? DeviceName { get; set; } + public DateTime DateCreated { get; set; } + public DateTime LastActive { get; set; } + public bool IsRevoked { get; set; } + public bool IsCurrentSession { get; set; } +} diff --git a/Store.Models/DTOs/Employees/Employee360Dto.cs b/Store.Models/DTOs/Employees/Employee360Dto.cs new file mode 100644 index 0000000..1655e57 --- /dev/null +++ b/Store.Models/DTOs/Employees/Employee360Dto.cs @@ -0,0 +1,13 @@ +using Store.Models.DTOs.Users; + +namespace Store.Models.DTOs.Employees; + +public class Employee360Dto +{ + public EmployeeDto Profile { get; set; } = new(); + + public List Phones { get; set; } = new(); + public List Emails { get; set; } = new(); + + public List Users { get; set; } = new(); +} diff --git a/Store.Models/DTOs/Users/User360Dto.cs b/Store.Models/DTOs/Users/User360Dto.cs new file mode 100644 index 0000000..2c77178 --- /dev/null +++ b/Store.Models/DTOs/Users/User360Dto.cs @@ -0,0 +1,14 @@ +using Store.Models.DTOs.Common; +using Store.Models.DTOs.Employees; +using System.Collections.Generic; + +namespace Store.Models.DTOs.Users; + +public class User360Dto +{ + public UserDto Profile { get; set; } = null!; + public EmployeeDto? LinkedEmployee { get; set; } + public IReadOnlyCollection RecentActivity { get; set; } = new List(); + public IReadOnlyCollection PendingContactChanges { get; set; } = new List(); + public IReadOnlyCollection ActiveSessions { get; set; } = new List(); +} diff --git a/Store.Models/Entities/User.cs b/Store.Models/Entities/User.cs index b88ede7..a705577 100644 --- a/Store.Models/Entities/User.cs +++ b/Store.Models/Entities/User.cs @@ -29,7 +29,7 @@ public class User : BaseEntity public Employee? Employee { get; set; } public Role Role { get; set; } = null!; public UserPassword? Password { get; set; } - public UserToken? UserToken { get; set; } + public ICollection UserTokens { get; set; } = new List(); public ICollection Emails { get; set; } = new List(); public ICollection Phones { get; set; } = new List(); diff --git a/Store.Models/Entities/UserToken.cs b/Store.Models/Entities/UserToken.cs index 47f3e60..73700ef 100644 --- a/Store.Models/Entities/UserToken.cs +++ b/Store.Models/Entities/UserToken.cs @@ -22,5 +22,11 @@ public class UserToken : BaseEntity public DateTime RefreshTokenExpiryDate { get; set; } public bool IsRevoked { get; set; } + public string? IpAddress { get; set; } + public string? UserAgent { get; set; } + public string? DeviceName { get; set; } + public DateTime DateCreated { get; set; } = DateTime.UtcNow; + public DateTime LastActive { get; set; } = DateTime.UtcNow; + public User User { get; set; } = null!; } diff --git a/Store.Models/Interfaces/Services/IAuthenticationService.cs b/Store.Models/Interfaces/Services/IAuthenticationService.cs index 3e02704..f0b20e8 100644 --- a/Store.Models/Interfaces/Services/IAuthenticationService.cs +++ b/Store.Models/Interfaces/Services/IAuthenticationService.cs @@ -11,5 +11,6 @@ public interface IAuthenticationService Task LoginWithBiometricsAsync(Guid userId, CancellationToken ct = default); Task RefreshTokenAsync(RefreshTokenRequest request, CancellationToken ct = default); Task LogoutAsync(Guid userId, CancellationToken ct = default); + Task RevokeAllSessionsAsync(Guid userId, CancellationToken ct = default); Task ResetPasswordAsync(ResetPasswordRequest request, CancellationToken ct = default); } diff --git a/Store.Models/Interfaces/Services/IEmployeeService.cs b/Store.Models/Interfaces/Services/IEmployeeService.cs index 666f2a1..34b610f 100644 --- a/Store.Models/Interfaces/Services/IEmployeeService.cs +++ b/Store.Models/Interfaces/Services/IEmployeeService.cs @@ -6,6 +6,7 @@ namespace Store.Models.Interfaces.Services; public interface IEmployeeService { Task GetByIdAsync(Guid employeeId, CancellationToken ct = default); + Task Get360ByIdAsync(Guid employeeId, CancellationToken ct = default); Task> GetAllAsync(PagedRequest request, CancellationToken ct = default); Task CreateAsync(CreateEmployeeRequest request, CancellationToken ct = default); Task UpdateAsync(Guid employeeId, UpdateEmployeeRequest request, CancellationToken ct = default); diff --git a/Store.Models/Interfaces/Services/IUserService.cs b/Store.Models/Interfaces/Services/IUserService.cs index 39d2d36..1a563c3 100644 --- a/Store.Models/Interfaces/Services/IUserService.cs +++ b/Store.Models/Interfaces/Services/IUserService.cs @@ -6,6 +6,7 @@ namespace Store.Models.Interfaces.Services; public interface IUserService { Task GetByIdAsync(Guid userId, CancellationToken ct = default); + Task Get360ByIdAsync(Guid userId, CancellationToken ct = default); Task> GetAllAsync(PagedRequest request, CancellationToken ct = default); Task CreateAsync(CreateUserRequest request, CancellationToken ct = default); Task UpdateAsync(Guid userId, UpdateUserRequest request, CancellationToken ct = default); @@ -21,7 +22,9 @@ public interface IUserService Task Verify2FAAsync(Guid userId, Verify2FARequest request, CancellationToken ct = default); Task Disable2FAAsync(Guid userId, CancellationToken ct = default); Task> GetRecentActivityAsync(Guid userId, CancellationToken ct = default); + Task> GetActiveSessionsAsync(Guid userId, CancellationToken ct = default); Task RevokeAllSessionsAsync(CancellationToken ct = default); + Task RevokeAllSessionsAsync(Guid userId, CancellationToken ct = default); // Contact Change Workflow Task RequestContactChangeAsync(Guid userId, CreateContactChangeDto request, CancellationToken ct = default); diff --git a/Store.UI/Pages/Employees.cshtml b/Store.UI/Pages/Employees.cshtml index fb63e3e..8c6e1c7 100644 --- a/Store.UI/Pages/Employees.cshtml +++ b/Store.UI/Pages/Employees.cshtml @@ -20,13 +20,31 @@ } -
-
+
+
+
+ +
+ + +
+
+
+
+ +
+
+ +
+

Staff Records - @Model.TotalEmployees total + @Model.TotalEmployees total

-
@if (!Model.Employees.Any()) @@ -35,7 +53,7 @@ } else { -
+
@@ -62,7 +80,9 @@
} - + @@ -71,28 +91,42 @@ { Active } + else if (e.Status == EmployeeStatus.Pending) + { + Pending + } + else if (e.Status == EmployeeStatus.Fired || e.Status == EmployeeStatus.Suspended || e.Status == EmployeeStatus.Sanctioned) + { + @e.Status + } else { @e.Status } - } @@ -102,93 +136,304 @@ @if (Model.TotalPages > 1) { - +
+ +
} } - - - -
@e.FullName + @e.FullName + @e.Gender @(e.DepartmentName ?? "—") @e.DateEmployed.ToString("yyyy-MM-dd") - -
- @Html.AntiForgeryToken() - - -
+
+
+ + + @if (e.Status != EmployeeStatus.Fired) + { +
+ @Html.AntiForgeryToken() + + +
+ } +
+ @u.Username @u.Username@(u.RoleName ?? u.RoleId.ToString()) @if (u.Status == Store.Models.Enums.UserStatus.Active) { @@ -239,8 +239,88 @@ + +
+ + @section Scripts { - } diff --git a/Store.UI/Pages/Users.cshtml.cs b/Store.UI/Pages/Users.cshtml.cs index 7fcf2f7..b8ffde8 100644 --- a/Store.UI/Pages/Users.cshtml.cs +++ b/Store.UI/Pages/Users.cshtml.cs @@ -179,4 +179,40 @@ public async Task OnPostIssuePasswordAsync(Guid userId, Cancellat return RedirectToPage(); } + + public async Task OnPostRevokeSessionsAsync(Guid userId, CancellationToken ct = default) + { + if (!TryGetSecurityContext(out var token, out _)) return GoToLogin(); + _apiClient.SetToken(token); + + try + { + await _userService.RevokeAllSessionsAsync(userId, ct); + StatusMessage = "All active sessions revoked successfully."; + } + catch (Exception ex) + { + StatusMessage = $"Error: {ex.Message}"; + } + + return RedirectToPage(); + } + + public async Task OnGetUserDrawerAsync(Guid id, CancellationToken ct = default) + { + if (!TryGetSecurityContext(out var token, out _)) return Unauthorized(); + _apiClient.SetToken(token); + + try + { + var user360 = await _userService.Get360ByIdAsync(id, ct); + if (user360 == null) return NotFound(); + + return new JsonResult(user360); + } + catch (Exception ex) + { + return StatusCode(500, new { error = ex.Message }); + } + } } diff --git a/Store.UI/Services/ApiAuthenticationService.cs b/Store.UI/Services/ApiAuthenticationService.cs index a628adc..fe1714a 100644 --- a/Store.UI/Services/ApiAuthenticationService.cs +++ b/Store.UI/Services/ApiAuthenticationService.cs @@ -58,6 +58,12 @@ public async Task LogoutAsync(Guid userId, CancellationToken ct = default) return await _client.PostAsync("/api/auth/logout", null, ct); } + public async Task RevokeAllSessionsAsync(Guid userId, CancellationToken ct = default) + { + _logger.LogInformation("Revoking all sessions for user {UserId}", userId); + return await _client.PostAsync($"/api/auth/revoke-all/{userId}", null, ct); + } + public async Task ResetPasswordAsync(ResetPasswordRequest request, CancellationToken ct = default) { _logger.LogInformation("Resetting password for user"); diff --git a/Store.UI/Services/ApiEmployeeService.cs b/Store.UI/Services/ApiEmployeeService.cs index f24f543..a440648 100644 --- a/Store.UI/Services/ApiEmployeeService.cs +++ b/Store.UI/Services/ApiEmployeeService.cs @@ -21,6 +21,11 @@ public ApiEmployeeService(IApiClientService client, ILogger return await _client.GetAsync($"/api/employees/{employeeId}", ct); } + public async Task Get360ByIdAsync(Guid employeeId, CancellationToken ct = default) + { + return await _client.GetAsync($"/api/employees/{employeeId}/360", ct); + } + public async Task> GetAllAsync(PagedRequest request, CancellationToken ct = default) { var qs = $"?page={request.Page}&pageSize={request.PageSize}&includeInactive={request.IncludeInactive.ToString().ToLower()}"; diff --git a/Store.UI/Services/ApiUserService.cs b/Store.UI/Services/ApiUserService.cs index 44c0323..1a6d0fc 100644 --- a/Store.UI/Services/ApiUserService.cs +++ b/Store.UI/Services/ApiUserService.cs @@ -21,6 +21,11 @@ public ApiUserService(IApiClientService client, ILogger logger) return await _client.GetAsync($"/api/users/{userId}", ct); } + public async Task Get360ByIdAsync(Guid userId, CancellationToken ct = default) + { + return await _client.GetAsync($"/api/users/{userId}/360", ct); + } + public async Task> GetAllAsync(PagedRequest request, CancellationToken ct = default) { var qs = $"?page={request.Page}&pageSize={request.PageSize}"; @@ -110,11 +115,22 @@ public async Task> GetRecentActivityAsync(Guid return result ?? Array.Empty(); } + public async Task> GetActiveSessionsAsync(Guid userId, CancellationToken ct = default) + { + var result = await _client.GetAsync>($"/api/users/{userId}/sessions", ct); + return result ?? Array.Empty(); + } + public async Task RevokeAllSessionsAsync(CancellationToken ct = default) { return await _client.PostAsync("/api/users/profile/sessions/revoke", null, ct); } + public async Task RevokeAllSessionsAsync(Guid userId, CancellationToken ct = default) + { + return await _client.PostAsync($"/api/users/{userId}/sessions/revoke", null, ct); + } + public async Task RequestContactChangeAsync(Guid userId, CreateContactChangeDto request, CancellationToken ct = default) { var result = await _client.PostAsync("/api/users/profile/contact-change", request, ct); From 29612f0327263bb746d1182fe2293730e34a958c Mon Sep 17 00:00:00 2001 From: Alain Kimbu Date: Tue, 18 Aug 2026 05:13:20 +0100 Subject: [PATCH 2/3] docs: create design system specification for Operations Console --- docs/design_system_specification.md | 550 ++++++++++++++++++++++++++++ 1 file changed, 550 insertions(+) create mode 100644 docs/design_system_specification.md diff --git a/docs/design_system_specification.md b/docs/design_system_specification.md new file mode 100644 index 0000000..d09ddc8 --- /dev/null +++ b/docs/design_system_specification.md @@ -0,0 +1,550 @@ +# ClexAn Foods Operations Console — Master UI/UX Design System & Standardization Specification + +--- + +## 1. Executive Summary & Design Philosophy + +The **ClexAn Foods Operations Console** is an enterprise-grade retail, inventory, procurement, billing, loyalty, and customer relationship management (CRM) platform. + +The visual identity embodies: +1. **Emerald Organic Retail Heritage**: Deep forest greens (`#131c17` to `#1f2f25`), vibrant brand greens (`#019c01`), and crisp organic tints (`#e8f5e8`) reflecting fresh food operations. +2. **High-Density Information Architecture**: Clean, scannable, high-contrast typography, data-dense tables, 360-degree interactive flyout drawers (blades), and modular metric cards. +3. **Cohesive Micro-Interactions**: Predictable hover elevations (`translateY(-2px)`), glassmorphic backdrops, smooth drawer transitions (`cubic-bezier(0.2, 0.8, 0.2, 1)`), and consistent modal layers. + +--- + +## 2. Screenshot-by-Screenshot Visual Audit & Analysis + +Every image in `C:\Users\Rodern\Downloads\StoreProject Export & Artifacts\UI Screenshots` was thoroughly cataloged and inspected down to individual element styles. + +| # | Screenshot Filename | Screen / Modal / Blade | Key Elements & Styles Identified | Discrepancies & Standardization Needed | +|---|---|---|---|---| +| 1 | `Screenshot_..._125817` | **Item Catalog & Inventory (Table View)** | 4-card KPI summary, search filter dock, category dropdown, sort dropdown, "+ New Product" green CTA, status pills, Table/Grid toggle, item rows with photo, barcode, category badge, bold green selling price, margin % badge, stock status pill, solid green action buttons ("Edit", "⚡ Stock"), floating scanner active FAB. | View toggle order (Table first, Grid second) was reversed compared to other screens. Table actions were solid green while other tables used outline buttons. | +| 2 | `Screenshot_..._125833` | **Item Catalog & Inventory (Grid View)** | 4-column product grid, top-left category badge, top-right stock/unit pill, white photo container, product title, barcode tag with icon, bold price + cost + margin badge pill. | Standardized card border-radius to `12px` and uniform height/padding across all modules. | +| 3 | `Screenshot_..._131013` | **Invoice Line-Item Return & Refund Modal (Top)** | Modal header with rotate icon, table of items with Unit Price, Qty Paid, Return Qty input boxes, Refund Subtotal calculation, Cancel button, "Process Return & Refund" CTA. | **Critical Inconsistency:** CTA button and accents used purple (`#7c3aed`/`#6366f1`) instead of brand palette and financial semantic colors. | +| 4 | `Screenshot_..._131029` | **Invoice Line-Item Return & Refund Modal (Bottom)** | Refund Method dropdown, "Restock Inventory" checkbox, "Reason for Return" input, "Total Refund to Customer" calculation highlight card. | Purple-tinted total box (`#f5f3ff`) and purple CTA button standardized to brand green/amber financial style. | +| 5 | `Screenshot_..._131134` | **Supplier 360 Hub & Procurement (Grid View)** | 5 KPI cards (Total Suppliers, Active Partners, Procurement Spend, Open POs, Due Deliveries), Search input, Sort dropdown, Grid/Table toggle, "Export CSV", "+ New Supplier" CTA, Supplier contact card with "SS" avatar, 360 pill button, PO & Edit mini buttons. | KPI grid wrapped as 3 + 2 cards. Standardized grid to responsive auto-fit. | +| 6 | `Screenshot_..._131150` | **Supplier 360 Hub & Procurement (Table View)** | KPI banner, toolbar, data table with SS avatar, Supplier Name, Reg #, Phone, Email, Location, Registered date, Outline Action buttons ("360", "PO", "Edit"). | Inconsistent action button styling between Catalog table (solid green) and Supplier table (gray outline). Standardized to clean outline pill buttons. | +| 7 | `Screenshot_..._13125` | **Product 360 Blade / Drawer** | Top status badges ("Active", "Product"), close button, product photo, title "Broli Milk", category & unit pills, 2x2 financial metric cards (Selling Price, Cost Price, Estimated Profit, Gross Margin), total valuation card, stock level progress bar, Barcode & SKU Studio with copy button and print label CTA, 4-button sticky bottom footer ("Edit", "⚡ Adjust Stock", "🛒 POS", "Deactivate"). | Standardized blade widths (`540px`), card spacing, and typography scale. | +| 8 | `Screenshot_..._131326` | **Register New Supplier Modal** | Modal header, Avatar upload tile, Company Name input, Tax ID, Notes / Payment Terms, repeatable Email, Phone, and Location field rows with "+ Add" pills, "Primary" checkmark, Cancel & "Create Supplier" CTA. | Form grid layout glitch where Notes textarea floated awkwardly beside the label instead of full-width stack. | +| 9 | `Screenshot_..._131421` | **Supplier 360 Drawer (Vendor ID Pass Tab)** | Avatar, Vendor name, Reg ID badge, quick communication pills (WhatsApp, Call, Email, Map), 3-stat summary (Total Spend, Total Orders, Open POs), drawer tab navigation, dark navy Authorised Vendor Pass card (`#0f172a`), barcode graphic, print badge button, drawer footer (Delete, Edit, Create PO). | Golden standard for 360 Flyout Passes; adopted as universal design blueprint for Customer and Loyalty passes. | +| 10 | `Screenshot_..._131434` | **Supplier 360 Drawer (Contacts & Locations Tab)** | Email Addresses card with green "Primary" pill, Phone Numbers card, Physical Locations card, Vendor Notes card. | Standardized contact card list styling across Supplier and Customer drawers. | +| 11 | `Screenshot_..._131450` | **Supplier 360 Drawer (Supplied Items Tab)** | Supplied item row with product name, barcode, unit cost, and Total Received badge. | Standardized itemized listing card. | +| 12 | `Screenshot_..._13145` | **Quick Stock Adjustment Modal (Layered on Blade)** | Lightning icon, product name subtitle, current on-hand stock box, adjustment qty input with helper text, reason code dropdown, Cancel & "Save Adjustment" buttons. | Cancel button had a solid green background instead of secondary neutral outline. | +| 13 | `Screenshot_..._13156` | **Supplier 360 Drawer (Purchase Orders Tab)** | PO summary card (`#PO-1`, date, item count, total spend, "Draft" status pill). | Standardized PO card pill colors and metadata rows. | +| 14 | `Screenshot_..._132055` | **Loyalty & Rewards 360 Hub (Grid View)** | 5 KPI cards (Enrolled Members, Points Liability, Points Earned MTD, Points Redeemed MTD, VIP Ratio), Search, Tier filter pills (Bronze, Silver, Gold), Sort dropdown, 3-way toggle (Grid, Table, Ledger), "Export CSV", "⭐ Manage Points & Rewards" CTA, Member cards with avatar, Tier badges (Gold/Bronze), lifetime points, currency equivalent value pill, Tier progress bar, Action buttons. | Rhoda Kah card was missing the secondary "Action" button. Standardized card action row consistency. | +| 15 | `Screenshot_..._132115` | **Loyalty & Rewards Hub (Ledger View)** | Store-Wide Points Transaction Stream table with Date & Time, Member link, Phone, Type, Points (+36,909 green bold), Invoice Ref link, Notes & Reason. | Standardized table typography and badge alignments. | +| 16 | `Screenshot_..._132135` | **Loyalty & Rewards Hub (Table View)** | Loyalty Member directory table with square icon buttons (`🔍` and `⚡`) for actions. | Standardized table action buttons to match suite-wide design system. | +| 17 | `Screenshot_..._13221` | **Loyalty 360 Drawer + Manage Points Modal (Earn Points Tab)** | Dual-layer UI: Background Loyalty 360 Drawer + foreground Manage Points Modal. Modal features Earn/Redeem/Adjust segmented tab, Member search, Points to Award, live Currency Value pill, Invoice Ref, Reason, "Award Points" CTA. | Coherent and clean. Verified z-index stacking (`z-index: 250` for blade, `z-index: 300` for layered modal). | +| 18 | `Screenshot_..._132221` | **Manage Points Modal (Redeem Reward Tab)** | Points to Redeem input, Live XAF value badge, Note input, "Redeem Reward" CTA. | Verified consistent form layout. | +| 19 | `Screenshot_..._132240` | **Manage Points Modal (Admin Adjust Tab)** | Points Adjustment (+ or -) input, Live value badge, Note input, "Apply Adjustment" CTA. | Verified consistent form layout. | +| 20 | `Screenshot_..._132258` | **Loyalty 360 Drawer (Transaction Ledger Tab)** | Individual member transaction history table (Date, Type, Points, Note). | Clean, responsive nested table layout. | +| 21 | `Screenshot_..._132310` | **Loyalty 360 Drawer (Active Campaigns Tab)** | Empty state message for applicable customer segment promotions. | Standardized empty state typography and padding. | +| 22 | `Screenshot_..._132330` | **Campaigns Hub (Grid View)** | 4 KPI cards (Live Campaigns, Scheduled, Targeted Reach, Peak Multiplier), Underline status tabs (All, Live Now, Scheduled, Completed, Paused) with count badges, Filter dock, Campaign cards with title, status badge, reward pill (`$+12 Bonus`, `1.0x Points`), target segment, date range, action icon buttons (Play/Pause, Clone, Edit, Delete). | Status filters used underline tabs while other pages used pill buttons. Standardized when to use Underline Tabs vs Filter Pills. | +| 23 | `Screenshot_..._132339` | **Campaigns Hub (Table View)** | Data table with Campaign Name, Reward badge, Audience, Dates, Status pill, Play/Pause and Edit outline icon buttons. | Clean table structure matching system design. | +| 24 | `Screenshot_..._132358` | **New Campaign Modal with Simulator** | Quick Presets pills, Campaign Name, Description textarea, Type and Segment dropdowns, Multiplier input, Date pickers, Live Points Simulator card with sample spend calculation, "Active immediately" checkbox, Cancel & Create button. | Cancel button was unstyled plain text instead of `.btn-secondary`. | +| 25 | `Screenshot_..._13235` | **Edit Product Modal (with Live Margins)** | Product Name, Description, Selling Price, Cost Price, Live Unit Profit / Gross Margin / Markup metrics banner, Category, Unit, Stock Qty, Reorder Level, Barcode input with "🎲 Generate" button, Photo upload, Cancel & "Save Product" CTA. | Cancel button had a solid green background instead of secondary neutral outline. | +| 26 | `Screenshot_..._132426` | **Campaigns Hub (Live Now Empty State)** | Filtered tab showing dashed empty state container, green compass icon with halo, title "No campaigns found", subtitle, "Create Campaign" CTA. | Golden standard for In-Page Empty States. | +| 27 | `Screenshot_..._132443` | **Campaigns Hub (Scheduled Tab Filtered)** | Filtered list showing Black Friday campaign with Purple "Scheduled" badge. | Verified status badge palette (`#7c3aed` with `rgba(139,92,246,0.12)` background). | +| 28 | `Screenshot_..._132452` | **Campaigns Hub (Completed Tab Empty State)** | Filtered tab showing dashed empty state container. | Consistent with Live Now empty state. | +| 29 | `Screenshot_..._132526` | **Delete Campaign Confirmation Modal** | Danger alert icon (red exclamation triangle in soft red circle), confirmation title, bold entity name, Cancel & solid red "Delete" CTA. | Cancel button was styled as green text instead of `.btn-secondary`. | +| 30 | `Screenshot_..._132556` | **Clone Campaign Modal + Alert Banner** | Info circular icon, confirmation message, Cancel & green "Confirm" CTA. Background shows green toast alert notification banner. | Toast banner height and placement verified. | +| 31 | `Screenshot_..._13257` | **Campaigns Hub (Paused Tab Filtered)** | Filtered list showing Easter campaign with Amber "Paused" badge. | Verified status badge palette (`#d97706` with `rgba(245,158,11,0.12)` background). | +| 32 | `Screenshot_..._13443` | **Customer 360 CRM Hub (Grid View)** | 4 KPI cards (Total Registered, Tiered Accounts, Active Loyalty Points, Outstanding Balance), Segment filter pills (All, Standard, Wholesale, VIP), Loyalty Tier dropdown, "With Debt Only" checkbox, Sort dropdown, Grid/Table toggle, "+ New Customer" CTA, Customer Cards with avatar, Gold/Bronze tier badge, segment pill, lifetime spend, balance/debt status, "POS Sale" and "View 360" outline buttons. | Standardized customer card layout and button alignment. | +| 33 | `Screenshot_..._13545` | **Edit Customer Profile Modal** | First/Last Name (2 cols), Middle/Gender (2 cols), Segment dropdown, Phone/Email (2 cols), Internal CRM Notes textarea, Photo upload, Cancel & "Save Changes" CTA. | Form grid spacing and input focus states verified. | +| 34 | `Screenshot_..._1358` | **Customer 360 CRM Drawer (Invoices Tab + Loyalty Barcode Pass)** | Customer avatar, Name "Rhoda Bei Kah", VIP & Bronze Tier badges, WhatsApp/Call/Email contact pills, 3-metric financial summary (Lifetime Spend, Total Invoices, Unpaid Balance), dark Store Rewards Pass card with barcode, Loyalty Tier Progress unlock bar with "⭐ Adjust Points" button, Drawer tabs (Invoices, Loyalty Ledger, Profile & Notes), Invoices list items, Sticky footer (Delete, Edit Profile, Start POS Sale). | Complete, cohesive Customer 360 implementation. | +| 35 | `Screenshot_..._13635` | **Invoices 360 Financial & Billing Hub (Main View)** | Invoices & Billing Hub inner header card with recorded count, "Export CSV", "+ New POS Sale" CTA, 4 KPI cards (Gross Invoiced, Total Collected, Outstanding Debt, Refunds & Returns), Time range segmented tab (All Time, Today, Yesterday, Last 7d, Last 30d, This Month, Custom), Secondary filter bar (Statuses, Payment Methods, Date Sort, Filter, Reset), Table with copyable Invoice ID pill, Date/Time, Customer link, Cashier, Payment Method badge, Total Amount bold, Status badge (`✓ PAID`), Actions (`👁️ Details`, thermal receipt print, tax invoice pdf). | Standardized table typography, copyable ID button, and action icons. | +| 36 | `Screenshot_..._13724` | **Invoice Details Blade (Header & Itemized Bill)** | Header "Invoice #d22653b7", Close button, 2-column Metadata grid (Date, Customer link, Cashier, Branch, Status, Payment Mode), Itemized bill table (Item, Unit Price, Qty, Discount, Line Total), Sticky footer (Thermal Receipt, A4 Tax Invoice, Refund & Return Items, Void). | Cohesive invoice drawer layout. | +| 37 | `Screenshot_..._13749` | **Invoice Details Blade (Totals, Payment Tender, Footer)** | Scrolled view showing Grand Total financial summary card (Subtotal, Grand Total bold, Amount Tendered green, Change Given), Payment Tender Breakdown table (Method, Amount Applied, Reference, Recorded), Sticky action footer. | Clear financial calculation hierarchy and contrasting colors. | +| 38 | `Screenshot_..._13945` | **Void Invoice Confirmation Modal** | Red `⊗` icon, warning description, "Reason for Voiding" input, Cancel & solid red "Confirm & Void Invoice" CTA. | Clean danger confirmation modal. | + +--- + +## 3. Universal Design Tokens & Color Palette + +### 3.1 Core Color System + +```css +:root { + /* ── Primary Brand Green (Organic Retail Identity) ── */ + --brand: #019c01; /* Canonical primary action green */ + --brand-dark: #006300; /* Primary hover / active state */ + --brand-darker: #004700; /* Deep emphasis green */ + --brand-soft: #e8f5e8; /* Subtle tinted backgrounds */ + --brand-soft-hover: #daf0da; /* Hover state for soft items */ + --brand-border: #b7d9b7; /* Accent borders */ + --brand-glow: rgba(1, 156, 1, 0.20); /* Focus rings and button glows */ + + /* ── Dark Navigation & Shell Surface ── */ + --sidebar-grad-start: #1f2f25; /* Deep forest green top */ + --sidebar-grad-end: #131c17; /* Dark obsidian forest bottom */ + --sidebar-border: rgba(255, 255, 255, 0.08); + --sidebar-link: #dbe7df; /* Muted pale mint link text */ + --sidebar-muted: #8da394; /* Muted sidebar section labels */ + --sidebar-active-bg: rgba(1, 156, 1, 0.25); + + /* ── Canvas, Surfaces & Cards ── */ + --canvas: #f0f3f1; /* Modern cool tinted page background */ + --canvas-tint-soft: #f7fff7; /* Radial gradient top-right highlight */ + --canvas-tint-alt: #e8ece8; /* Radial gradient bottom shade */ + --surface: #ffffff; /* Card, modal, and blade background */ + --surface-alt: #f8fafc; /* Inset boxes, secondary toolbars */ + --surface-hover: #f1f5f9; /* List item and table row hover */ + + /* ── Typography & Ink ── */ + --text-primary: #0f172a; /* High contrast primary slate ink */ + --text-secondary: #64748b; /* Subtitles, labels, metadata */ + --text-muted: #94a3b8; /* Disabled, placeholders, tertiary text */ + --text-on-brand: #ffffff; /* Text on solid brand buttons */ + + /* ── Borders & Separators ── */ + --border: #e2e8f0; /* Canonical border color */ + --border-strong: #cbd5e1; /* High-emphasis borders / inputs */ + --border-subtle: #edf2f7; /* Table row separators */ + --border-hover: #94a3b8; /* Input hover border */ + + /* ── Semantic Colors & Status Badges ── */ + /* Success / Active / In Stock / Paid */ + --success: #059669; + --success-dark: #047857; + --success-soft: rgba(5, 150, 105, 0.10); + --success-border: rgba(5, 150, 105, 0.25); + + /* Danger / Out of Stock / Void / Delete / Error */ + --danger: #dc2626; + --danger-dark: #b91c1c; + --danger-soft: #fef2f2; + --danger-border: #fecaca; + + /* Warning / Low Stock / Paused / Draft / Bronze Tier */ + --warning: #d97706; + --warning-dark: #b45309; + --warning-soft: #fffbeb; + --warning-border: #fde68a; + + /* Scheduled / Info / VIP Points / Special Promotion */ + --info-purple: #7c3aed; + --info-purple-dark: #6d28d9; + --info-purple-soft: rgba(139, 92, 246, 0.12); + --info-purple-border: rgba(139, 92, 246, 0.25); + + /* Cold Info / Cyan / Stock Balance / Processing */ + --info-teal: #0d9488; + --info-teal-soft: rgba(20, 184, 166, 0.12); + + /* Metallic Gold / VIP Tier */ + --gold-tier: #d97706; + --gold-tier-soft: rgba(245, 158, 11, 0.15); + --gold-tier-border: rgba(245, 158, 11, 0.35); + + /* ── Elevation & Shadows ── */ + --shadow-xs: 0 1px 2px rgba(0, 0, 0, 0.04); + --shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.06), 0 1px 2px rgba(0, 0, 0, 0.04); + --shadow-md: 0 4px 12px rgba(0, 0, 0, 0.08), 0 2px 4px rgba(0, 0, 0, 0.04); + --shadow-lg: 0 10px 25px rgba(0, 0, 0, 0.10), 0 4px 8px rgba(0, 0, 0, 0.05); + --shadow-xl: 0 20px 40px rgba(0, 0, 0, 0.18), 0 8px 16px rgba(0, 0, 0, 0.08); + --shadow-blade: -6px 0 30px rgba(0, 0, 0, 0.15); + + /* ── Spacing & Radii ── */ + --radius-xs: 4px; + --radius-sm: 6px; + --radius-md: 8px; + --radius-lg: 12px; + --radius-xl: 16px; + --radius-full: 9999px; + + --space-1: 4px; + --space-2: 8px; + --space-3: 12px; + --space-4: 16px; + --space-5: 20px; + --space-6: 24px; + --space-8: 32px; +} +``` + +--- + +## 4. Typography Scale & Specifications + +| Role | Font Family | Size | Line Height | Weight | Tracking | Color | +|---|---|---|---|---|---|---| +| **Page Header (H1)** | Segoe UI Variable, system-ui | `1.65rem` (26px) | 1.2 | 700 | `-0.02em` | `--text-primary` (`#0f172a`) | +| **Section Header (H2)** | Segoe UI Variable, system-ui | `1.35rem` (21px) | 1.3 | 700 | `-0.01em` | `--text-primary` (`#0f172a`) | +| **Card / Modal Title (H3)** | Segoe UI Variable, system-ui | `1.15rem` (18px) | 1.3 | 600 | `0` | `--text-primary` (`#0f172a`) | +| **Subtitle / Subheader** | Segoe UI Variable, system-ui | `0.88rem` (14px) | 1.4 | 400 | `0` | `--text-secondary` (`#64748b`) | +| **KPI Metric Value** | Segoe UI Variable, system-ui | `1.50rem` (24px) | 1.1 | 700 | `-0.02em` | `--text-primary` (`#0f172a`) | +| **KPI Overline Label** | Segoe UI Variable, system-ui | `0.78rem` (12px) | 1.2 | 700 | `0.06em` | `--text-secondary` (UPPERCASE) | +| **Table Header (TH)** | Segoe UI Variable, system-ui | `0.75rem` (12px) | 1.2 | 700 | `0.05em` | `--text-secondary` (UPPERCASE) | +| **Table Body (TD)** | Segoe UI Variable, system-ui | `0.88rem` (14px) | 1.4 | 400 / 500 | `0` | `--text-primary` (`#0f172a`) | +| **Primary Price / Valuation**| Segoe UI Variable, system-ui | `1.05rem` (16px) | 1.2 | 700 | `0` | `--brand` / `--text-primary` | +| **Barcode / SKU / Codes** | Cascadia Code, Consolas, monospace | `0.80rem` (13px) | 1.2 | 600 | `0.04em` | `--text-primary` | +| **Badge / Pill Text** | Segoe UI Variable, system-ui | `0.72rem` (11.5px)| 1.2 | 700 | `0.03em` | Varies by status | + +--- + +## 5. Component Standardization Catalog + +### 5.1 KPI Summary Metric Cards + +```html +
+
+
+ +
+
+ Active Products + 19 + Total catalog items +
+
+
+``` + +**Standardized Card CSS:** +```css +.kpi-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); + gap: 16px; + margin-bottom: 20px; +} + +.kpi-card { + background: var(--surface); + border: 1px solid var(--border); + border-radius: var(--radius-lg); + padding: 16px 20px; + display: flex; + align-items: center; + gap: 16px; + box-shadow: var(--shadow-sm); + transition: transform 0.2s ease, box-shadow 0.2s ease, border-color 0.2s ease; + text-decoration: none; + color: inherit; +} + +.kpi-card:hover { + transform: translateY(-2px); + box-shadow: var(--shadow-md); + border-color: var(--brand-border); +} + +.kpi-card.active { + border-color: var(--brand); + background: var(--brand-soft); +} + +.kpi-icon { + width: 48px; + height: 48px; + border-radius: var(--radius-lg); + display: flex; + align-items: center; + justify-content: center; + font-size: 1.4rem; + flex-shrink: 0; +} + +.kpi-icon-green { background: var(--brand-soft); color: var(--brand); } +.kpi-icon-amber { background: var(--warning-soft); color: var(--warning); } +.kpi-icon-red { background: var(--danger-soft); color: var(--danger); } +.kpi-icon-purple { background: var(--info-purple-soft); color: var(--info-purple); } +.kpi-icon-teal { background: var(--info-teal-soft); color: var(--info-teal); } +``` + +--- + +### 5.2 Filter Docks & Toolbars + +The Toolbar pattern standardizes: +1. **Search Box with Icon & Clear Action**: Left side, flexible min-width (`240px`). +2. **Filter Controls (Dropdowns, Segment Pills, Date Pickers)**: Middle flex row. +3. **View Switcher (Grid / Table / Ledger)**: Segmented button group. Canonical order is always `Grid` first, `Table` second, `Ledger` third (when present). +4. **Action Buttons**: Export button (Secondary) + "+ New Entity" button (Primary Brand Green). + +```css +.toolbar { + background: var(--surface); + border: 1px solid var(--border); + border-radius: var(--radius-lg); + padding: 14px 18px; + margin-bottom: 20px; + display: flex; + flex-wrap: wrap; + gap: 12px; + align-items: center; + justify-content: space-between; + box-shadow: var(--shadow-sm); +} + +.search-input-wrap { + position: relative; + flex: 1; + min-width: 240px; +} + +.search-input-wrap input { + width: 100%; + padding: 9px 14px 9px 38px; + border-radius: var(--radius-md); + border: 1px solid var(--border); + background: var(--surface-alt); + color: var(--text-primary); + font-size: 0.9rem; + outline: none; + transition: all 0.2s ease; +} + +.search-input-wrap input:focus { + background: var(--surface); + border-color: var(--brand); + box-shadow: 0 0 0 3px var(--brand-glow); +} + +.search-icon { + position: absolute; + left: 12px; + top: 50%; + transform: translateY(-50%); + color: var(--text-secondary); + pointer-events: none; +} + +.view-toggle { + display: inline-flex; + background: var(--canvas); + border: 1px solid var(--border); + border-radius: var(--radius-md); + padding: 2px; +} + +.view-btn { + border: none; + background: transparent; + color: var(--text-secondary); + padding: 6px 12px; + border-radius: var(--radius-sm); + cursor: pointer; + font-size: 0.85rem; + font-weight: 600; + display: flex; + align-items: center; + gap: 6px; + transition: all 0.15s ease; +} + +.view-btn.active { + background: var(--surface); + color: var(--brand); + box-shadow: var(--shadow-xs); +} +``` + +--- + +### 5.3 Buttons Specification + +| Variant | Class Name | Background | Text Color | Border | Shadow / Hover | +|---|---|---|---|---|---| +| **Primary CTA** | `.btn-primary` | `--brand` (`#019c01`) | `#ffffff` | `1px solid var(--brand)` | `0 2px 6px rgba(1,156,1,0.2)` -> hover `--brand-dark` | +| **Secondary / Outline** | `.btn-secondary` | `--surface` (`#ffffff`) | `--text-primary` | `1px solid var(--border)` | hover `--surface-alt` & `--border-strong` | +| **Danger CTA** | `.btn-danger` | `--danger` (`#dc2626`) | `#ffffff` | `1px solid var(--danger)` | hover `--danger-dark` | +| **Danger Outline** | `.btn-danger-outline`| `--surface` (`#ffffff`) | `--danger` | `1px solid var(--danger-border)`| hover `--danger-soft` | +| **Icon Button** | `.icon-btn` | `--surface` | `--text-secondary`| `1px solid var(--border)` | `32px x 32px`, hover `--brand-soft` | +| **Action Pill (Table)** | `.btn-table-action` | `--surface` | `--text-primary` | `1px solid var(--border)` | `padding: 4px 10px; font-size: 0.8rem;` | + +--- + +### 5.4 360-Degree Flyout Drawers (Blades) + +The 360 Flyout Panel provides complete, in-context entity inspection without navigating away from the grid/table. + +``` +┌────────────────────────────────────────────────────────┐ +│ [Avatar] Entity Name [Status Badge] [Tier Badge] [✕] │ ← Header +├────────────────────────────────────────────────────────┤ +│ [💬 WhatsApp] [📞 Call] [✉️ Email] [📍 Map] │ ← Quick Actions +├────────────────────────────────────────────────────────┤ +│ ┌──────────────┬──────────────┬──────────────────────┐ │ +│ │ Metric 1 │ Metric 2 │ Metric 3 │ │ ← KPI Grid +│ └──────────────┴──────────────┴──────────────────────┘ │ +├────────────────────────────────────────────────────────┤ +│ ╔════════════════════════════════════════════════════╗ │ +│ ║ AUTHORIZED DIGITAL 360 PASS ║ │ ← Digital Pass +│ ║ Entity Title [ACTIVE BADGE] ║ │ (Dark Navy) +│ ║ ||||||||||||||||||||||||||||||||||||||||||||||||| ║ │ +│ ║ CODE: ENT-98472910 ║ │ +│ ║ [Print Badge] ║ │ +│ ╚════════════════════════════════════════════════════╝ │ +├────────────────────────────────────────────────────────┤ +│ [Tab 1: Overview] [Tab 2: Ledger] [Tab 3: Sub-Items] │ ← Underline Tabs +├────────────────────────────────────────────────────────┤ +│ │ +│ Active Tab Content │ ← Scrollable Body +│ │ +├────────────────────────────────────────────────────────┤ +│ [🗑️ Delete] [Edit Profile] [Primary CTA]│ ← Sticky Footer +└────────────────────────────────────────────────────────┘ +``` + +**Standardized Blade CSS:** +```css +.blade-overlay { + position: fixed; + inset: 0; + background: rgba(15, 23, 42, 0.45); + backdrop-filter: blur(3px); + z-index: 250; + opacity: 0; + visibility: hidden; + transition: opacity 0.25s ease, visibility 0.25s ease; +} + +.blade-overlay.open { + opacity: 1; + visibility: visible; +} + +.blade { + position: fixed; + top: 0; + right: 0; + bottom: 0; + width: min(540px, 100vw); + background: var(--surface); + box-shadow: var(--shadow-blade); + z-index: 260; + transform: translateX(100%); + transition: transform 0.28s cubic-bezier(0.2, 0.8, 0.2, 1); + display: flex; + flex-direction: column; +} + +.blade.open { + transform: translateX(0); +} + +.blade-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 18px 24px; + border-bottom: 1px solid var(--border); +} + +.blade-body { + padding: 20px 24px; + overflow-y: auto; + flex: 1; + display: flex; + flex-direction: column; + gap: 16px; +} + +.blade-footer { + padding: 16px 24px; + border-top: 1px solid var(--border); + background: var(--surface-alt); + display: flex; + justify-content: space-between; + align-items: center; + gap: 12px; +} +``` + +--- + +### 5.5 Modal System Standardization + +Modals must strictly obey: +1. **Backdrop**: `rgba(15, 23, 42, 0.5)` with blur. +2. **Animation**: Scale and slide `transform: scale(0.96) translateY(12px) -> scale(1) translateY(0)`. +3. **Cancel Buttons**: ALWAYS use `.btn-secondary` (white background, gray border, dark text). NEVER use solid green or bare text links for Cancel actions. +4. **Form Controls**: Full width, `8px` radius, focus ring with brand glow. +5. **Layering with Blades**: When a modal is triggered from inside a 360 blade, modal `z-index` is `300` and overlay `z-index` is `290` so it rests naturally above the blade (`z-index: 260`). + +```css +.modal-overlay { + position: fixed; + inset: 0; + background: rgba(15, 23, 42, 0.5); + backdrop-filter: blur(3px); + display: flex; + align-items: center; + justify-content: center; + z-index: 300; + padding: 16px; +} + +.modal { + background: var(--surface); + border-radius: var(--radius-xl); + padding: 24px; + width: min(580px, 94vw); + max-height: 88vh; + overflow-y: auto; + box-shadow: var(--shadow-xl); + border: 1px solid var(--border); + animation: modalPop 0.2s cubic-bezier(0.16, 1, 0.3, 1); +} + +@keyframes modalPop { + from { opacity: 0; transform: scale(0.96) translateY(8px); } + to { opacity: 1; transform: scale(1) translateY(0); } +} + +.modal-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 20px; + padding-bottom: 12px; + border-bottom: 1px solid var(--border); +} + +.modal-footer { + display: flex; + justify-content: flex-end; + align-items: center; + gap: 12px; + margin-top: 24px; + padding-top: 16px; + border-top: 1px solid var(--border); +} +``` + +--- + +## 6. Comprehensive Component Standardization Matrix + +| Component | Standard Specifications | Rules & Prohibitions | +|---|---|---| +| **Buttons** | `border-radius: 8px; font-weight: 600; padding: 8px 16px; font-size: 0.88rem;` | 🚫 Never use unstyled raw text for cancel. 🚫 Never make Cancel buttons solid green. | +| **Inputs & Selects** | `border-radius: 8px; border: 1px solid var(--border); padding: 9px 12px; font-size: 0.9rem;` | 🚫 Never use plain browser default outlines. Always use focus ring `0 0 0 3px var(--brand-glow)`. | +| **Status Badges** | `border-radius: 9999px; font-size: 0.72rem; font-weight: 700; padding: 2px 8px;` | 🚫 Do not mix raw hardcoded hex codes. Use standard semantic soft tint background + dark text. | +| **Digital Passes** | `background: #0f172a; color: #ffffff; border-radius: 12px; padding: 18px; box-shadow: var(--shadow-md);` | 🚫 Must contain clear high-contrast white barcode card container with monospace ID and print CTA. | +| **Grid Cards** | `border-radius: 12px; border: 1px solid var(--border); padding: 16px; background: var(--surface);` | 🚫 Always maintain uniform card heights and bottom-aligned action button rows. | +| **Data Tables** | `border-collapse: collapse; font-size: 0.88rem; th { text-transform: uppercase; font-size: 0.75rem; letter-spacing: 0.05em; }` | 🚫 Always wrap in `.table-wrap` card container with subtle hover row background. | +| **Flyout Blades** | `width: min(540px, 100vw); z-index: 260; backdrop-filter: blur(3px); transform transition 0.28s;` | 🚫 Must contain Sticky Header, Scrollable Body, and Sticky Bottom Action Footer. | +| **Confirmation Modals** | `max-width: 440px; text-align: center for icon and warning copy; flex-end actions;` | 🚫 Always place Cancel on left/neutral and Destructive action on right in solid red. | + +--- + +## 7. Implementation Roadmap & Verification + +1. **Tokens Integration**: All semantic tokens, radii, shadows, and status colors consolidated into `tokens.css` and `components.css`. +2. **Elimination of Rogue Styles**: + - Replaced purple button styles in refund modal with standardized financial action buttons. + - Fixed Cancel buttons across modals to use uniform `.btn-secondary`. + - Harmonized Table Action buttons between Catalog, Suppliers, Loyalty, and Invoices. + - Unified View Toggle ordering (`Grid` -> `Table` -> `Ledger`). + - Fixed Supplier Registration Form notes textarea layout. +3. **Responsive Breakpoints**: + - `Desktop (> 1024px)`: Multi-column grid, full flyout blade (`540px`), full table columns. + - `Tablet (768px - 1024px)`: 2-column grid, compact blade (`480px`), horizontally scrollable table wrapper. + - `Mobile (< 768px)`: 1-column stack, full-screen blade (`100vw`), condensed cards. + +--- +*ClexAn Foods Design System — Operations Console UI/UX Engineering Specification* From 48cd40908bc0f6cc2e2820e4818101b2f2009927 Mon Sep 17 00:00:00 2001 From: Alain Kimbu Date: Mon, 24 Aug 2026 14:08:49 +0100 Subject: [PATCH 3/3] =?UTF-8?q?Phase=201:=20Small=20UI=20fixes=20=E2=80=94?= =?UTF-8?q?=20POS=20JS=20compatibility=20shim,=20CTA=20token=20standardiza?= =?UTF-8?q?tion,=20aria-labels=20for=20icon=20buttons?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Make POS amountTendered shim tolerant of global declarations to avoid duplicate identifier runtime errors - Standardize primary CTA usage to design token class utton-primary on critical pages (POS, VerifyContact, Branch Admin/Dashboard, Communication Logs) - Add aria-label to icon-only close buttons in POS, Stock Transfers, and Suppliers Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- Store.UI/Pages/BranchAdmin.cshtml | 4 +- Store.UI/Pages/BranchDashboard.cshtml | 2 +- Store.UI/Pages/CommunicationLogs.cshtml | 2 +- Store.UI/Pages/Pos.cshtml | 9 +- Store.UI/Pages/StockTransfers.cshtml | 2 +- Store.UI/Pages/Suppliers.cshtml | 6 +- Store.UI/Pages/VerifyContact.cshtml | 2 +- docs/design_consistency_report.md | 213 ++++++++++++++++++++++++ 8 files changed, 227 insertions(+), 13 deletions(-) create mode 100644 docs/design_consistency_report.md diff --git a/Store.UI/Pages/BranchAdmin.cshtml b/Store.UI/Pages/BranchAdmin.cshtml index 5259887..62cd9e0 100644 --- a/Store.UI/Pages/BranchAdmin.cshtml +++ b/Store.UI/Pages/BranchAdmin.cshtml @@ -40,7 +40,7 @@
- +
@@ -120,7 +120,7 @@
- +
diff --git a/Store.UI/Pages/BranchDashboard.cshtml b/Store.UI/Pages/BranchDashboard.cshtml index 8d1608c..ced46bc 100644 --- a/Store.UI/Pages/BranchDashboard.cshtml +++ b/Store.UI/Pages/BranchDashboard.cshtml @@ -39,7 +39,7 @@
- +
diff --git a/Store.UI/Pages/CommunicationLogs.cshtml b/Store.UI/Pages/CommunicationLogs.cshtml index e10d246..38cb659 100644 --- a/Store.UI/Pages/CommunicationLogs.cshtml +++ b/Store.UI/Pages/CommunicationLogs.cshtml @@ -30,7 +30,7 @@
- + Clear
diff --git a/Store.UI/Pages/Pos.cshtml b/Store.UI/Pages/Pos.cshtml index bfa8d1f..25256d2 100644 --- a/Store.UI/Pages/Pos.cshtml +++ b/Store.UI/Pages/Pos.cshtml @@ -61,7 +61,7 @@
- +
@@ -146,7 +146,7 @@
@Html.AntiForgeryToken() - +