diff --git a/services/inventory-service-net/src/InventoryService.API/Application/Behaviors/TransactionBehavior.cs b/services/inventory-service-net/src/InventoryService.API/Application/Behaviors/TransactionBehavior.cs index 3608dbf8..65a23bce 100644 --- a/services/inventory-service-net/src/InventoryService.API/Application/Behaviors/TransactionBehavior.cs +++ b/services/inventory-service-net/src/InventoryService.API/Application/Behaviors/TransactionBehavior.cs @@ -13,11 +13,11 @@ namespace InventoryService.API.Application.Behaviors; public class TransactionBehavior : IPipelineBehavior where TRequest : IRequest { - private readonly InventoryServiceContext _dbContext; + private readonly InventoryContext _dbContext; private readonly ILogger> _logger; public TransactionBehavior( - InventoryServiceContext dbContext, + InventoryContext dbContext, ILogger> logger) { _dbContext = dbContext ?? throw new ArgumentNullException(nameof(dbContext)); diff --git a/services/inventory-service-net/src/InventoryService.API/InventoryService.API.csproj b/services/inventory-service-net/src/InventoryService.API/InventoryService.API.csproj index c6a8247a..a7e87fd5 100644 --- a/services/inventory-service-net/src/InventoryService.API/InventoryService.API.csproj +++ b/services/inventory-service-net/src/InventoryService.API/InventoryService.API.csproj @@ -14,6 +14,10 @@ + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + diff --git a/services/inventory-service-net/src/InventoryService.Infrastructure/DependencyInjection.cs b/services/inventory-service-net/src/InventoryService.Infrastructure/DependencyInjection.cs index 94d2b5f8..82e1a527 100644 --- a/services/inventory-service-net/src/InventoryService.Infrastructure/DependencyInjection.cs +++ b/services/inventory-service-net/src/InventoryService.Infrastructure/DependencyInjection.cs @@ -1,7 +1,7 @@ using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; -using InventoryService.Domain.AggregatesModel.SampleAggregate; +using InventoryService.Domain.AggregatesModel.InventoryAggregate; using InventoryService.Infrastructure.Idempotency; using InventoryService.Infrastructure.Repositories; @@ -22,7 +22,7 @@ public static class DependencyInjection IConfiguration configuration) { // EN: Add DbContext with PostgreSQL / VI: Thêm DbContext với PostgreSQL - services.AddDbContext(options => + services.AddDbContext(options => { var connectionString = configuration.GetConnectionString("DefaultConnection") ?? configuration["DATABASE_URL"] @@ -30,7 +30,7 @@ public static class DependencyInjection options.UseNpgsql(connectionString, npgsqlOptions => { - npgsqlOptions.MigrationsAssembly(typeof(InventoryServiceContext).Assembly.FullName); + npgsqlOptions.MigrationsAssembly(typeof(InventoryContext).Assembly.FullName); npgsqlOptions.EnableRetryOnFailure( maxRetryCount: 5, maxRetryDelay: TimeSpan.FromSeconds(30), @@ -47,7 +47,7 @@ public static class DependencyInjection }); // EN: Register repositories / VI: Đăng ký repositories - services.AddScoped(); + services.AddScoped(); // EN: Register idempotency services / VI: Đăng ký idempotency services services.AddScoped(); diff --git a/services/inventory-service-net/src/InventoryService.Infrastructure/EntityConfigurations/InventoryItemEntityTypeConfiguration.cs b/services/inventory-service-net/src/InventoryService.Infrastructure/EntityConfigurations/InventoryItemEntityTypeConfiguration.cs new file mode 100644 index 00000000..8faf27dc --- /dev/null +++ b/services/inventory-service-net/src/InventoryService.Infrastructure/EntityConfigurations/InventoryItemEntityTypeConfiguration.cs @@ -0,0 +1,58 @@ +// EN: InventoryItem entity configuration. +// VI: Cấu hình entity InventoryItem. + +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using InventoryService.Domain.AggregatesModel.InventoryAggregate; + +namespace InventoryService.Infrastructure.EntityConfigurations; + +public class InventoryItemEntityTypeConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("inventory_items"); + + builder.HasKey(i => i.Id); + builder.Property(i => i.Id).HasColumnName("id").ValueGeneratedNever(); + + builder.Property("_productId").HasColumnName("product_id").IsRequired(); + builder.Property("_shopId").HasColumnName("shop_id").IsRequired(); + builder.Property("_quantityOnHand").HasColumnName("quantity_on_hand").IsRequired(); + builder.Property("_reservedQuantity").HasColumnName("reserved_quantity").IsRequired(); + builder.Property("_reorderLevel").HasColumnName("reorder_level").HasDefaultValue(10); + builder.Property("_updatedAt").HasColumnName("updated_at").IsRequired(); + + // Owned InventoryTransaction collection + builder.OwnsMany(i => i.Transactions, txn => + { + txn.ToTable("inventory_transactions"); + txn.WithOwner().HasForeignKey("InventoryItemId"); + txn.Property("InventoryItemId").HasColumnName("inventory_item_id"); + txn.HasKey(t => t.Id); + txn.Property(t => t.Id).HasColumnName("id").ValueGeneratedNever(); + txn.Property(t => t.TypeId).HasColumnName("type_id").IsRequired(); + txn.Property("_quantity").HasColumnName("quantity").IsRequired(); + txn.Property("_referenceId").HasColumnName("reference_id"); + txn.Property("_notes").HasColumnName("notes").HasMaxLength(500); + txn.Property("_createdAt").HasColumnName("created_at").IsRequired(); + + txn.Ignore(t => t.Type); + txn.Ignore(t => t.Quantity); + txn.Ignore(t => t.ReferenceId); + txn.Ignore(t => t.Notes); + txn.Ignore(t => t.CreatedAt); + }); + + builder.HasIndex("_productId").HasDatabaseName("ix_inventory_product_id"); + builder.HasIndex("_shopId").HasDatabaseName("ix_inventory_shop_id"); + + builder.Ignore(i => i.ProductId); + builder.Ignore(i => i.ShopId); + builder.Ignore(i => i.QuantityOnHand); + builder.Ignore(i => i.ReservedQuantity); + builder.Ignore(i => i.AvailableQuantity); + builder.Ignore(i => i.ReorderLevel); + builder.Ignore(i => i.UpdatedAt); + } +} diff --git a/services/inventory-service-net/src/InventoryService.Infrastructure/EntityConfigurations/TransactionTypeEntityTypeConfiguration.cs b/services/inventory-service-net/src/InventoryService.Infrastructure/EntityConfigurations/TransactionTypeEntityTypeConfiguration.cs new file mode 100644 index 00000000..ede20181 --- /dev/null +++ b/services/inventory-service-net/src/InventoryService.Infrastructure/EntityConfigurations/TransactionTypeEntityTypeConfiguration.cs @@ -0,0 +1,28 @@ +// EN: TransactionType enumeration configuration. +// VI: Cấu hình TransactionType enumeration. + +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using InventoryService.Domain.AggregatesModel.InventoryAggregate; + +namespace InventoryService.Infrastructure.EntityConfigurations; + +public class TransactionTypeEntityTypeConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("transaction_types"); + + builder.HasKey(t => t.Id); + builder.Property(t => t.Id).HasColumnName("id").ValueGeneratedNever(); + builder.Property(t => t.Name).HasColumnName("name").HasMaxLength(50).IsRequired(); + + builder.HasData( + TransactionType.In, + TransactionType.Out, + TransactionType.Adjustment, + TransactionType.Reserve, + TransactionType.Release + ); + } +} diff --git a/services/inventory-service-net/src/InventoryService.Infrastructure/Idempotency/RequestManager.cs b/services/inventory-service-net/src/InventoryService.Infrastructure/Idempotency/RequestManager.cs index fb60d4f0..ba4cbb5f 100644 --- a/services/inventory-service-net/src/InventoryService.Infrastructure/Idempotency/RequestManager.cs +++ b/services/inventory-service-net/src/InventoryService.Infrastructure/Idempotency/RequestManager.cs @@ -8,9 +8,9 @@ namespace InventoryService.Infrastructure.Idempotency; /// public class RequestManager : IRequestManager { - private readonly InventoryServiceContext _context; + private readonly InventoryContext _context; - public RequestManager(InventoryServiceContext context) + public RequestManager(InventoryContext context) { _context = context ?? throw new ArgumentNullException(nameof(context)); } diff --git a/services/inventory-service-net/src/InventoryService.Infrastructure/MyServiceContext.cs b/services/inventory-service-net/src/InventoryService.Infrastructure/InventoryContext.cs similarity index 63% rename from services/inventory-service-net/src/InventoryService.Infrastructure/MyServiceContext.cs rename to services/inventory-service-net/src/InventoryService.Infrastructure/InventoryContext.cs index 287d1a9e..5c3f628f 100644 --- a/services/inventory-service-net/src/InventoryService.Infrastructure/MyServiceContext.cs +++ b/services/inventory-service-net/src/InventoryService.Infrastructure/InventoryContext.cs @@ -1,7 +1,7 @@ using MediatR; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Storage; -using InventoryService.Domain.AggregatesModel.SampleAggregate; +using InventoryService.Domain.AggregatesModel.InventoryAggregate; using InventoryService.Domain.SeedWork; using InventoryService.Infrastructure.EntityConfigurations; @@ -11,16 +11,16 @@ namespace InventoryService.Infrastructure; /// EN: EF Core DbContext for InventoryService. /// VI: EF Core DbContext cho InventoryService. /// -public class InventoryServiceContext : DbContext, IUnitOfWork +public class InventoryContext : DbContext, IUnitOfWork { private readonly IMediator _mediator; private IDbContextTransaction? _currentTransaction; /// - /// EN: Samples table. - /// VI: Bảng Samples. + /// EN: Inventory items table. + /// VI: Bảng Inventory items. /// - public DbSet Samples => Set(); + public DbSet InventoryItems => Set(); /// /// EN: Read-only access to current transaction. @@ -34,64 +34,35 @@ public class InventoryServiceContext : DbContext, IUnitOfWork /// public bool HasActiveTransaction => _currentTransaction != null; - public InventoryServiceContext(DbContextOptions options) : base(options) - { - _mediator = null!; - } - - public InventoryServiceContext(DbContextOptions options, IMediator mediator) : base(options) + public InventoryContext(DbContextOptions options, IMediator mediator) : base(options) { _mediator = mediator ?? throw new ArgumentNullException(nameof(mediator)); - - System.Diagnostics.Debug.WriteLine("InventoryServiceContext::ctor - " + GetHashCode()); + System.Diagnostics.Debug.WriteLine("InventoryContext::ctor - " + GetHashCode()); } protected override void OnModelCreating(ModelBuilder modelBuilder) { - // EN: Apply entity configurations - // VI: Áp dụng các cấu hình entity - modelBuilder.ApplyConfiguration(new SampleEntityTypeConfiguration()); - modelBuilder.ApplyConfiguration(new SampleStatusEntityTypeConfiguration()); + modelBuilder.ApplyConfiguration(new InventoryItemEntityTypeConfiguration()); + modelBuilder.ApplyConfiguration(new TransactionTypeEntityTypeConfiguration()); } - /// - /// EN: Save entities and dispatch domain events. - /// VI: Lưu entities và dispatch domain events. - /// public async Task SaveEntitiesAsync(CancellationToken cancellationToken = default) { - // EN: Dispatch domain events before saving (side effects) - // VI: Dispatch domain events trước khi lưu (side effects) await DispatchDomainEventsAsync(); - - // EN: Save changes to database - // VI: Lưu thay đổi vào database await base.SaveChangesAsync(cancellationToken); - return true; } - /// - /// EN: Begin a new transaction if none is active. - /// VI: Bắt đầu một transaction mới nếu không có transaction nào đang hoạt động. - /// public async Task BeginTransactionAsync() { if (_currentTransaction != null) return null; - _currentTransaction = await Database.BeginTransactionAsync(System.Data.IsolationLevel.ReadCommitted); - return _currentTransaction; } - /// - /// EN: Commit the current transaction. - /// VI: Commit transaction hiện tại. - /// public async Task CommitTransactionAsync(IDbContextTransaction transaction) { ArgumentNullException.ThrowIfNull(transaction); - if (transaction != _currentTransaction) throw new InvalidOperationException($"Transaction {transaction.TransactionId} is not current"); @@ -115,10 +86,6 @@ public class InventoryServiceContext : DbContext, IUnitOfWork } } - /// - /// EN: Rollback the current transaction. - /// VI: Rollback transaction hiện tại. - /// public void RollbackTransaction() { try @@ -135,10 +102,6 @@ public class InventoryServiceContext : DbContext, IUnitOfWork } } - /// - /// EN: Dispatch all domain events from tracked entities. - /// VI: Dispatch tất cả domain events từ các entities đang được track. - /// private async Task DispatchDomainEventsAsync() { var domainEntities = ChangeTracker diff --git a/services/inventory-service-net/src/InventoryService.Infrastructure/Repositories/InventoryRepository.cs b/services/inventory-service-net/src/InventoryService.Infrastructure/Repositories/InventoryRepository.cs new file mode 100644 index 00000000..cc9d19cf --- /dev/null +++ b/services/inventory-service-net/src/InventoryService.Infrastructure/Repositories/InventoryRepository.cs @@ -0,0 +1,44 @@ +using InventoryService.Domain.AggregatesModel.InventoryAggregate; +using InventoryService.Domain.SeedWork; +using Microsoft.EntityFrameworkCore; + +namespace InventoryService.Infrastructure.Repositories; + +public class InventoryRepository : IInventoryRepository +{ + private readonly InventoryContext _context; + public IUnitOfWork UnitOfWork => _context; + + public InventoryRepository(InventoryContext context) + { + _context = context ?? throw new ArgumentNullException(nameof(context)); + } + + public InventoryItem Add(InventoryItem item) + { + return _context.InventoryItems.Add(item).Entity; + } + + public void Update(InventoryItem item) + { + _context.Entry(item).State = EntityState.Modified; + } + + public async Task GetByIdAsync(Guid id, CancellationToken cancellationToken = default) + { + return await _context.InventoryItems.FirstOrDefaultAsync(i => i.Id == id, cancellationToken); + } + + public async Task GetByProductIdAsync(Guid productId, Guid shopId, CancellationToken cancellationToken = default) + { + return await _context.InventoryItems + .FirstOrDefaultAsync(i => i.ProductId == productId && i.ShopId == shopId, cancellationToken); + } + + public async Task> GetByShopIdAsync(Guid shopId, CancellationToken cancellationToken = default) + { + return await _context.InventoryItems + .Where(i => i.ShopId == shopId) + .ToListAsync(cancellationToken); + } +} diff --git a/services/inventory-service-net/tests/InventoryService.FunctionalTests/CustomWebApplicationFactory.cs b/services/inventory-service-net/tests/InventoryService.FunctionalTests/CustomWebApplicationFactory.cs index b3efcfc0..1556d62f 100644 --- a/services/inventory-service-net/tests/InventoryService.FunctionalTests/CustomWebApplicationFactory.cs +++ b/services/inventory-service-net/tests/InventoryService.FunctionalTests/CustomWebApplicationFactory.cs @@ -21,7 +21,7 @@ public class CustomWebApplicationFactory : WebApplicationFactory // EN: Remove the existing DbContext registration // VI: Xóa đăng ký DbContext hiện tại var descriptor = services.SingleOrDefault( - d => d.ServiceType == typeof(DbContextOptions)); + d => d.ServiceType == typeof(DbContextOptions)); if (descriptor != null) { @@ -31,7 +31,7 @@ public class CustomWebApplicationFactory : WebApplicationFactory // EN: Remove DbContext service // VI: Xóa DbContext service var dbContextDescriptor = services.SingleOrDefault( - d => d.ServiceType == typeof(InventoryServiceContext)); + d => d.ServiceType == typeof(InventoryContext)); if (dbContextDescriptor != null) { @@ -40,7 +40,7 @@ public class CustomWebApplicationFactory : WebApplicationFactory // EN: Add in-memory database for testing // VI: Thêm in-memory database để test - services.AddDbContext(options => + services.AddDbContext(options => { options.UseInMemoryDatabase("TestDatabase_" + Guid.NewGuid().ToString()); }); @@ -49,7 +49,7 @@ public class CustomWebApplicationFactory : WebApplicationFactory // VI: Đảm bảo database được tạo với seed data var sp = services.BuildServiceProvider(); using var scope = sp.CreateScope(); - var db = scope.ServiceProvider.GetRequiredService(); + var db = scope.ServiceProvider.GetRequiredService(); db.Database.EnsureCreated(); }); }