first commit
CI/CD / CI · Admin API (dotnet build) (push) Successful in 41s
CI/CD / CI · Admin Web (tsc) (push) Failing after 5s
CI/CD / CI · Website (tsc) (push) Failing after 4s
CI/CD / CI · Koja (tsc) (push) Failing after 5s
CI/CD / CI · API (dotnet build + test) (push) Successful in 1m13s
CI/CD / CI · Dashboard (tsc) (push) Failing after 2m32s
CI/CD / Deploy · all services (push) Has been skipped

This commit is contained in:
soroush.asadi
2026-05-31 11:06:24 +03:30
parent 51e422272d
commit 345ae0a4b5
69 changed files with 11964 additions and 152 deletions
+61 -11
View File
@@ -1,5 +1,6 @@
using System.Text.Json;
using Meezi.Core.Entities;
using Meezi.Core.Interfaces;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.ChangeTracking;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
@@ -8,8 +9,22 @@ namespace Meezi.Infrastructure.Data;
public class AppDbContext : DbContext
{
public AppDbContext(DbContextOptions<AppDbContext> options) : base(options)
// Strict branch isolation. When an active branch scope is present (a
// branch-scoped staff session), every branch-owned entity is filtered to that
// branch at the DB layer — independent of, and backing up, controller checks.
// Café-wide sessions (Owner / "all branches") and non-HTTP contexts (migrations,
// background jobs, seeders) leave the scope empty so nothing is filtered.
private readonly string? _branchScopeId;
private readonly bool _branchScoped;
public AppDbContext(DbContextOptions<AppDbContext> options, IBranchContext? branch = null)
: base(options)
{
if (branch is { HasBranch: true })
{
_branchScopeId = branch.BranchId;
_branchScoped = true;
}
}
public DbSet<Cafe> Cafes => Set<Cafe>();
@@ -17,6 +32,7 @@ public class AppDbContext : DbContext
public DbSet<Table> Tables => Set<Table>();
public DbSet<TableSection> TableSections => Set<TableSection>();
public DbSet<Employee> Employees => Set<Employee>();
public DbSet<EmployeeBranchRole> EmployeeBranchRoles => Set<EmployeeBranchRole>();
public DbSet<MenuCategory> MenuCategories => Set<MenuCategory>();
public DbSet<MenuItem> MenuItems => Set<MenuItem>();
public DbSet<BranchMenuItemOverride> BranchMenuItemOverrides => Set<BranchMenuItemOverride>();
@@ -63,6 +79,9 @@ public class AppDbContext : DbContext
// Push notifications (Pushe)
public DbSet<PushDevice> PushDevices => Set<PushDevice>();
// Immutable audit trail of sensitive POS / management actions.
public DbSet<AuditLog> AuditLogs => Set<AuditLog>();
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
@@ -120,7 +139,7 @@ public class AppDbContext : DbContext
e.HasIndex(x => new { x.BranchId, x.Name });
e.HasIndex(x => x.CafeId);
e.HasOne(x => x.Branch).WithMany(b => b.Sections).HasForeignKey(x => x.BranchId).OnDelete(DeleteBehavior.Cascade);
e.HasQueryFilter(x => x.DeletedAt == null);
e.HasQueryFilter(x => x.DeletedAt == null && (!_branchScoped || x.BranchId == _branchScopeId));
});
modelBuilder.Entity<Table>(e =>
@@ -134,7 +153,7 @@ public class AppDbContext : DbContext
e.HasOne(x => x.Cafe).WithMany(c => c.Tables).HasForeignKey(x => x.CafeId).OnDelete(DeleteBehavior.Cascade);
e.HasOne(x => x.Branch).WithMany(b => b.Tables).HasForeignKey(x => x.BranchId).OnDelete(DeleteBehavior.Restrict);
e.HasOne(x => x.Section).WithMany(s => s.Tables).HasForeignKey(x => x.SectionId).OnDelete(DeleteBehavior.SetNull);
e.HasQueryFilter(x => x.DeletedAt == null);
e.HasQueryFilter(x => x.DeletedAt == null && (!_branchScoped || x.BranchId == _branchScopeId));
});
modelBuilder.Entity<Employee>(e =>
@@ -149,6 +168,37 @@ public class AppDbContext : DbContext
e.HasQueryFilter(x => x.DeletedAt == null);
});
modelBuilder.Entity<EmployeeBranchRole>(e =>
{
e.HasKey(x => x.Id);
e.HasIndex(x => new { x.EmployeeId, x.BranchId })
.IsUnique()
.HasFilter("\"DeletedAt\" IS NULL");
e.HasIndex(x => new { x.CafeId, x.BranchId });
e.HasOne(x => x.Employee).WithMany(emp => emp.BranchRoles)
.HasForeignKey(x => x.EmployeeId).OnDelete(DeleteBehavior.Cascade);
e.HasOne(x => x.Branch).WithMany(b => b.StaffRoles)
.HasForeignKey(x => x.BranchId).OnDelete(DeleteBehavior.Cascade);
e.HasQueryFilter(x => x.DeletedAt == null);
});
modelBuilder.Entity<AuditLog>(e =>
{
e.HasKey(x => x.Id);
e.Property(x => x.Category).HasMaxLength(64).IsRequired();
e.Property(x => x.Action).HasMaxLength(96).IsRequired();
e.Property(x => x.EntityType).HasMaxLength(64);
e.Property(x => x.EntityId).HasMaxLength(64);
e.Property(x => x.ActorName).HasMaxLength(160);
e.Property(x => x.ActorRole).HasMaxLength(32);
e.Property(x => x.Summary).HasMaxLength(500).IsRequired();
e.HasIndex(x => new { x.CafeId, x.Category });
e.HasIndex(x => new { x.CafeId, x.BranchId });
e.HasIndex(x => new { x.CafeId, x.CreatedAt });
e.HasOne<Cafe>().WithMany().HasForeignKey(x => x.CafeId).OnDelete(DeleteBehavior.Cascade);
e.HasQueryFilter(x => x.DeletedAt == null && (!_branchScoped || x.BranchId == _branchScopeId));
});
modelBuilder.Entity<MenuCategory>(e =>
{
e.HasKey(x => x.Id);
@@ -180,7 +230,7 @@ public class AppDbContext : DbContext
e.HasIndex(x => x.CafeId);
e.HasOne(x => x.Branch).WithMany().HasForeignKey(x => x.BranchId).OnDelete(DeleteBehavior.Cascade);
e.HasOne(x => x.MenuItem).WithMany(m => m.BranchOverrides).HasForeignKey(x => x.MenuItemId).OnDelete(DeleteBehavior.Cascade);
e.HasQueryFilter(x => x.DeletedAt == null);
e.HasQueryFilter(x => x.DeletedAt == null && (!_branchScoped || x.BranchId == _branchScopeId));
});
modelBuilder.Entity<Order>(e =>
@@ -204,7 +254,7 @@ public class AppDbContext : DbContext
e.HasOne(x => x.Customer).WithMany(c => c.Orders).HasForeignKey(x => x.CustomerId).OnDelete(DeleteBehavior.SetNull);
e.HasOne(x => x.Employee).WithMany(emp => emp.Orders).HasForeignKey(x => x.EmployeeId).OnDelete(DeleteBehavior.SetNull);
e.HasOne(x => x.Coupon).WithMany(c => c.Orders).HasForeignKey(x => x.CouponId).OnDelete(DeleteBehavior.SetNull);
e.HasQueryFilter(x => x.DeletedAt == null);
e.HasQueryFilter(x => x.DeletedAt == null && (!_branchScoped || x.BranchId == _branchScopeId));
});
modelBuilder.Entity<OrderItem>(e =>
@@ -287,7 +337,7 @@ public class AppDbContext : DbContext
e.HasOne(x => x.Branch).WithMany().HasForeignKey(x => x.BranchId).OnDelete(DeleteBehavior.Cascade);
e.HasOne(x => x.OpenedBy).WithMany().HasForeignKey(x => x.OpenedByUserId).OnDelete(DeleteBehavior.Restrict);
e.HasOne(x => x.ClosedBy).WithMany().HasForeignKey(x => x.ClosedByUserId).OnDelete(DeleteBehavior.SetNull);
e.HasQueryFilter(x => x.DeletedAt == null);
e.HasQueryFilter(x => x.DeletedAt == null && (!_branchScoped || x.BranchId == _branchScopeId));
});
modelBuilder.Entity<CashTransaction>(e =>
@@ -298,7 +348,7 @@ public class AppDbContext : DbContext
e.HasIndex(x => new { x.CafeId, x.BranchId });
e.HasOne(x => x.Shift).WithMany(s => s.Transactions).HasForeignKey(x => x.ShiftId).OnDelete(DeleteBehavior.Cascade);
e.HasOne(x => x.Branch).WithMany().HasForeignKey(x => x.BranchId).OnDelete(DeleteBehavior.SetNull);
e.HasQueryFilter(x => x.DeletedAt == null);
e.HasQueryFilter(x => x.DeletedAt == null && (!_branchScoped || x.BranchId == _branchScopeId));
});
modelBuilder.Entity<LeaveRequest>(e =>
@@ -353,7 +403,7 @@ public class AppDbContext : DbContext
e.HasIndex(x => new { x.CafeId, x.SortOrder });
e.HasOne(x => x.Cafe).WithMany().HasForeignKey(x => x.CafeId).OnDelete(DeleteBehavior.Cascade);
e.HasOne(x => x.Branch).WithMany().HasForeignKey(x => x.BranchId).OnDelete(DeleteBehavior.SetNull);
e.HasQueryFilter(x => x.DeletedAt == null);
e.HasQueryFilter(x => x.DeletedAt == null && (!_branchScoped || x.BranchId == _branchScopeId));
});
modelBuilder.Entity<SubscriptionPayment>(e =>
@@ -414,7 +464,7 @@ public class AppDbContext : DbContext
e.HasOne(x => x.Cafe).WithMany().HasForeignKey(x => x.CafeId).OnDelete(DeleteBehavior.Cascade);
e.HasOne(x => x.Branch).WithMany().HasForeignKey(x => x.BranchId).OnDelete(DeleteBehavior.SetNull);
e.HasOne(x => x.Order).WithMany().HasForeignKey(x => x.OrderId).OnDelete(DeleteBehavior.SetNull);
e.HasQueryFilter(x => x.DeletedAt == null);
e.HasQueryFilter(x => x.DeletedAt == null && (!_branchScoped || x.BranchId == _branchScopeId));
});
modelBuilder.Entity<Expense>(e =>
@@ -426,7 +476,7 @@ public class AppDbContext : DbContext
e.HasIndex(x => new { x.CafeId, x.BranchId, x.CreatedAt });
e.HasOne(x => x.Branch).WithMany().HasForeignKey(x => x.BranchId).OnDelete(DeleteBehavior.Cascade);
e.HasOne(x => x.Shift).WithMany().HasForeignKey(x => x.ShiftId).OnDelete(DeleteBehavior.SetNull);
e.HasQueryFilter(x => x.DeletedAt == null);
e.HasQueryFilter(x => x.DeletedAt == null && (!_branchScoped || x.BranchId == _branchScopeId));
});
modelBuilder.Entity<DailyReport>(e =>
@@ -457,7 +507,7 @@ public class AppDbContext : DbContext
.HasConversion(topProductsConverter, topProductsComparer)
.HasColumnType("jsonb");
e.HasOne(x => x.Branch).WithMany().HasForeignKey(x => x.BranchId).OnDelete(DeleteBehavior.Cascade);
e.HasQueryFilter(x => x.DeletedAt == null);
e.HasQueryFilter(x => x.DeletedAt == null && (!_branchScoped || x.BranchId == _branchScopeId));
});
modelBuilder.Entity<WebhookLog>(e =>
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,83 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Meezi.Infrastructure.Data.Migrations
{
/// <inheritdoc />
public partial class AddEmployeeBranchRole : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "EmployeeBranchRoles",
columns: table => new
{
Id = table.Column<string>(type: "text", nullable: false),
EmployeeId = table.Column<string>(type: "text", nullable: false),
BranchId = table.Column<string>(type: "text", nullable: false),
Role = table.Column<int>(type: "integer", nullable: false),
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
DeletedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
CafeId = table.Column<string>(type: "text", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_EmployeeBranchRoles", x => x.Id);
table.ForeignKey(
name: "FK_EmployeeBranchRoles_Branches_BranchId",
column: x => x.BranchId,
principalTable: "Branches",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_EmployeeBranchRoles_Employees_EmployeeId",
column: x => x.EmployeeId,
principalTable: "Employees",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_EmployeeBranchRoles_BranchId",
table: "EmployeeBranchRoles",
column: "BranchId");
migrationBuilder.CreateIndex(
name: "IX_EmployeeBranchRoles_CafeId_BranchId",
table: "EmployeeBranchRoles",
columns: new[] { "CafeId", "BranchId" });
migrationBuilder.CreateIndex(
name: "IX_EmployeeBranchRoles_EmployeeId_BranchId",
table: "EmployeeBranchRoles",
columns: new[] { "EmployeeId", "BranchId" },
unique: true,
filter: "\"DeletedAt\" IS NULL");
// Backfill: every existing branch-pinned, non-owner employee gets an
// explicit per-branch role row mirroring their current (BranchId, Role).
// Owners (Role = 0) and café-wide non-pinned staff (BranchId IS NULL) are
// left untouched — they remain café-wide via Employee.Role.
migrationBuilder.Sql(@"
INSERT INTO ""EmployeeBranchRoles""
(""Id"", ""EmployeeId"", ""BranchId"", ""Role"", ""CafeId"", ""CreatedAt"")
SELECT replace(gen_random_uuid()::text, '-', ''),
e.""Id"", e.""BranchId"", e.""Role"", e.""CafeId"", now()
FROM ""Employees"" e
WHERE e.""BranchId"" IS NOT NULL
AND e.""DeletedAt"" IS NULL
AND e.""Role"" <> 0;
");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "EmployeeBranchRoles");
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,67 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Meezi.Infrastructure.Data.Migrations
{
/// <inheritdoc />
public partial class AddAuditLog : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "AuditLogs",
columns: table => new
{
Id = table.Column<string>(type: "text", nullable: false),
Category = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false),
Action = table.Column<string>(type: "character varying(96)", maxLength: 96, nullable: false),
EntityType = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: true),
EntityId = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: true),
BranchId = table.Column<string>(type: "text", nullable: true),
ActorId = table.Column<string>(type: "text", nullable: true),
ActorName = table.Column<string>(type: "character varying(160)", maxLength: 160, nullable: true),
ActorRole = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: true),
Summary = table.Column<string>(type: "character varying(500)", maxLength: 500, nullable: false),
DetailsJson = table.Column<string>(type: "text", nullable: true),
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
DeletedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
CafeId = table.Column<string>(type: "text", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_AuditLogs", x => x.Id);
table.ForeignKey(
name: "FK_AuditLogs_Cafes_CafeId",
column: x => x.CafeId,
principalTable: "Cafes",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_AuditLogs_CafeId_BranchId",
table: "AuditLogs",
columns: new[] { "CafeId", "BranchId" });
migrationBuilder.CreateIndex(
name: "IX_AuditLogs_CafeId_Category",
table: "AuditLogs",
columns: new[] { "CafeId", "Category" });
migrationBuilder.CreateIndex(
name: "IX_AuditLogs_CafeId_CreatedAt",
table: "AuditLogs",
columns: new[] { "CafeId", "CreatedAt" });
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "AuditLogs");
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,49 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Meezi.Infrastructure.Data.Migrations
{
/// <inheritdoc />
public partial class AddOrderCancellationFields : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "CancelReason",
table: "Orders",
type: "text",
nullable: true);
migrationBuilder.AddColumn<DateTime>(
name: "CancelledAt",
table: "Orders",
type: "timestamp with time zone",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "CancelledByEmployeeId",
table: "Orders",
type: "text",
nullable: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "CancelReason",
table: "Orders");
migrationBuilder.DropColumn(
name: "CancelledAt",
table: "Orders");
migrationBuilder.DropColumn(
name: "CancelledByEmployeeId",
table: "Orders");
}
}
}
@@ -57,6 +57,72 @@ namespace Meezi.Infrastructure.Data.Migrations
b.ToTable("Attendances");
});
modelBuilder.Entity("Meezi.Core.Entities.AuditLog", b =>
{
b.Property<string>("Id")
.HasColumnType("text");
b.Property<string>("Action")
.IsRequired()
.HasMaxLength(96)
.HasColumnType("character varying(96)");
b.Property<string>("ActorId")
.HasColumnType("text");
b.Property<string>("ActorName")
.HasMaxLength(160)
.HasColumnType("character varying(160)");
b.Property<string>("ActorRole")
.HasMaxLength(32)
.HasColumnType("character varying(32)");
b.Property<string>("BranchId")
.HasColumnType("text");
b.Property<string>("CafeId")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Category")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<DateTime?>("DeletedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("DetailsJson")
.HasColumnType("text");
b.Property<string>("EntityId")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("EntityType")
.HasMaxLength(64)
.HasColumnType("character varying(64)");
b.Property<string>("Summary")
.IsRequired()
.HasMaxLength(500)
.HasColumnType("character varying(500)");
b.HasKey("Id");
b.HasIndex("CafeId", "BranchId");
b.HasIndex("CafeId", "Category");
b.HasIndex("CafeId", "CreatedAt");
b.ToTable("AuditLogs");
});
modelBuilder.Entity("Meezi.Core.Entities.Branch", b =>
{
b.Property<string>("Id")
@@ -884,6 +950,45 @@ namespace Meezi.Infrastructure.Data.Migrations
b.ToTable("Employees");
});
modelBuilder.Entity("Meezi.Core.Entities.EmployeeBranchRole", b =>
{
b.Property<string>("Id")
.HasColumnType("text");
b.Property<string>("BranchId")
.IsRequired()
.HasColumnType("text");
b.Property<string>("CafeId")
.IsRequired()
.HasColumnType("text");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<DateTime?>("DeletedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("EmployeeId")
.IsRequired()
.HasColumnType("text");
b.Property<int>("Role")
.HasColumnType("integer");
b.HasKey("Id");
b.HasIndex("BranchId");
b.HasIndex("CafeId", "BranchId");
b.HasIndex("EmployeeId", "BranchId")
.IsUnique()
.HasFilter("\"DeletedAt\" IS NULL");
b.ToTable("EmployeeBranchRoles");
});
modelBuilder.Entity("Meezi.Core.Entities.EmployeeSalary", b =>
{
b.Property<string>("Id")
@@ -1317,6 +1422,15 @@ namespace Meezi.Infrastructure.Data.Migrations
.IsRequired()
.HasColumnType("text");
b.Property<string>("CancelReason")
.HasColumnType("text");
b.Property<DateTime?>("CancelledAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("CancelledByEmployeeId")
.HasColumnType("text");
b.Property<string>("CouponId")
.HasColumnType("text");
@@ -2424,6 +2538,15 @@ namespace Meezi.Infrastructure.Data.Migrations
b.Navigation("Employee");
});
modelBuilder.Entity("Meezi.Core.Entities.AuditLog", b =>
{
b.HasOne("Meezi.Core.Entities.Cafe", null)
.WithMany()
.HasForeignKey("CafeId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Meezi.Core.Entities.Branch", b =>
{
b.HasOne("Meezi.Core.Entities.Cafe", "Cafe")
@@ -2565,6 +2688,25 @@ namespace Meezi.Infrastructure.Data.Migrations
b.Navigation("Cafe");
});
modelBuilder.Entity("Meezi.Core.Entities.EmployeeBranchRole", b =>
{
b.HasOne("Meezi.Core.Entities.Branch", "Branch")
.WithMany("StaffRoles")
.HasForeignKey("BranchId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Meezi.Core.Entities.Employee", "Employee")
.WithMany("BranchRoles")
.HasForeignKey("EmployeeId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Branch");
b.Navigation("Employee");
});
modelBuilder.Entity("Meezi.Core.Entities.EmployeeSalary", b =>
{
b.HasOne("Meezi.Core.Entities.Employee", "Employee")
@@ -3012,6 +3154,8 @@ namespace Meezi.Infrastructure.Data.Migrations
b.Navigation("Staff");
b.Navigation("StaffRoles");
b.Navigation("Tables");
});
@@ -3061,6 +3205,8 @@ namespace Meezi.Infrastructure.Data.Migrations
{
b.Navigation("Attendances");
b.Navigation("BranchRoles");
b.Navigation("LeaveRequests");
b.Navigation("Orders");