diff --git a/src/Printbase.Application/Printbase.Application.csproj b/src/Printbase.Application/Printbase.Application.csproj index d096b39..7d1a6dc 100644 --- a/src/Printbase.Application/Printbase.Application.csproj +++ b/src/Printbase.Application/Printbase.Application.csproj @@ -8,6 +8,7 @@ + diff --git a/src/Printbase.Domain/Repositories/IProductGroupRepository.cs b/src/Printbase.Domain/Repositories/IProductGroupRepository.cs new file mode 100644 index 0000000..d2e2dca --- /dev/null +++ b/src/Printbase.Domain/Repositories/IProductGroupRepository.cs @@ -0,0 +1,13 @@ +using Printbase.Domain.Entities.Products; + +namespace Printbase.Domain.Repositories; + +public interface IProductGroupRepository +{ + Task GetByIdAsync(Guid id, CancellationToken cancellationToken = default); + Task> GetAllAsync(CancellationToken cancellationToken = default); + Task ExistsAsync(Guid id, CancellationToken cancellationToken = default); + Task AddAsync(ProductGroup productGroup, CancellationToken cancellationToken = default); + Task UpdateAsync(ProductGroup productGroup, CancellationToken cancellationToken = default); + Task DeleteAsync(Guid id, CancellationToken cancellationToken = default); +} \ No newline at end of file diff --git a/src/Printbase.Domain/Repositories/IProductRepository.cs b/src/Printbase.Domain/Repositories/IProductRepository.cs new file mode 100644 index 0000000..33ccd9e --- /dev/null +++ b/src/Printbase.Domain/Repositories/IProductRepository.cs @@ -0,0 +1,14 @@ +using Printbase.Domain.Entities.Products; + +namespace Printbase.Domain.Repositories; + +public interface IProductRepository +{ + Task GetByIdAsync(Guid id, CancellationToken cancellationToken = default); + Task> GetAllAsync(CancellationToken cancellationToken = default); + Task> GetByTypeIdAsync(Guid typeId, CancellationToken cancellationToken = default); + Task ExistsAsync(Guid id, CancellationToken cancellationToken = default); + Task AddAsync(Product product, CancellationToken cancellationToken = default); + Task UpdateAsync(Product product, CancellationToken cancellationToken = default); + Task DeleteAsync(Guid id, CancellationToken cancellationToken = default); +} \ No newline at end of file diff --git a/src/Printbase.Domain/Repositories/IProductTypeRepository.cs b/src/Printbase.Domain/Repositories/IProductTypeRepository.cs new file mode 100644 index 0000000..cf9906c --- /dev/null +++ b/src/Printbase.Domain/Repositories/IProductTypeRepository.cs @@ -0,0 +1,14 @@ +using Printbase.Domain.Entities.Products; + +namespace Printbase.Domain.Repositories; + +public interface IProductTypeRepository +{ + Task GetByIdAsync(Guid id, CancellationToken cancellationToken = default); + Task> GetAllAsync(CancellationToken cancellationToken = default); + Task> GetByGroupIdAsync(Guid groupId, CancellationToken cancellationToken = default); + Task ExistsAsync(Guid id, CancellationToken cancellationToken = default); + Task AddAsync(ProductType productType, CancellationToken cancellationToken = default); + Task UpdateAsync(ProductType productType, CancellationToken cancellationToken = default); + Task DeleteAsync(Guid id, CancellationToken cancellationToken = default); +} \ No newline at end of file diff --git a/src/Printbase.Domain/Repositories/IProductVariantRepository.cs b/src/Printbase.Domain/Repositories/IProductVariantRepository.cs new file mode 100644 index 0000000..6f14a60 --- /dev/null +++ b/src/Printbase.Domain/Repositories/IProductVariantRepository.cs @@ -0,0 +1,13 @@ +using Printbase.Domain.Entities.Products; + +namespace Printbase.Domain.Repositories; + +public interface IProductVariantRepository +{ + Task GetByIdAsync(Guid id, CancellationToken cancellationToken = default); + Task> GetByProductIdAsync(Guid productId, CancellationToken cancellationToken = default); + Task ExistsAsync(Guid id, CancellationToken cancellationToken = default); + Task AddAsync(ProductVariant productVariant, CancellationToken cancellationToken = default); + Task UpdateAsync(ProductVariant productVariant, CancellationToken cancellationToken = default); + Task DeleteAsync(Guid id, CancellationToken cancellationToken = default); +} \ No newline at end of file diff --git a/src/Printbase.Infrastructure/Database/ApplicationDbContext.cs b/src/Printbase.Infrastructure/Database/ApplicationDbContext.cs new file mode 100644 index 0000000..fc1f5b7 --- /dev/null +++ b/src/Printbase.Infrastructure/Database/ApplicationDbContext.cs @@ -0,0 +1,47 @@ +using Microsoft.EntityFrameworkCore; +using Printbase.Infrastructure.DbEntities.Products; + +namespace Printbase.Infrastructure.Database; + +public class ApplicationDbContext(DbContextOptions options) : DbContext(options) +{ + public DbSet Products { get; set; } = null!; + public DbSet ProductVariants { get; set; } = null!; + public DbSet ProductTypes { get; set; } = null!; + public DbSet ProductGroups { get; set; } = null!; + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + + modelBuilder.Entity() + .HasMany(p => p.Variants) + .WithOne(v => v.Product) + .HasForeignKey(v => v.ProductId) + .OnDelete(DeleteBehavior.Cascade); + + modelBuilder.Entity() + .HasOne(p => p.Type) + .WithMany(t => t.Products) + .HasForeignKey(p => p.TypeId) + .OnDelete(DeleteBehavior.Restrict); + + modelBuilder.Entity() + .HasOne(t => t.Group) + .WithMany(g => g.Types) + .HasForeignKey(t => t.GroupId) + .OnDelete(DeleteBehavior.Cascade); + + modelBuilder.Entity() + .Property(p => p.Discount) + .HasPrecision(18, 2); + + modelBuilder.Entity() + .Property(v => v.Price) + .HasPrecision(18, 2); + + modelBuilder.Entity() + .Property(v => v.Discount) + .HasPrecision(18, 2); + } +} \ No newline at end of file diff --git a/src/Printbase.Infrastructure/Printbase.Infrastructure.csproj b/src/Printbase.Infrastructure/Printbase.Infrastructure.csproj index 8baa33a..044a29f 100644 --- a/src/Printbase.Infrastructure/Printbase.Infrastructure.csproj +++ b/src/Printbase.Infrastructure/Printbase.Infrastructure.csproj @@ -8,6 +8,8 @@ + + diff --git a/src/Printbase.Infrastructure/Repositories/ProductGroupRepository.cs b/src/Printbase.Infrastructure/Repositories/ProductGroupRepository.cs new file mode 100644 index 0000000..dddab4e --- /dev/null +++ b/src/Printbase.Infrastructure/Repositories/ProductGroupRepository.cs @@ -0,0 +1,91 @@ +using AutoMapper; +using Microsoft.EntityFrameworkCore; +using Printbase.Domain.Entities.Products; +using Printbase.Domain.Repositories; +using Printbase.Infrastructure.Database; +using Printbase.Infrastructure.DbEntities.Products; + +namespace Printbase.Infrastructure.Repositories; + +public class ProductGroupRepository(ApplicationDbContext dbContext, IMapper mapper) : IProductGroupRepository +{ + private readonly ApplicationDbContext _dbContext = dbContext ?? throw new ArgumentNullException(nameof(dbContext)); + private readonly IMapper _mapper = mapper ?? throw new ArgumentNullException(nameof(mapper)); + + public async Task GetByIdAsync(Guid id, CancellationToken cancellationToken = default) + { + var dbEntity = await _dbContext.Set() + .Include(g => g.Types) + .FirstOrDefaultAsync(g => g.Id == id, cancellationToken); + + return dbEntity != null ? _mapper.Map(dbEntity) : null; + } + + public async Task> GetAllAsync(CancellationToken cancellationToken = default) + { + var dbEntities = await _dbContext.Set() + .Include(g => g.Types) + .ToListAsync(cancellationToken); + + return _mapper.Map>(dbEntities); + } + + public async Task ExistsAsync(Guid id, CancellationToken cancellationToken = default) + { + return await _dbContext.Set() + .AnyAsync(g => g.Id == id, cancellationToken); + } + + public async Task AddAsync(ProductGroup productGroup, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(productGroup); + + var dbEntity = _mapper.Map(productGroup); + await _dbContext.Set().AddAsync(dbEntity, cancellationToken); + await _dbContext.SaveChangesAsync(cancellationToken); + } + + public async Task UpdateAsync(ProductGroup productGroup, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(productGroup); + + var dbEntity = await _dbContext.Set() + .Include(g => g.Types) + .FirstOrDefaultAsync(g => g.Id == productGroup.Id, cancellationToken); + + if (dbEntity == null) + throw new KeyNotFoundException($"ProductGroup with ID {productGroup.Id} not found"); + + _mapper.Map(productGroup, dbEntity); + + var existingTypeIds = dbEntity.Types.Select(t => t.Id).ToList(); + var updatedTypeIds = productGroup.Types.Select(t => t.Id).ToList(); + + var typesToRemove = dbEntity.Types.Where(t => !updatedTypeIds.Contains(t.Id)).ToList(); + foreach (var type in typesToRemove) + { + dbEntity.Types.Remove(type); + } + + foreach (var type in productGroup.Types) + { + if (existingTypeIds.Contains(type.Id)) continue; + var newType = _mapper.Map(type); + dbEntity.Types.Add(newType); + } + + await _dbContext.SaveChangesAsync(cancellationToken); + } + + public async Task DeleteAsync(Guid id, CancellationToken cancellationToken = default) + { + var dbEntity = await _dbContext.Set() + .FindAsync([id], cancellationToken); + + if (dbEntity == null) + return; + + _dbContext.Set().Remove(dbEntity); + await _dbContext.SaveChangesAsync(cancellationToken); + } +} \ No newline at end of file diff --git a/src/Printbase.Infrastructure/Repositories/ProductRepository.cs b/src/Printbase.Infrastructure/Repositories/ProductRepository.cs new file mode 100644 index 0000000..6854a0d --- /dev/null +++ b/src/Printbase.Infrastructure/Repositories/ProductRepository.cs @@ -0,0 +1,100 @@ +using AutoMapper; +using Microsoft.EntityFrameworkCore; +using Printbase.Domain.Entities.Products; +using Printbase.Domain.Repositories; +using Printbase.Infrastructure.Database; +using Printbase.Infrastructure.DbEntities.Products; + +namespace Printbase.Infrastructure.Repositories; + +public class ProductRepository(ApplicationDbContext dbContext, IMapper mapper) : IProductRepository +{ + private readonly ApplicationDbContext _dbContext = dbContext ?? throw new ArgumentNullException(nameof(dbContext)); + private readonly IMapper _mapper = mapper ?? throw new ArgumentNullException(nameof(mapper)); + + public async Task GetByIdAsync(Guid id, CancellationToken cancellationToken = default) + { + var dbEntity = await _dbContext.Set() + .Include(p => p.Variants) + .FirstOrDefaultAsync(p => p.Id == id, cancellationToken); + + return dbEntity != null ? _mapper.Map(dbEntity) : null; + } + + public async Task> GetAllAsync(CancellationToken cancellationToken = default) + { + var dbEntities = await _dbContext.Set() + .Include(p => p.Variants) + .ToListAsync(cancellationToken); + + return _mapper.Map>(dbEntities); + } + + public async Task> GetByTypeIdAsync(Guid typeId, CancellationToken cancellationToken = default) + { + var dbEntities = await _dbContext.Set() + .Include(p => p.Variants) + .Where(p => p.TypeId == typeId) + .ToListAsync(cancellationToken); + + return _mapper.Map>(dbEntities); + } + + public async Task ExistsAsync(Guid id, CancellationToken cancellationToken = default) + { + return await _dbContext.Set() + .AnyAsync(p => p.Id == id, cancellationToken); + } + + public async Task AddAsync(Product product, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(product); + + var dbEntity = _mapper.Map(product); + await _dbContext.Set().AddAsync(dbEntity, cancellationToken); + await _dbContext.SaveChangesAsync(cancellationToken); + } + + public async Task UpdateAsync(Product product, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(product); + + var dbEntity = await _dbContext.Set() + .Include(p => p.Variants) + .FirstOrDefaultAsync(p => p.Id == product.Id, cancellationToken); + + if (dbEntity == null) + throw new KeyNotFoundException($"Product with ID {product.Id} not found"); + + _mapper.Map(product, dbEntity); + + var existingVariantIds = dbEntity.Variants.Select(v => v.Id).ToList(); + var updatedVariantIds = product.Variants.Select(v => v.Id).ToList(); + + var variantsToRemove = dbEntity.Variants.Where(v => !updatedVariantIds.Contains(v.Id)).ToList(); + foreach (var variant in variantsToRemove) + { + dbEntity.Variants.Remove(variant); + } + + foreach (var variant in product.Variants) + { + if (existingVariantIds.Contains(variant.Id)) continue; + var newVariant = _mapper.Map(variant); + dbEntity.Variants.Add(newVariant); + } + + await _dbContext.SaveChangesAsync(cancellationToken); + } + + public async Task DeleteAsync(Guid id, CancellationToken cancellationToken = default) + { + var dbEntity = await _dbContext.Set() + .FindAsync([id], cancellationToken); + + if (dbEntity == null) return; + + _dbContext.Set().Remove(dbEntity); + await _dbContext.SaveChangesAsync(cancellationToken); + } +} \ No newline at end of file diff --git a/src/Printbase.Infrastructure/Repositories/ProductTypeRepository.cs b/src/Printbase.Infrastructure/Repositories/ProductTypeRepository.cs new file mode 100644 index 0000000..4916597 --- /dev/null +++ b/src/Printbase.Infrastructure/Repositories/ProductTypeRepository.cs @@ -0,0 +1,80 @@ +using AutoMapper; +using Microsoft.EntityFrameworkCore; +using Printbase.Domain.Entities.Products; +using Printbase.Domain.Repositories; +using Printbase.Infrastructure.Database; +using Printbase.Infrastructure.DbEntities.Products; + +namespace Printbase.Infrastructure.Repositories; + +public class ProductTypeRepository(ApplicationDbContext dbContext, IMapper mapper) : IProductTypeRepository +{ + private readonly ApplicationDbContext _dbContext = dbContext ?? throw new ArgumentNullException(nameof(dbContext)); + private readonly IMapper _mapper = mapper ?? throw new ArgumentNullException(nameof(mapper)); + + public async Task GetByIdAsync(Guid id, CancellationToken cancellationToken = default) + { + var dbEntity = await _dbContext.Set() + .FirstOrDefaultAsync(t => t.Id == id, cancellationToken); + + return dbEntity != null ? _mapper.Map(dbEntity) : null; + } + + public async Task> GetAllAsync(CancellationToken cancellationToken = default) + { + var dbEntities = await _dbContext.Set() + .ToListAsync(cancellationToken); + + return _mapper.Map>(dbEntities); + } + + public async Task> GetByGroupIdAsync(Guid groupId, CancellationToken cancellationToken = default) + { + var dbEntities = await _dbContext.Set() + .Where(t => t.GroupId == groupId) + .ToListAsync(cancellationToken); + + return _mapper.Map>(dbEntities); + } + + public async Task ExistsAsync(Guid id, CancellationToken cancellationToken = default) + { + return await _dbContext.Set() + .AnyAsync(t => t.Id == id, cancellationToken); + } + + public async Task AddAsync(ProductType productType, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(productType); + + var dbEntity = _mapper.Map(productType); + await _dbContext.Set().AddAsync(dbEntity, cancellationToken); + await _dbContext.SaveChangesAsync(cancellationToken); + } + + public async Task UpdateAsync(ProductType productType, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(productType); + + var dbEntity = await _dbContext.Set() + .FirstOrDefaultAsync(t => t.Id == productType.Id, cancellationToken); + + if (dbEntity == null) + throw new KeyNotFoundException($"ProductType with ID {productType.Id} not found"); + + _mapper.Map(productType, dbEntity); + await _dbContext.SaveChangesAsync(cancellationToken); + } + + public async Task DeleteAsync(Guid id, CancellationToken cancellationToken = default) + { + var dbEntity = await _dbContext.Set() + .FindAsync([id], cancellationToken); + + if (dbEntity == null) + return; + + _dbContext.Set().Remove(dbEntity); + await _dbContext.SaveChangesAsync(cancellationToken); + } +} \ No newline at end of file diff --git a/src/Printbase.Infrastructure/Repositories/ProductVariantRepository.cs b/src/Printbase.Infrastructure/Repositories/ProductVariantRepository.cs new file mode 100644 index 0000000..c556fc5 --- /dev/null +++ b/src/Printbase.Infrastructure/Repositories/ProductVariantRepository.cs @@ -0,0 +1,72 @@ +using AutoMapper; +using Microsoft.EntityFrameworkCore; +using Printbase.Domain.Entities.Products; +using Printbase.Domain.Repositories; +using Printbase.Infrastructure.Database; +using Printbase.Infrastructure.DbEntities.Products; + +namespace Printbase.Infrastructure.Repositories; + +public class ProductVariantRepository(ApplicationDbContext dbContext, IMapper mapper) : IProductVariantRepository +{ + private readonly ApplicationDbContext _dbContext = dbContext ?? throw new ArgumentNullException(nameof(dbContext)); + private readonly IMapper _mapper = mapper ?? throw new ArgumentNullException(nameof(mapper)); + + public async Task GetByIdAsync(Guid id, CancellationToken cancellationToken = default) + { + var dbEntity = await _dbContext.Set() + .FirstOrDefaultAsync(v => v.Id == id, cancellationToken); + + return dbEntity != null ? _mapper.Map(dbEntity) : null; + } + + public async Task> GetByProductIdAsync(Guid productId, CancellationToken cancellationToken = default) + { + var dbEntities = await _dbContext.Set() + .Where(v => v.ProductId == productId) + .ToListAsync(cancellationToken); + + return _mapper.Map>(dbEntities); + } + + public async Task ExistsAsync(Guid id, CancellationToken cancellationToken = default) + { + return await _dbContext.Set() + .AnyAsync(v => v.Id == id, cancellationToken); + } + + public async Task AddAsync(ProductVariant productVariant, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(productVariant); + + var dbEntity = _mapper.Map(productVariant); + await _dbContext.Set().AddAsync(dbEntity, cancellationToken); + await _dbContext.SaveChangesAsync(cancellationToken); + } + + public async Task UpdateAsync(ProductVariant productVariant, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(productVariant); + + var dbEntity = await _dbContext.Set() + .FirstOrDefaultAsync(v => v.Id == productVariant.Id, cancellationToken); + + if (dbEntity == null) + throw new KeyNotFoundException($"ProductVariant with ID {productVariant.Id} not found"); + + _mapper.Map(productVariant, dbEntity); + await _dbContext.SaveChangesAsync(cancellationToken); + } + + public async Task DeleteAsync(Guid id, CancellationToken cancellationToken = default) + { + var dbEntity = await _dbContext.Set() + .FindAsync([id], cancellationToken); + + if (dbEntity == null) + return; + + _dbContext.Set().Remove(dbEntity); + await _dbContext.SaveChangesAsync(cancellationToken); + } +} \ No newline at end of file diff --git a/src/Printbase.WebApi/Printbase.WebApi.csproj b/src/Printbase.WebApi/Printbase.WebApi.csproj index c741fda..bc95296 100644 --- a/src/Printbase.WebApi/Printbase.WebApi.csproj +++ b/src/Printbase.WebApi/Printbase.WebApi.csproj @@ -10,4 +10,8 @@ + + + + diff --git a/src/Printbase.WebApi/Program.cs b/src/Printbase.WebApi/Program.cs index ee9d65d..3853a27 100644 --- a/src/Printbase.WebApi/Program.cs +++ b/src/Printbase.WebApi/Program.cs @@ -1,41 +1,34 @@ -var builder = WebApplication.CreateBuilder(args); +using Microsoft.EntityFrameworkCore; +using Printbase.Domain.Repositories; +using Printbase.Infrastructure.Database; +using Printbase.Infrastructure.Mapping; +using Printbase.Infrastructure.Repositories; -// Add services to the container. -// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi -builder.Services.AddOpenApi(); +var builder = WebApplication.CreateBuilder(args); +var services = builder.Services; +var configuration = builder.Configuration; + +services.AddOpenApi(); +services.AddDbContext(options => + options.UseSqlServer( + configuration.GetConnectionString("DefaultConnection"), + b => b.MigrationsAssembly(typeof(ApplicationDbContext).Assembly.FullName))); +services.AddAutoMapper(typeof(ProductMappingProfile).Assembly); + +services.AddScoped(); +services.AddScoped(); +services.AddScoped(); +services.AddScoped(); var app = builder.Build(); -// Configure the HTTP request pipeline. if (app.Environment.IsDevelopment()) { app.MapOpenApi(); } - -app.UseHttpsRedirection(); - -var summaries = new[] +else { - "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" -}; - -app.MapGet("/weatherforecast", () => -{ - var forecast = Enumerable.Range(1, 5).Select(index => - new WeatherForecast - ( - DateOnly.FromDateTime(DateTime.Now.AddDays(index)), - Random.Shared.Next(-20, 55), - summaries[Random.Shared.Next(summaries.Length)] - )) - .ToArray(); - return forecast; -}) -.WithName("GetWeatherForecast"); - -app.Run(); - -record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary) -{ - public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); + app.UseHttpsRedirection(); } + +app.Run(); \ No newline at end of file