Security (P0-5): - Implement ITenantProvider + HttpContextTenantProvider per service (order, fnb, inventory, catalog, wallet) - Add EF Core global query filters for tenant isolation (shop_id/user_id based) - Add TenantMiddleware setting PostgreSQL session variables for RLS - Create PostgreSQL RLS policies script (scripts/db/rls-policies.sql) - Adapter pattern bridges API-layer to Infrastructure-layer (Clean Architecture) - Bypass mechanisms for admin roles, service-to-service calls, and migrations Testing (P1-12): - Add 96 unit tests for fnb-engine (up from 3) - 57 domain entity tests: Table(18), KitchenTicket(12), Session(8), Reservation(13), Recipe(6) - 39 command handler tests: CRUD operations, status transitions, validation Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
177 lines
6.1 KiB
C#
177 lines
6.1 KiB
C#
namespace WalletService.Infrastructure;
|
|
|
|
using MediatR;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using Microsoft.EntityFrameworkCore.Storage;
|
|
using WalletService.Domain.AggregatesModel.PaymentAggregate;
|
|
using WalletService.Domain.AggregatesModel.PointAccountAggregate;
|
|
using WalletService.Domain.AggregatesModel.WalletAggregate;
|
|
using WalletService.Domain.SeedWork;
|
|
|
|
/// <summary>
|
|
/// EN: Tenant provider interface for WalletServiceContext global query filters.
|
|
/// Wallet service uses user_id as primary tenant key (wallets are per-user).
|
|
/// VI: Interface tenant provider cho global query filters của WalletServiceContext.
|
|
/// Wallet service sử dụng user_id làm tenant key chính (ví là per-user).
|
|
/// </summary>
|
|
public interface IWalletTenantProvider
|
|
{
|
|
Guid? GetCurrentUserId();
|
|
bool ShouldBypassTenantFilter();
|
|
}
|
|
|
|
/// <summary>
|
|
/// EN: Database context for Wallet Service with Unit of Work pattern and multi-tenant filtering.
|
|
/// VI: Database context cho Wallet Service với pattern Unit of Work và filtering đa tenant.
|
|
/// </summary>
|
|
public class WalletServiceContext : DbContext, IUnitOfWork
|
|
{
|
|
private readonly IMediator _mediator;
|
|
private readonly IWalletTenantProvider? _tenantProvider;
|
|
private IDbContextTransaction? _currentTransaction;
|
|
|
|
public DbSet<Wallet> Wallets { get; set; } = null!;
|
|
public DbSet<WalletItem> WalletItems { get; set; } = null!;
|
|
public DbSet<WalletTransaction> WalletTransactions { get; set; } = null!;
|
|
public DbSet<HoldItem> WalletHolds { get; set; } = null!;
|
|
public DbSet<Payment> Payments { get; set; } = null!;
|
|
public DbSet<PointAccount> PointAccounts { get; set; } = null!;
|
|
public DbSet<PointTransaction> PointTransactions { get; set; } = null!;
|
|
|
|
public bool HasActiveTransaction => _currentTransaction != null;
|
|
|
|
public WalletServiceContext(DbContextOptions<WalletServiceContext> options, IMediator mediator)
|
|
: base(options)
|
|
{
|
|
_mediator = mediator ?? throw new ArgumentNullException(nameof(mediator));
|
|
}
|
|
|
|
/// <summary>
|
|
/// EN: Constructor with tenant provider for multi-tenant filtering.
|
|
/// VI: Constructor với tenant provider cho filtering đa tenant.
|
|
/// </summary>
|
|
public WalletServiceContext(
|
|
DbContextOptions<WalletServiceContext> options,
|
|
IMediator mediator,
|
|
IWalletTenantProvider tenantProvider)
|
|
: base(options)
|
|
{
|
|
_mediator = mediator ?? throw new ArgumentNullException(nameof(mediator));
|
|
_tenantProvider = tenantProvider;
|
|
}
|
|
|
|
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
|
{
|
|
// EN: Apply all entity configurations
|
|
// VI: Áp dụng tất cả các cấu hình entity
|
|
modelBuilder.ApplyConfigurationsFromAssembly(typeof(WalletServiceContext).Assembly);
|
|
|
|
// EN: Global query filter for tenant isolation on Wallets (user-level).
|
|
// Wallets belong to a user, not a shop. Each user can only see their own wallet.
|
|
// VI: Global query filter cho cách ly tenant trên Wallets (cấp user).
|
|
// Wallets thuộc về user, không phải shop. Mỗi user chỉ thấy ví của mình.
|
|
modelBuilder.Entity<Wallet>().HasQueryFilter(w =>
|
|
_tenantProvider == null
|
|
|| _tenantProvider.ShouldBypassTenantFilter()
|
|
|| _tenantProvider.GetCurrentUserId() == null
|
|
|| w.UserId == _tenantProvider.GetCurrentUserId());
|
|
}
|
|
|
|
/// <summary>
|
|
/// EN: Save changes and dispatch domain events
|
|
/// VI: Lưu thay đổi và dispatch domain events
|
|
/// </summary>
|
|
public async Task<bool> SaveEntitiesAsync(CancellationToken cancellationToken = default)
|
|
{
|
|
// EN: Dispatch domain events after saving
|
|
// VI: Dispatch domain events sau khi lưu
|
|
await DispatchDomainEventsAsync();
|
|
|
|
// EN: Save changes to database
|
|
// VI: Lưu thay đổi vào database
|
|
var result = await base.SaveChangesAsync(cancellationToken);
|
|
return result > 0;
|
|
}
|
|
|
|
/// <summary>
|
|
/// EN: Begin a new transaction
|
|
/// VI: Bắt đầu transaction mới
|
|
/// </summary>
|
|
public async Task<IDbContextTransaction?> BeginTransactionAsync()
|
|
{
|
|
if (_currentTransaction != null) return null;
|
|
|
|
_currentTransaction = await Database.BeginTransactionAsync();
|
|
return _currentTransaction;
|
|
}
|
|
|
|
/// <summary>
|
|
/// EN: Commit the current transaction
|
|
/// VI: Commit transaction hiện tại
|
|
/// </summary>
|
|
public async Task CommitTransactionAsync(IDbContextTransaction transaction)
|
|
{
|
|
if (transaction == null) throw new ArgumentNullException(nameof(transaction));
|
|
if (transaction != _currentTransaction)
|
|
throw new InvalidOperationException($"Transaction {transaction.TransactionId} is not current");
|
|
|
|
try
|
|
{
|
|
await SaveChangesAsync();
|
|
await transaction.CommitAsync();
|
|
}
|
|
catch
|
|
{
|
|
RollbackTransaction();
|
|
throw;
|
|
}
|
|
finally
|
|
{
|
|
if (_currentTransaction != null)
|
|
{
|
|
_currentTransaction.Dispose();
|
|
_currentTransaction = null;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// EN: Rollback the current transaction
|
|
/// VI: Rollback transaction hiện tại
|
|
/// </summary>
|
|
public void RollbackTransaction()
|
|
{
|
|
try
|
|
{
|
|
_currentTransaction?.Rollback();
|
|
}
|
|
finally
|
|
{
|
|
if (_currentTransaction != null)
|
|
{
|
|
_currentTransaction.Dispose();
|
|
_currentTransaction = null;
|
|
}
|
|
}
|
|
}
|
|
|
|
private async Task DispatchDomainEventsAsync()
|
|
{
|
|
var domainEntities = ChangeTracker
|
|
.Entries<Entity>()
|
|
.Where(x => x.Entity.DomainEvents.Any())
|
|
.ToList();
|
|
|
|
var domainEvents = domainEntities
|
|
.SelectMany(x => x.Entity.DomainEvents)
|
|
.ToList();
|
|
|
|
domainEntities.ForEach(entity => entity.Entity.ClearDomainEvents());
|
|
|
|
foreach (var domainEvent in domainEvents)
|
|
{
|
|
await _mediator.Publish(domainEvent);
|
|
}
|
|
}
|
|
}
|