Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b70d9559b0 | |||
| 032b9e1818 | |||
| 424c1c6f8c | |||
| 81d5e8f07c | |||
| 7d5e9a18d8 | |||
| 20a53942b5 | |||
| 3656223b5f | |||
| 9edb2aa4aa | |||
| ccf30ac36b | |||
| 6ed023f2cf | |||
| d6fdf1b9c8 | |||
| 2c9f5a846c | |||
| 89a343a85f | |||
| 41f7c05be3 | |||
| 52d204e286 |
@@ -14,8 +14,8 @@ public class CommonFixture : IDisposable
|
|||||||
{
|
{
|
||||||
Configuration = new ConfigurationBuilder()
|
Configuration = new ConfigurationBuilder()
|
||||||
.SetBasePath(Directory.GetCurrentDirectory())
|
.SetBasePath(Directory.GetCurrentDirectory())
|
||||||
.AddJsonFile("appsettings.json")
|
|
||||||
.AddUserSecrets<CommonFixture>()
|
.AddUserSecrets<CommonFixture>()
|
||||||
|
.AddJsonFile(Path.Combine(Directory.GetCurrentDirectory(), "appsettings.json"), optional: true, reloadOnChange: true)
|
||||||
.AddEnvironmentVariables()
|
.AddEnvironmentVariables()
|
||||||
.Build();
|
.Build();
|
||||||
|
|
||||||
@@ -24,8 +24,10 @@ public class CommonFixture : IDisposable
|
|||||||
.AddLogging()
|
.AddLogging()
|
||||||
.AddShopServices()
|
.AddShopServices()
|
||||||
.AddEmailServiceBus()
|
.AddEmailServiceBus()
|
||||||
|
.AddGarageS3(Configuration)
|
||||||
.AddShopDatabase(Configuration)
|
.AddShopDatabase(Configuration)
|
||||||
.AddEmailServices(Configuration)
|
.AddEmailServices(Configuration)
|
||||||
|
.AddSingleton(Configuration)
|
||||||
.BuildServiceProvider();
|
.BuildServiceProvider();
|
||||||
|
|
||||||
Mediator = Services.GetRequiredService<IMediator>();
|
Mediator = Services.GetRequiredService<IMediator>();
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="coverlet.collector" Version="10.0.0">
|
<PackageReference Include="coverlet.collector" Version="10.0.1">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
@@ -27,10 +27,10 @@
|
|||||||
|
|
||||||
<!-- Global Usings -->
|
<!-- Global Usings -->
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Using Include="Mediator"/>
|
<Using Include="Mediator" />
|
||||||
<Using Include="Xunit.Abstractions"/>
|
<Using Include="Xunit.Abstractions" />
|
||||||
<Using Include="Microsoft.Extensions.DependencyInjection"/>
|
<Using Include="Microsoft.Extensions.DependencyInjection" />
|
||||||
<Using Include="Microsoft.Extensions.Configuration"/>
|
<Using Include="Microsoft.Extensions.Configuration" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
@@ -43,7 +43,7 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<None Update="appsettings.json">
|
<None Update="appsettings.json">
|
||||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||||
</None>
|
</None>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
using LiteCharms.Features.Models;
|
using LiteCharms.Features.Models;
|
||||||
using LiteCharms.Features.Shop.Notifications;
|
using LiteCharms.Features.Shop.Notifications;
|
||||||
using LiteCharms.Features.Shop.Notifications.Events;
|
using LiteCharms.Features.Shop.Notifications.Events;
|
||||||
|
using static LiteCharms.Features.Extensions.Email;
|
||||||
|
|
||||||
namespace LiteCharms.Features.Tests;
|
namespace LiteCharms.Features.Tests;
|
||||||
|
|
||||||
@@ -20,8 +21,8 @@ public class NotificationsFeatureTests(CommonFixture fixture, ITestOutputHelper
|
|||||||
Priority = Shop.Priorities.Medium,
|
Priority = Shop.Priorities.Medium,
|
||||||
Sender = "xUnit Test",
|
Sender = "xUnit Test",
|
||||||
SenderAddress = "khwezi@mngoma.africa",
|
SenderAddress = "khwezi@mngoma.africa",
|
||||||
Recipient = $"{Email.Extensions.Constants.ShopEmailFromName} [Test]",
|
Recipient = $"{ShopEmailFromName} [Test]",
|
||||||
RecipientAddress = Email.Extensions.Constants.ShopEmailFromAddress,
|
RecipientAddress = ShopEmailFromAddress,
|
||||||
Subject = "Test Message",
|
Subject = "Test Message",
|
||||||
Message = "This is an automation test",
|
Message = "This is an automation test",
|
||||||
IsHtml = false,
|
IsHtml = false,
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
using LiteCharms.Features.Shop.Products;
|
||||||
|
|
||||||
|
namespace LiteCharms.Features.Tests;
|
||||||
|
|
||||||
|
public class ProductsFeatureTests(CommonFixture fixture, ITestOutputHelper output) : IClassFixture<CommonFixture>
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public async Task GetProductsAsync_ReturnsProducts()
|
||||||
|
{
|
||||||
|
var productService = fixture.Services.GetRequiredService<ProductService>();
|
||||||
|
|
||||||
|
var result = await productService.GetProductsAsync();
|
||||||
|
|
||||||
|
Assert.True(result.IsSuccess);
|
||||||
|
Assert.NotNull(result.Value);
|
||||||
|
|
||||||
|
output.WriteLine($"Retrieved {result.Value.Length} products.");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
using LiteCharms.Features.S3.Abstractions;
|
||||||
|
|
||||||
|
namespace LiteCharms.Features.Tests;
|
||||||
|
|
||||||
|
public class S3ServiceFeatureTests(CommonFixture fixture, ITestOutputHelper output) : IClassFixture<CommonFixture>
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public async Task BookshopS3Service_MustReturnUrl()
|
||||||
|
{
|
||||||
|
var service = fixture.Services.GetKeyedService<IS3Service>(S3.Constants.BookshopBucketName);
|
||||||
|
|
||||||
|
var fileName = "appsettings.json";
|
||||||
|
|
||||||
|
string path = Path.Combine(Directory.GetCurrentDirectory(), fileName);
|
||||||
|
|
||||||
|
Assert.True(File.Exists(path));
|
||||||
|
|
||||||
|
var stream = File.OpenRead(path);
|
||||||
|
|
||||||
|
var result = await service!.UploadFileAsync(fileName, stream, MimeKit.MimeTypes.GetMimeType(fileName));
|
||||||
|
|
||||||
|
Assert.True(result.IsSuccess);
|
||||||
|
Assert.NotNull(result.Value);
|
||||||
|
Assert.NotEmpty(result.Value);
|
||||||
|
|
||||||
|
output.WriteLine(result.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task BookshopS3Service_MustDeleteFile()
|
||||||
|
{
|
||||||
|
var service = fixture.Services.GetKeyedService<IS3Service>(S3.Constants.BookshopBucketName);
|
||||||
|
|
||||||
|
var fileName = "appsettings.json";
|
||||||
|
|
||||||
|
string path = Path.Combine(Directory.GetCurrentDirectory(), fileName);
|
||||||
|
|
||||||
|
Assert.True(File.Exists(path));
|
||||||
|
|
||||||
|
var stream = File.OpenRead(path);
|
||||||
|
|
||||||
|
var uploadResult = await service!.UploadFileAsync(fileName, stream, MimeKit.MimeTypes.GetMimeType(fileName));
|
||||||
|
|
||||||
|
Assert.True(uploadResult.IsSuccess);
|
||||||
|
Assert.NotNull(uploadResult.Value);
|
||||||
|
Assert.NotEmpty(uploadResult.Value);
|
||||||
|
|
||||||
|
var fileKey = uploadResult.Value.Split('/').Last();
|
||||||
|
|
||||||
|
var deleteResult = await service!.DeleteFileAsync(fileKey);
|
||||||
|
|
||||||
|
Assert.True(deleteResult.IsSuccess);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,16 @@
|
|||||||
{
|
{
|
||||||
|
"BookshopS3Settings": {
|
||||||
|
"ServiceUrl": "http://192.168.1.177:30900",
|
||||||
|
"Region": "garage",
|
||||||
|
"BucketName": "bookshop",
|
||||||
|
"CdnBaseUrl": "https://bookshop.cdn.khongisa.co.za"
|
||||||
|
},
|
||||||
|
"BookshopQuotesS3Settings": {
|
||||||
|
"ServiceUrl": "http://192.168.1.177:30900",
|
||||||
|
"Region": "garage",
|
||||||
|
"BucketName": "bookshop.quotes",
|
||||||
|
"CdnBaseUrl": "https://bookshop.quotes.cdn.khongisa.co.za"
|
||||||
|
},
|
||||||
"Email": {
|
"Email": {
|
||||||
"Credentials": {
|
"Credentials": {
|
||||||
"Username": "shop@litecharms.co.za"
|
"Username": "shop@litecharms.co.za"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
using LiteCharms.Features.Shop;
|
using LiteCharms.Features.Shop;
|
||||||
using LiteCharms.Features.Shop.Notifications;
|
using LiteCharms.Features.Shop.Notifications;
|
||||||
using static LiteCharms.Features.Email.Extensions.Constants;
|
using static LiteCharms.Features.Extensions.Email;
|
||||||
|
|
||||||
namespace LiteCharms.Features.Email.Events.Handlers;
|
namespace LiteCharms.Features.Email.Events.Handlers;
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +0,0 @@
|
|||||||
namespace LiteCharms.Features.Email.Extensions;
|
|
||||||
|
|
||||||
public static class Constants
|
|
||||||
{
|
|
||||||
public const string ShopSchedulerName = "shop";
|
|
||||||
public const string ShopEmailFromName = "Khongisa Shop";
|
|
||||||
public const string ShopEmailFromAddress = "shop@litecharms.co.za";
|
|
||||||
}
|
|
||||||
+1
-1
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
namespace LiteCharms.Features.Email.Models;
|
namespace LiteCharms.Features.Email.Models;
|
||||||
|
|
||||||
public sealed class EmailEnquiry
|
public sealed class EmailEnquiryModel
|
||||||
{
|
{
|
||||||
[Required]
|
[Required]
|
||||||
[MinLength(2)]
|
[MinLength(2)]
|
||||||
@@ -5,6 +5,9 @@ namespace LiteCharms.Features.Extensions;
|
|||||||
|
|
||||||
public static class Email
|
public static class Email
|
||||||
{
|
{
|
||||||
|
public const string ShopEmailFromName = "Khongisa Shop";
|
||||||
|
public const string ShopEmailFromAddress = "shop@litecharms.co.za";
|
||||||
|
|
||||||
public static IServiceCollection AddEmailServices(this IServiceCollection services, IConfiguration configuration)
|
public static IServiceCollection AddEmailServices(this IServiceCollection services, IConfiguration configuration)
|
||||||
{
|
{
|
||||||
services.Configure<SmtpSettings>(configuration.GetSection("Email"));
|
services.Configure<SmtpSettings>(configuration.GetSection("Email"));
|
||||||
|
|||||||
@@ -177,12 +177,14 @@ public static class EntityModeMappers
|
|||||||
new()
|
new()
|
||||||
{
|
{
|
||||||
Id = entity.Id,
|
Id = entity.Id,
|
||||||
|
CreatedAt = entity.CreatedAt,
|
||||||
Name = entity.Name,
|
Name = entity.Name,
|
||||||
Description = entity.Description,
|
Description = entity.Description,
|
||||||
Active = entity.Active,
|
Active = entity.Active,
|
||||||
Summary = entity.Summary,
|
Summary = entity.Summary,
|
||||||
ImageUrl = entity.ImageUrl,
|
ImageUrl = entity.ImageUrl,
|
||||||
Thumbnails = entity.Thumbnails
|
Thumbnails = entity.Thumbnails,
|
||||||
|
Metadata = entity.Metadata,
|
||||||
};
|
};
|
||||||
|
|
||||||
public static ProductPrice ToModel(this Features.Shop.Products.Entities.ProductPrice entity) =>
|
public static ProductPrice ToModel(this Features.Shop.Products.Entities.ProductPrice entity) =>
|
||||||
|
|||||||
@@ -2,6 +2,12 @@
|
|||||||
|
|
||||||
public static class Hash
|
public static class Hash
|
||||||
{
|
{
|
||||||
public static Func<string?, string?> GenerateSha256HashString = (input) =>
|
public static Func<string?, string?> StringToSha256Hash = (input) =>
|
||||||
Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(input!)));
|
Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(input!)));
|
||||||
|
|
||||||
|
public static Func<Stream, string?> StreamToSha256Hash = (stream) =>
|
||||||
|
Convert.ToHexString(SHA256.HashData(stream));
|
||||||
|
|
||||||
|
public static Func<byte[], string?> BytesToSha256Hash = (bytes) =>
|
||||||
|
Convert.ToHexString(SHA256.HashData(bytes));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +1,34 @@
|
|||||||
using LiteCharms.Features.HealthChecks;
|
using LiteCharms.Features.HealthChecks;
|
||||||
|
using static LiteCharms.Features.Extensions.Postgres;
|
||||||
|
|
||||||
namespace LiteCharms.Features.Extensions;
|
namespace LiteCharms.Features.Extensions;
|
||||||
|
|
||||||
public static class HealthChecks
|
public static class HealthChecks
|
||||||
{
|
{
|
||||||
public static IServiceCollection AddQuartzHealtchCheck(this IServiceCollection services)
|
public static IServiceCollection AddShopQuartzHealthCheck(this IServiceCollection services)
|
||||||
{
|
{
|
||||||
services.AddHealthChecks().AddCheck<QuartzHealthCheck>("Quartz");
|
services.AddHealthChecks().AddCheck<ShopQuartzHealthCheck>("ShopQuartz");
|
||||||
|
|
||||||
return services;
|
return services;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static IServiceCollection AddPostgresHealtchCheck(this IServiceCollection services)
|
public static IServiceCollection AddMidrandShopQuartzHealthCheck(this IServiceCollection services)
|
||||||
{
|
{
|
||||||
services.AddHealthChecks().AddCheck<PostgresHealthCheck>("PostgreSQL");
|
services.AddHealthChecks().AddCheck<MidrandShopQuartzHealthCheck>("MidrandShopQuartz");
|
||||||
|
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IServiceCollection AddShopPostgresHealthCheck(this IServiceCollection services)
|
||||||
|
{
|
||||||
|
services.AddHealthChecks().AddCheck<PostgresShopHealthCheck>(ShopDbConfigName);
|
||||||
|
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IServiceCollection AddMidrandShopPostgresHealthCheck(this IServiceCollection services)
|
||||||
|
{
|
||||||
|
services.AddHealthChecks().AddCheck<PostgresMidrandShopHealthCheck>(MidrandShopDbConfigName);
|
||||||
|
|
||||||
return services;
|
return services;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,26 @@
|
|||||||
using LiteCharms.Features.Shop.Postgres;
|
using LiteCharms.Features.MidrandShop.Postgres;
|
||||||
|
using LiteCharms.Features.Shop.Postgres;
|
||||||
|
|
||||||
namespace LiteCharms.Features.Extensions;
|
namespace LiteCharms.Features.Extensions;
|
||||||
|
|
||||||
public static class Postgres
|
public static class Postgres
|
||||||
{
|
{
|
||||||
|
public const string MidrandShopDbConfigName = "PostgresMidrandShop";
|
||||||
|
public const string ShopDbConfigName = "PostgresShop";
|
||||||
|
public const string SchedulerDbConfigName = "PostgresScheduler";
|
||||||
|
|
||||||
public static IServiceCollection AddShopDatabase(this IServiceCollection services, IConfiguration configuration)
|
public static IServiceCollection AddShopDatabase(this IServiceCollection services, IConfiguration configuration)
|
||||||
{
|
{
|
||||||
services.AddPooledDbContextFactory<ShopDbContext>(options =>
|
services.AddPooledDbContextFactory<ShopDbContext>(options =>
|
||||||
options.UseNpgsql(configuration.GetConnectionString("PostgresShop")));
|
options.UseNpgsql(configuration.GetConnectionString(ShopDbConfigName)));
|
||||||
|
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IServiceCollection AddMidrandShopDatabase(this IServiceCollection services, IConfiguration configuration)
|
||||||
|
{
|
||||||
|
services.AddPooledDbContextFactory<MidrandShopDbContext>(options =>
|
||||||
|
options.UseNpgsql(configuration.GetConnectionString(MidrandShopDbConfigName)));
|
||||||
|
|
||||||
return services;
|
return services;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,17 @@
|
|||||||
using LiteCharms.Features.Quartz;
|
using LiteCharms.Features.Quartz;
|
||||||
using LiteCharms.Features.Quartz.Abstractions;
|
using LiteCharms.Features.Quartz.Abstractions;
|
||||||
|
using static LiteCharms.Features.Extensions.Postgres;
|
||||||
|
|
||||||
namespace LiteCharms.Features.Extensions;
|
namespace LiteCharms.Features.Extensions;
|
||||||
|
|
||||||
public static class Quartz
|
public static class Quartz
|
||||||
{
|
{
|
||||||
private const string databaseConfigName = "PostgresScheduler";
|
public const string ShopSchedulerName = "shop";
|
||||||
|
public const string MidrandShopSchedulerName = "midrandshop";
|
||||||
|
|
||||||
public static IServiceCollection AddQuartzSchedulerClient(this IServiceCollection services, string schedulerName, IConfiguration configuration)
|
public static IServiceCollection AddQuartzSchedulerClient(this IServiceCollection services, string schedulerName, IConfiguration configuration)
|
||||||
{
|
{
|
||||||
var connectionString = configuration.GetConnectionString(databaseConfigName);
|
var connectionString = configuration.GetConnectionString(SchedulerDbConfigName);
|
||||||
|
|
||||||
services.ConfigureCommon();
|
services.ConfigureCommon();
|
||||||
|
|
||||||
@@ -44,7 +46,7 @@ public static class Quartz
|
|||||||
|
|
||||||
public static IServiceCollection AddQuartzScheduler(this IServiceCollection services, string schedulerName, IConfiguration configuration)
|
public static IServiceCollection AddQuartzScheduler(this IServiceCollection services, string schedulerName, IConfiguration configuration)
|
||||||
{
|
{
|
||||||
var connectionString = configuration.GetConnectionString(databaseConfigName);
|
var connectionString = configuration.GetConnectionString(SchedulerDbConfigName);
|
||||||
|
|
||||||
services.ConfigureCommon();
|
services.ConfigureCommon();
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
using Amazon.Runtime;
|
using LiteCharms.Features.S3;
|
||||||
using LiteCharms.Features.S3;
|
using LiteCharms.Features.S3.Abstractions;
|
||||||
using LiteCharms.Features.S3.Configuration;
|
using static LiteCharms.Features.S3.Constants;
|
||||||
|
|
||||||
namespace LiteCharms.Features.Extensions;
|
namespace LiteCharms.Features.Extensions;
|
||||||
|
|
||||||
@@ -8,23 +8,56 @@ public static class S3
|
|||||||
{
|
{
|
||||||
public static IServiceCollection AddGarageS3(this IServiceCollection services, IConfiguration configuration)
|
public static IServiceCollection AddGarageS3(this IServiceCollection services, IConfiguration configuration)
|
||||||
{
|
{
|
||||||
var optionsSection = configuration.GetSection(nameof(S3Settings));
|
if (!string.IsNullOrWhiteSpace(configuration.GetSection($"{BookshopS3SettingsSection}:ServiceUrl").Value))
|
||||||
services.Configure<S3Settings>(optionsSection);
|
|
||||||
|
|
||||||
var options = optionsSection.Get<S3Settings>()
|
|
||||||
?? throw new InvalidOperationException("S3 configuration section is missing.");
|
|
||||||
|
|
||||||
var credentials = new BasicAWSCredentials(options.AccessKey, options.SecretKey);
|
|
||||||
|
|
||||||
var s3Config = new AmazonS3Config
|
|
||||||
{
|
{
|
||||||
ServiceURL = options.ServiceUrl,
|
services.AddKeyedSingleton<IAmazonS3, AmazonS3Client>(BookshopBucketName, (provider, client) =>
|
||||||
AuthenticationRegion = options.Region,
|
new AmazonS3Client(new BasicAWSCredentials(configuration.GetSection($"{BookshopS3SettingsSection}:AccessKey").Value,
|
||||||
ForcePathStyle = true,
|
configuration.GetSection($"{BookshopS3SettingsSection}:SecretKey").Value),
|
||||||
};
|
new AmazonS3Config
|
||||||
|
{
|
||||||
|
ServiceURL = configuration.GetSection($"{BookshopS3SettingsSection}:ServiceUrl").Value,
|
||||||
|
AuthenticationRegion = configuration.GetSection($"{BookshopS3SettingsSection}:Region").Value,
|
||||||
|
ForcePathStyle = true,
|
||||||
|
EndpointDiscoveryEnabled = true,
|
||||||
|
UseHttp = configuration.GetSection($"{BookshopS3SettingsSection}:ServiceUrl").Value!.Contains("http://")
|
||||||
|
}));
|
||||||
|
|
||||||
services.AddSingleton<IAmazonS3>(new AmazonS3Client(credentials, s3Config));
|
services.AddKeyedScoped<IS3Service, BookshopS3Service>(BookshopBucketName);
|
||||||
services.AddScoped<S3Service>();
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(configuration.GetSection($"{BookshopInvoicesS3SettingsSection}:ServiceUrl").Value))
|
||||||
|
{
|
||||||
|
services.AddKeyedSingleton<IAmazonS3, AmazonS3Client>(BookshopInvoicesBucketName, (provider, client) =>
|
||||||
|
new AmazonS3Client(new BasicAWSCredentials(configuration.GetSection($"{BookshopInvoicesS3SettingsSection}:AccessKey").Value,
|
||||||
|
configuration.GetSection($"{BookshopInvoicesS3SettingsSection}:SecretKey").Value),
|
||||||
|
new AmazonS3Config
|
||||||
|
{
|
||||||
|
ServiceURL = configuration.GetSection($"{BookshopInvoicesS3SettingsSection}:ServiceUrl").Value,
|
||||||
|
AuthenticationRegion = configuration.GetSection($"{BookshopInvoicesS3SettingsSection}:Region").Value,
|
||||||
|
ForcePathStyle = true,
|
||||||
|
EndpointDiscoveryEnabled = true,
|
||||||
|
UseHttp = configuration.GetSection($"{BookshopS3SettingsSection}:ServiceUrl").Value!.Contains("http://")
|
||||||
|
}));
|
||||||
|
|
||||||
|
services.AddKeyedScoped<IS3Service, BookshopInvoicesS3Service>(BookshopInvoicesBucketName);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(configuration.GetSection($"{BookshopQuotesS3SettingsSection}:ServiceUrl").Value))
|
||||||
|
{
|
||||||
|
services.AddKeyedSingleton<IAmazonS3, AmazonS3Client>(BookshopQuotesBucketName, (provider, client) =>
|
||||||
|
new AmazonS3Client(new BasicAWSCredentials(configuration.GetSection($"{BookshopQuotesS3SettingsSection}:AccessKey").Value,
|
||||||
|
configuration.GetSection($"{BookshopQuotesS3SettingsSection}:SecretKey").Value),
|
||||||
|
new AmazonS3Config
|
||||||
|
{
|
||||||
|
ServiceURL = configuration.GetSection($"{BookshopQuotesS3SettingsSection}:ServiceUrl").Value,
|
||||||
|
AuthenticationRegion = configuration.GetSection($"{BookshopQuotesS3SettingsSection}:Region").Value,
|
||||||
|
ForcePathStyle = true,
|
||||||
|
EndpointDiscoveryEnabled = true,
|
||||||
|
UseHttp = configuration.GetSection($"{BookshopS3SettingsSection}:ServiceUrl").Value!.Contains("http://")
|
||||||
|
}));
|
||||||
|
|
||||||
|
services.AddKeyedScoped<IS3Service, BookshopQuotesS3Service>(BookshopQuotesBucketName);
|
||||||
|
}
|
||||||
|
|
||||||
return services;
|
return services;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
using static LiteCharms.Features.Extensions.Quartz;
|
||||||
|
|
||||||
|
namespace LiteCharms.Features.HealthChecks;
|
||||||
|
|
||||||
|
public class MidrandShopQuartzHealthCheck(ISchedulerFactory schedulerFactory) : IHealthCheck
|
||||||
|
{
|
||||||
|
public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var scheduler = await schedulerFactory.GetScheduler(MidrandShopSchedulerName, cancellationToken);
|
||||||
|
|
||||||
|
if(scheduler == null)
|
||||||
|
return HealthCheckResult.Unhealthy($"Scheduler with name '{MidrandShopSchedulerName}' not found.");
|
||||||
|
|
||||||
|
if (!scheduler.IsStarted)
|
||||||
|
return HealthCheckResult.Unhealthy($"{MidrandShopSchedulerName} Quartz scheduler is not running");
|
||||||
|
|
||||||
|
await scheduler.CheckExists(new JobKey(Guid.NewGuid().ToString()), cancellationToken);
|
||||||
|
|
||||||
|
return HealthCheckResult.Healthy($"{MidrandShopSchedulerName} Quartz scheduler is ready");
|
||||||
|
}
|
||||||
|
catch (SchedulerException)
|
||||||
|
{
|
||||||
|
return HealthCheckResult.Unhealthy($"{MidrandShopSchedulerName} Quartz scheduler cannot connect to the store");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
using static LiteCharms.Features.Extensions.Postgres;
|
||||||
|
|
||||||
|
namespace LiteCharms.Features.HealthChecks;
|
||||||
|
|
||||||
|
public class PostgresMidrandShopHealthCheck(IConfiguration configuration) : IHealthCheck
|
||||||
|
{
|
||||||
|
private readonly string connectionString = configuration.GetConnectionString(MidrandShopDbConfigName)!;
|
||||||
|
|
||||||
|
public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await using var dataSource = NpgsqlDataSource.Create(connectionString);
|
||||||
|
await using var connection = await dataSource.OpenConnectionAsync(cancellationToken);
|
||||||
|
|
||||||
|
await using var command = connection.CreateCommand();
|
||||||
|
command.CommandText = "SELECT 1";
|
||||||
|
|
||||||
|
await command.ExecuteScalarAsync(cancellationToken);
|
||||||
|
|
||||||
|
return HealthCheckResult.Healthy($"{MidrandShopDbConfigName} is responsive.");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return HealthCheckResult.Unhealthy($"{MidrandShopDbConfigName} is unreachable.", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+7
-5
@@ -1,8 +1,10 @@
|
|||||||
namespace LiteCharms.Features.HealthChecks;
|
using static LiteCharms.Features.Extensions.Postgres;
|
||||||
|
|
||||||
public class PostgresHealthCheck(IConfiguration configuration) : IHealthCheck
|
namespace LiteCharms.Features.HealthChecks;
|
||||||
|
|
||||||
|
public class PostgresShopHealthCheck(IConfiguration configuration) : IHealthCheck
|
||||||
{
|
{
|
||||||
private readonly string connectionString = configuration.GetConnectionString("PostgresShop")!;
|
private readonly string connectionString = configuration.GetConnectionString(ShopDbConfigName)!;
|
||||||
|
|
||||||
public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
|
public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
@@ -16,11 +18,11 @@ public class PostgresHealthCheck(IConfiguration configuration) : IHealthCheck
|
|||||||
|
|
||||||
await command.ExecuteScalarAsync(cancellationToken);
|
await command.ExecuteScalarAsync(cancellationToken);
|
||||||
|
|
||||||
return HealthCheckResult.Healthy("PostgreSQL is responsive.");
|
return HealthCheckResult.Healthy($"{ShopDbConfigName} is responsive.");
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
return HealthCheckResult.Unhealthy("PostgreSQL is unreachable.", ex);
|
return HealthCheckResult.Unhealthy($"{ShopDbConfigName} is unreachable.", ex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
namespace LiteCharms.Features.HealthChecks;
|
|
||||||
|
|
||||||
public class QuartzHealthCheck(ISchedulerFactory schedulerFactory) : IHealthCheck
|
|
||||||
{
|
|
||||||
public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var scheduler = await schedulerFactory.GetScheduler(cancellationToken);
|
|
||||||
|
|
||||||
if (!scheduler.IsStarted)
|
|
||||||
return HealthCheckResult.Unhealthy("Quartz scheduler is not running");
|
|
||||||
|
|
||||||
await scheduler.CheckExists(new JobKey(Guid.NewGuid().ToString()), cancellationToken);
|
|
||||||
|
|
||||||
return HealthCheckResult.Healthy("Quartz scheduler is ready");
|
|
||||||
}
|
|
||||||
catch (SchedulerException)
|
|
||||||
{
|
|
||||||
return HealthCheckResult.Unhealthy("Quartz scheduler cannot connect to the store");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
using static LiteCharms.Features.Extensions.Quartz;
|
||||||
|
|
||||||
|
namespace LiteCharms.Features.HealthChecks;
|
||||||
|
|
||||||
|
public class ShopQuartzHealthCheck(ISchedulerFactory schedulerFactory) : IHealthCheck
|
||||||
|
{
|
||||||
|
public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var scheduler = await schedulerFactory.GetScheduler(ShopSchedulerName, cancellationToken);
|
||||||
|
|
||||||
|
if(scheduler == null)
|
||||||
|
return HealthCheckResult.Unhealthy($"Scheduler with name '{ShopSchedulerName}' not found.");
|
||||||
|
|
||||||
|
if (!scheduler.IsStarted)
|
||||||
|
return HealthCheckResult.Unhealthy($"{ShopSchedulerName} Quartz scheduler is not running");
|
||||||
|
|
||||||
|
await scheduler.CheckExists(new JobKey(Guid.NewGuid().ToString()), cancellationToken);
|
||||||
|
|
||||||
|
return HealthCheckResult.Healthy($"{ShopSchedulerName} Quartz scheduler is ready");
|
||||||
|
}
|
||||||
|
catch (SchedulerException)
|
||||||
|
{
|
||||||
|
return HealthCheckResult.Unhealthy($"{ShopSchedulerName} Quartz scheduler cannot connect to the store");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -131,12 +131,13 @@
|
|||||||
|
|
||||||
<!-- Amazon S3 SDK -->
|
<!-- Amazon S3 SDK -->
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="AWSSDK.Extensions.NetCore.Setup" Version="4.0.3.40" />
|
<PackageReference Include="AWSSDK.Extensions.NetCore.Setup" Version="4.0.4.1" />
|
||||||
<PackageReference Include="AWSSDK.S3" Version="4.0.23.3" />
|
<PackageReference Include="AWSSDK.S3" Version="4.0.23.4" />
|
||||||
|
|
||||||
<!-- global Usings -->
|
<!-- global Usings -->
|
||||||
<Using Include="Amazon.S3" />
|
<Using Include="Amazon.S3" />
|
||||||
<Using Include="Amazon.S3.Model" />
|
<Using Include="Amazon.S3.Model" />
|
||||||
|
<Using Include="Amazon.Runtime" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<!-- Shared Usings -->
|
<!-- Shared Usings -->
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
namespace LiteCharms.Features.MidrandShop.Postgres;
|
||||||
|
|
||||||
|
public class MidrandShopDbContext(DbContextOptions<MidrandShopDbContext> options) : DbContext(options)
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
using static LiteCharms.Features.Extensions.Postgres;
|
||||||
|
|
||||||
|
namespace LiteCharms.Features.MidrandShop.Postgres;
|
||||||
|
|
||||||
|
public class MidrandShopDbContextFactory : IDesignTimeDbContextFactory<MidrandShopDbContext>
|
||||||
|
{
|
||||||
|
public MidrandShopDbContext CreateDbContext(string[] args)
|
||||||
|
{
|
||||||
|
var configuration = new ConfigurationBuilder()
|
||||||
|
.SetBasePath(Directory.GetCurrentDirectory())
|
||||||
|
.AddUserSecrets(typeof(MidrandShopDbContext).Assembly)
|
||||||
|
.AddJsonFile("appsettings.json")
|
||||||
|
.AddEnvironmentVariables()
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
var optionsBuilder = new DbContextOptionsBuilder<MidrandShopDbContext>();
|
||||||
|
optionsBuilder.UseNpgsql(configuration.GetConnectionString(MidrandShopDbConfigName));
|
||||||
|
|
||||||
|
return new MidrandShopDbContext(optionsBuilder.Options);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
namespace LiteCharms.Features.S3.Abstractions;
|
||||||
|
|
||||||
|
public interface IS3Service
|
||||||
|
{
|
||||||
|
Task<Result<string>> UploadFileAsync(string fileName, Stream fileStream, string contentType, CancellationToken cancellationToken = default);
|
||||||
|
Task<Result> DeleteFileAsync(string fileKey, CancellationToken cancellationToken = default);
|
||||||
|
}
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
using static LiteCharms.Features.Extensions.Hash;
|
||||||
|
|
||||||
|
namespace LiteCharms.Features.S3.Abstractions;
|
||||||
|
|
||||||
|
public abstract class S3ServiceBase(IAmazonS3 amazonS3)
|
||||||
|
{
|
||||||
|
protected readonly IAmazonS3 Client = amazonS3;
|
||||||
|
|
||||||
|
protected abstract string BucketName { get; }
|
||||||
|
protected abstract string CdnBaseUrl { get; }
|
||||||
|
|
||||||
|
public virtual async Task<Result<string>> UploadFileAsync(string fileName, Stream fileStream, string contentType, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(BucketName))
|
||||||
|
return Result.Fail<string>("Bucket name is not configured.");
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(CdnBaseUrl))
|
||||||
|
return Result.Fail<string>("CDN base URL is not configured.");
|
||||||
|
|
||||||
|
using var stream = new MemoryStream();
|
||||||
|
|
||||||
|
await fileStream.CopyToAsync(stream, cancellationToken);
|
||||||
|
await fileStream.DisposeAsync();
|
||||||
|
|
||||||
|
stream.Seek(0, SeekOrigin.Begin);
|
||||||
|
|
||||||
|
var fileHash = StreamToSha256Hash(stream);
|
||||||
|
|
||||||
|
if(string.IsNullOrWhiteSpace(fileHash))
|
||||||
|
return Result.Fail<string>("Failed to compute file hash.");
|
||||||
|
|
||||||
|
var fileKey = $"{fileHash.ToLower()}{Path.GetExtension(fileName)}";
|
||||||
|
|
||||||
|
var putRequest = new PutObjectRequest
|
||||||
|
{
|
||||||
|
BucketName = BucketName,
|
||||||
|
Key = fileKey,
|
||||||
|
InputStream = stream,
|
||||||
|
ContentType = contentType,
|
||||||
|
UseChunkEncoding = false
|
||||||
|
};
|
||||||
|
|
||||||
|
stream.Seek(0, SeekOrigin.Begin);
|
||||||
|
|
||||||
|
var response = await Client.PutObjectAsync(putRequest, cancellationToken);
|
||||||
|
|
||||||
|
return response.HttpStatusCode != System.Net.HttpStatusCode.OK
|
||||||
|
? Result.Fail<string>($"Failed to upload {fileName} to S3.")
|
||||||
|
: Result.Ok($"{CdnBaseUrl}/{fileKey}");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return Result.Fail<string>(new Error($"Error uploading {fileName} to S3: {ex.Message}").CausedBy(ex));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual async Task<Result> DeleteFileAsync(string fileKey, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(BucketName))
|
||||||
|
return Result.Fail("Bucket name is not configured.");
|
||||||
|
|
||||||
|
var deleteRequest = new DeleteObjectRequest
|
||||||
|
{
|
||||||
|
BucketName = BucketName,
|
||||||
|
Key = fileKey
|
||||||
|
};
|
||||||
|
|
||||||
|
var response = await Client.DeleteObjectAsync(deleteRequest, cancellationToken);
|
||||||
|
|
||||||
|
return response.HttpStatusCode != System.Net.HttpStatusCode.NoContent
|
||||||
|
? Result.Fail($"Failed to delete {fileKey} from S3.")
|
||||||
|
: Result.Ok();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return Result.Fail(new Error($"Error deleting {fileKey} from S3: {ex.Message}").CausedBy(ex));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
using LiteCharms.Features.S3.Abstractions;
|
||||||
|
using static LiteCharms.Features.S3.Constants;
|
||||||
|
|
||||||
|
namespace LiteCharms.Features.S3;
|
||||||
|
|
||||||
|
public class BookshopInvoicesS3Service(IConfiguration configuration, [FromKeyedServices(BookshopInvoicesBucketName)] IAmazonS3 amazonS3) :
|
||||||
|
S3ServiceBase(amazonS3), IS3Service
|
||||||
|
{
|
||||||
|
protected override string BucketName => configuration.GetSection($"{BookshopInvoicesS3SettingsSection}:BucketName").Value ?? "";
|
||||||
|
protected override string CdnBaseUrl => configuration.GetSection($"{BookshopInvoicesS3SettingsSection}:CdnBaseUrl").Value ?? "";
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
using LiteCharms.Features.S3.Abstractions;
|
||||||
|
using static LiteCharms.Features.S3.Constants;
|
||||||
|
|
||||||
|
namespace LiteCharms.Features.S3;
|
||||||
|
|
||||||
|
public class BookshopQuotesS3Service(IConfiguration configuration, [FromKeyedServices(BookshopQuotesBucketName)] IAmazonS3 amazonS3) :
|
||||||
|
S3ServiceBase(amazonS3), IS3Service
|
||||||
|
{
|
||||||
|
protected override string BucketName => configuration.GetSection($"{BookshopQuotesS3SettingsSection}:BucketName").Value ?? "";
|
||||||
|
protected override string CdnBaseUrl => configuration.GetSection($"{BookshopQuotesS3SettingsSection}:CdnBaseUrl").Value ?? "";
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
using LiteCharms.Features.S3.Abstractions;
|
||||||
|
using static LiteCharms.Features.S3.Constants;
|
||||||
|
|
||||||
|
namespace LiteCharms.Features.S3;
|
||||||
|
|
||||||
|
public class BookshopS3Service(IConfiguration configuration, [FromKeyedServices(BookshopBucketName)] IAmazonS3 amazonS3) :
|
||||||
|
S3ServiceBase(amazonS3), IS3Service
|
||||||
|
{
|
||||||
|
protected override string BucketName => configuration.GetSection($"{BookshopS3SettingsSection}:BucketName").Value ?? "";
|
||||||
|
protected override string CdnBaseUrl => configuration.GetSection($"{BookshopS3SettingsSection}:CdnBaseUrl").Value ?? "";
|
||||||
|
}
|
||||||
@@ -11,4 +11,6 @@ public class S3Settings
|
|||||||
public string? BucketName { get; set; }
|
public string? BucketName { get; set; }
|
||||||
|
|
||||||
public string? Region { get; set; }
|
public string? Region { get; set; }
|
||||||
|
|
||||||
|
public string? CdnBaseUrl { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
namespace LiteCharms.Features.S3;
|
||||||
|
|
||||||
|
public static class Constants
|
||||||
|
{
|
||||||
|
public const string BookshopS3SettingsSection = "BookshopS3Settings";
|
||||||
|
public const string BookshopInvoicesS3SettingsSection = "BookshopInvoicesS3Settings";
|
||||||
|
public const string BookshopQuotesS3SettingsSection = "BookshopQuotesS3Settings";
|
||||||
|
|
||||||
|
public const string BookshopBucketName = "bookshop";
|
||||||
|
public const string BookshopInvoicesBucketName = "bookshop.invoices";
|
||||||
|
public const string BookshopQuotesBucketName = "bookshop.quotes";
|
||||||
|
}
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
namespace LiteCharms.Features.S3;
|
|
||||||
|
|
||||||
public class S3Service(IAmazonS3 amazonS3)
|
|
||||||
{
|
|
||||||
public async Task<Result<string>> UploadFileAsync(string bucketName, string fileName, Stream fileStream, string contentType, string cdnBaseUrl, CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var putRequest = new PutObjectRequest
|
|
||||||
{
|
|
||||||
BucketName = bucketName,
|
|
||||||
Key = fileName,
|
|
||||||
InputStream = fileStream,
|
|
||||||
ContentType = contentType
|
|
||||||
};
|
|
||||||
|
|
||||||
var response = await amazonS3.PutObjectAsync(putRequest, cancellationToken);
|
|
||||||
|
|
||||||
return response.HttpStatusCode != System.Net.HttpStatusCode.OK
|
|
||||||
? Result.Fail<string>($"Failed to upload {fileName} to S3.")
|
|
||||||
: Result.Ok(string.Format(cdnBaseUrl, bucketName, fileName));
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
return Result.Fail<string>(new Error($"Error uploading {fileName} to S3: {ex.Message}").CausedBy(ex));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -2,7 +2,6 @@
|
|||||||
using LiteCharms.Features.Models;
|
using LiteCharms.Features.Models;
|
||||||
using LiteCharms.Features.Shop.CartPackages.Models;
|
using LiteCharms.Features.Shop.CartPackages.Models;
|
||||||
using LiteCharms.Features.Shop.Postgres;
|
using LiteCharms.Features.Shop.Postgres;
|
||||||
using static LiteCharms.Features.Extensions.Timezones;
|
|
||||||
|
|
||||||
namespace LiteCharms.Features.Shop.CartPackages;
|
namespace LiteCharms.Features.Shop.CartPackages;
|
||||||
|
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ public class LeadService(IDbContextFactory<ShopDbContext> contextFactory)
|
|||||||
FeedItemId = request.FeedItemId,
|
FeedItemId = request.FeedItemId,
|
||||||
Status = LeadStatus.New,
|
Status = LeadStatus.New,
|
||||||
TargetId = request.TargetId,
|
TargetId = request.TargetId,
|
||||||
AttributionHash = GenerateSha256HashString.Invoke($"{request.ClickId}{request.AppClickId}{request.WebClickId}")
|
AttributionHash = StringToSha256Hash.Invoke($"{request.ClickId}{request.AppClickId}{request.WebClickId}")
|
||||||
});
|
});
|
||||||
|
|
||||||
return await context.SaveChangesAsync(cancellationToken) > 0
|
return await context.SaveChangesAsync(cancellationToken) > 0
|
||||||
|
|||||||
Generated
+788
@@ -0,0 +1,788 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using LiteCharms.Features.Shop.Postgres;
|
||||||
|
using LiteCharms.Features.Shop.Products.Models;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace LiteCharms.Features.Shop.Postgres.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(ShopDbContext))]
|
||||||
|
[Migration("20260520191059_AddedProductMetadata")]
|
||||||
|
partial class AddedProductMetadata
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder
|
||||||
|
.HasAnnotation("ProductVersion", "10.0.8")
|
||||||
|
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||||
|
|
||||||
|
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||||
|
|
||||||
|
modelBuilder.Entity("LiteCharms.Features.Shop.CartPackages.Entities.Package", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<bool>("Active")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasDefaultValueSql("now()");
|
||||||
|
|
||||||
|
b.Property<string>("Description")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(2048)
|
||||||
|
.HasColumnType("character varying(2048)");
|
||||||
|
|
||||||
|
b.Property<string>("ImageUrl")
|
||||||
|
.HasMaxLength(2048)
|
||||||
|
.HasColumnType("character varying(2048)");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("Summary")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(512)
|
||||||
|
.HasColumnType("character varying(512)");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("UpdatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("Package", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("LiteCharms.Features.Shop.CartPackages.Entities.PackageItem", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<bool>("Active")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAt")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasDefaultValueSql("now()");
|
||||||
|
|
||||||
|
b.Property<Guid>("PackageId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<Guid>("ProductPriceId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("PackageId");
|
||||||
|
|
||||||
|
b.HasIndex("ProductPriceId");
|
||||||
|
|
||||||
|
b.ToTable("PackageItem", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("LiteCharms.Features.Shop.Customers.Entities.Customer", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<bool>("Active")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("boolean")
|
||||||
|
.HasDefaultValue(true);
|
||||||
|
|
||||||
|
b.Property<string>("Address")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("City")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("Company")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("Country")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasDefaultValueSql("now()");
|
||||||
|
|
||||||
|
b.Property<string>("Discord")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("Email")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("LastName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("LinkedIn")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("Phone")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("PostalCode")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("Region")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("Slack")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("Tax")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("UpdatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<string>("Website")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("Whatsapp")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("Customers", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("LiteCharms.Features.Shop.Leads.Entities.Lead", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<long?>("AdGroupId")
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
b.Property<long?>("AdName")
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
b.Property<string>("AppClickId")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("AttributionHash")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<long?>("CampaignId")
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
b.Property<string>("ClickId")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("ClickLocation")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasDefaultValueSql("now()");
|
||||||
|
|
||||||
|
b.Property<Guid?>("CustomerId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<long?>("FeedItemId")
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
b.Property<string>("Source")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<int>("Status")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<long?>("TargetId")
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("UpdatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<string>("WebClickId")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("CustomerId");
|
||||||
|
|
||||||
|
b.ToTable("Leads", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("LiteCharms.Features.Shop.Notifications.Entities.Notification", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<string>("CorrelationId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<int>("CorrelationIdType")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasDefaultValueSql("now()");
|
||||||
|
|
||||||
|
b.Property<int>("Direction")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.PrimitiveCollection<string>("Errors")
|
||||||
|
.HasColumnType("jsonb");
|
||||||
|
|
||||||
|
b.Property<bool>("HasError")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("boolean")
|
||||||
|
.HasDefaultValue(false);
|
||||||
|
|
||||||
|
b.Property<bool>("IsHtml")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("boolean")
|
||||||
|
.HasDefaultValue(false);
|
||||||
|
|
||||||
|
b.Property<bool>("IsInternal")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("boolean")
|
||||||
|
.HasDefaultValue(true);
|
||||||
|
|
||||||
|
b.Property<string>("Message")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<int>("Platform")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<int>("Priority")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<bool>("Processed")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("boolean")
|
||||||
|
.HasDefaultValue(false);
|
||||||
|
|
||||||
|
b.Property<string>("RecipientAddress")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("RecipientName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("SenderAddress")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("SenderName")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("Subject")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("UpdatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("Notification", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("LiteCharms.Features.Shop.Orders.Entities.Order", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasDefaultValueSql("now()");
|
||||||
|
|
||||||
|
b.Property<Guid>("CustomerId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<string>("InvoiceUrl")
|
||||||
|
.HasMaxLength(2048)
|
||||||
|
.HasColumnType("character varying(2048)");
|
||||||
|
|
||||||
|
b.PrimitiveCollection<string>("Notes")
|
||||||
|
.HasColumnType("jsonb");
|
||||||
|
|
||||||
|
b.PrimitiveCollection<string>("Requirements")
|
||||||
|
.HasColumnType("jsonb");
|
||||||
|
|
||||||
|
b.Property<int>("Status")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.PrimitiveCollection<string>("Terms")
|
||||||
|
.HasColumnType("jsonb");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("UpdatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("CustomerId");
|
||||||
|
|
||||||
|
b.ToTable("Orders", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("LiteCharms.Features.Shop.Orders.Entities.OrderRefund", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<decimal>("Amount")
|
||||||
|
.HasPrecision(18, 2)
|
||||||
|
.HasColumnType("numeric(18,2)");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasDefaultValueSql("now()");
|
||||||
|
|
||||||
|
b.Property<Guid>("OrderId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<string>("Reason")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("OrderId");
|
||||||
|
|
||||||
|
b.ToTable("OrderRefunds", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("LiteCharms.Features.Shop.Products.Entities.Product", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<bool>("Active")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("boolean")
|
||||||
|
.HasDefaultValue(false);
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasDefaultValueSql("now()");
|
||||||
|
|
||||||
|
b.Property<string>("Description")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(2048)
|
||||||
|
.HasColumnType("character varying(2048)");
|
||||||
|
|
||||||
|
b.Property<string>("ImageUrl")
|
||||||
|
.HasMaxLength(2048)
|
||||||
|
.HasColumnType("character varying(2048)");
|
||||||
|
|
||||||
|
b.Property<ProductMetadata>("Metadata")
|
||||||
|
.HasColumnType("jsonb");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("Summary")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(512)
|
||||||
|
.HasColumnType("character varying(512)");
|
||||||
|
|
||||||
|
b.PrimitiveCollection<string>("Thumbnails")
|
||||||
|
.HasColumnType("jsonb");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("UpdatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("Products", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("LiteCharms.Features.Shop.Products.Entities.ProductPrice", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<bool>("Active")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasDefaultValueSql("now()");
|
||||||
|
|
||||||
|
b.Property<decimal>("Discount")
|
||||||
|
.HasPrecision(18, 2)
|
||||||
|
.HasColumnType("numeric(18,2)");
|
||||||
|
|
||||||
|
b.Property<decimal>("Price")
|
||||||
|
.HasPrecision(18, 2)
|
||||||
|
.HasColumnType("numeric(18,2)");
|
||||||
|
|
||||||
|
b.Property<Guid>("ProductId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("UpdatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("ProductId");
|
||||||
|
|
||||||
|
b.ToTable("ProductPrices", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("LiteCharms.Features.Shop.Quotes.Entities.Quote", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasDefaultValueSql("now()");
|
||||||
|
|
||||||
|
b.Property<Guid>("CustomerId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("ExpiredAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<string>("InvoiceUrl")
|
||||||
|
.HasMaxLength(2048)
|
||||||
|
.HasColumnType("character varying(2048)");
|
||||||
|
|
||||||
|
b.Property<Guid?>("OrderId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<string>("Reason")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<Guid?>("ShoppingCartId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<int>("Status")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("UpdatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("CustomerId");
|
||||||
|
|
||||||
|
b.HasIndex("OrderId")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.HasIndex("ShoppingCartId")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("Quotes", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("LiteCharms.Features.Shop.ShoppingCarts.Entities.ShoppingCart", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasDefaultValueSql("now()");
|
||||||
|
|
||||||
|
b.Property<Guid>("CustomerId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<Guid?>("OrderId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("UpdatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("CustomerId");
|
||||||
|
|
||||||
|
b.HasIndex("OrderId")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("ShoppingCarts", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("LiteCharms.Features.Shop.ShoppingCarts.Entities.ShoppingCartItem", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasDefaultValueSql("now()");
|
||||||
|
|
||||||
|
b.Property<Guid>("ProductPriceId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<int>("Quantity")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasDefaultValue(1);
|
||||||
|
|
||||||
|
b.Property<Guid>("ShoppingCartId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("UpdatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("ProductPriceId");
|
||||||
|
|
||||||
|
b.HasIndex("ShoppingCartId");
|
||||||
|
|
||||||
|
b.ToTable("ShoppingCartItems", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("LiteCharms.Features.Shop.ShoppingCarts.Entities.ShoppingCartPackage", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasDefaultValueSql("now()");
|
||||||
|
|
||||||
|
b.Property<Guid>("PackageId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<Guid>("ShoppingCartId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("PackageId");
|
||||||
|
|
||||||
|
b.HasIndex("ShoppingCartId");
|
||||||
|
|
||||||
|
b.ToTable("ShoppingCartPackages", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("LiteCharms.Features.Shop.CartPackages.Entities.PackageItem", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("LiteCharms.Features.Shop.CartPackages.Entities.Package", "Package")
|
||||||
|
.WithMany("PackageItems")
|
||||||
|
.HasForeignKey("PackageId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("LiteCharms.Features.Shop.Products.Entities.ProductPrice", "ProductPrice")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("ProductPriceId")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Package");
|
||||||
|
|
||||||
|
b.Navigation("ProductPrice");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("LiteCharms.Features.Shop.Leads.Entities.Lead", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("LiteCharms.Features.Shop.Customers.Entities.Customer", "Customer")
|
||||||
|
.WithMany("Leads")
|
||||||
|
.HasForeignKey("CustomerId")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict);
|
||||||
|
|
||||||
|
b.Navigation("Customer");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("LiteCharms.Features.Shop.Orders.Entities.Order", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("LiteCharms.Features.Shop.Customers.Entities.Customer", "Customer")
|
||||||
|
.WithMany("Orders")
|
||||||
|
.HasForeignKey("CustomerId")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Customer");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("LiteCharms.Features.Shop.Orders.Entities.OrderRefund", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("LiteCharms.Features.Shop.Orders.Entities.Order", "Order")
|
||||||
|
.WithMany("Refunds")
|
||||||
|
.HasForeignKey("OrderId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Order");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("LiteCharms.Features.Shop.Products.Entities.ProductPrice", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("LiteCharms.Features.Shop.Products.Entities.Product", "Product")
|
||||||
|
.WithMany("ProductPrices")
|
||||||
|
.HasForeignKey("ProductId")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Product");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("LiteCharms.Features.Shop.Quotes.Entities.Quote", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("LiteCharms.Features.Shop.Customers.Entities.Customer", "Customer")
|
||||||
|
.WithMany("Quotes")
|
||||||
|
.HasForeignKey("CustomerId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("LiteCharms.Features.Shop.Orders.Entities.Order", "Order")
|
||||||
|
.WithOne("Quote")
|
||||||
|
.HasForeignKey("LiteCharms.Features.Shop.Quotes.Entities.Quote", "OrderId");
|
||||||
|
|
||||||
|
b.HasOne("LiteCharms.Features.Shop.ShoppingCarts.Entities.ShoppingCart", "ShoppingCart")
|
||||||
|
.WithOne("Quote")
|
||||||
|
.HasForeignKey("LiteCharms.Features.Shop.Quotes.Entities.Quote", "ShoppingCartId");
|
||||||
|
|
||||||
|
b.Navigation("Customer");
|
||||||
|
|
||||||
|
b.Navigation("Order");
|
||||||
|
|
||||||
|
b.Navigation("ShoppingCart");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("LiteCharms.Features.Shop.ShoppingCarts.Entities.ShoppingCart", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("LiteCharms.Features.Shop.Customers.Entities.Customer", "Customer")
|
||||||
|
.WithMany("ShoppingCarts")
|
||||||
|
.HasForeignKey("CustomerId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("LiteCharms.Features.Shop.Orders.Entities.Order", "Order")
|
||||||
|
.WithOne("ShoppingCart")
|
||||||
|
.HasForeignKey("LiteCharms.Features.Shop.ShoppingCarts.Entities.ShoppingCart", "OrderId")
|
||||||
|
.OnDelete(DeleteBehavior.SetNull);
|
||||||
|
|
||||||
|
b.Navigation("Customer");
|
||||||
|
|
||||||
|
b.Navigation("Order");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("LiteCharms.Features.Shop.ShoppingCarts.Entities.ShoppingCartItem", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("LiteCharms.Features.Shop.Products.Entities.ProductPrice", "ProductPrice")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("ProductPriceId")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("LiteCharms.Features.Shop.ShoppingCarts.Entities.ShoppingCart", "ShoppingCart")
|
||||||
|
.WithMany("ShoppingCartItems")
|
||||||
|
.HasForeignKey("ShoppingCartId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("ProductPrice");
|
||||||
|
|
||||||
|
b.Navigation("ShoppingCart");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("LiteCharms.Features.Shop.ShoppingCarts.Entities.ShoppingCartPackage", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("LiteCharms.Features.Shop.CartPackages.Entities.Package", "Package")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("PackageId")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("LiteCharms.Features.Shop.ShoppingCarts.Entities.ShoppingCart", "ShoppingCart")
|
||||||
|
.WithMany("ShoppingCartPackages")
|
||||||
|
.HasForeignKey("ShoppingCartId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Package");
|
||||||
|
|
||||||
|
b.Navigation("ShoppingCart");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("LiteCharms.Features.Shop.CartPackages.Entities.Package", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("PackageItems");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("LiteCharms.Features.Shop.Customers.Entities.Customer", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Leads");
|
||||||
|
|
||||||
|
b.Navigation("Orders");
|
||||||
|
|
||||||
|
b.Navigation("Quotes");
|
||||||
|
|
||||||
|
b.Navigation("ShoppingCarts");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("LiteCharms.Features.Shop.Orders.Entities.Order", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Quote");
|
||||||
|
|
||||||
|
b.Navigation("Refunds");
|
||||||
|
|
||||||
|
b.Navigation("ShoppingCart");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("LiteCharms.Features.Shop.Products.Entities.Product", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("ProductPrices");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("LiteCharms.Features.Shop.ShoppingCarts.Entities.ShoppingCart", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Quote");
|
||||||
|
|
||||||
|
b.Navigation("ShoppingCartItems");
|
||||||
|
|
||||||
|
b.Navigation("ShoppingCartPackages");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
using System;
|
||||||
|
using LiteCharms.Features.Shop.Products.Models;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace LiteCharms.Features.Shop.Postgres.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddedProductMetadata : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AlterColumn<bool>(
|
||||||
|
name: "Active",
|
||||||
|
table: "Products",
|
||||||
|
type: "boolean",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: false,
|
||||||
|
oldClrType: typeof(bool),
|
||||||
|
oldType: "boolean",
|
||||||
|
oldDefaultValue: true);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<DateTime>(
|
||||||
|
name: "CreatedAt",
|
||||||
|
table: "Products",
|
||||||
|
type: "timestamp with time zone",
|
||||||
|
nullable: false,
|
||||||
|
defaultValueSql: "now()");
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<ProductMetadata>(
|
||||||
|
name: "Metadata",
|
||||||
|
table: "Products",
|
||||||
|
type: "jsonb",
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<DateTime>(
|
||||||
|
name: "UpdatedAt",
|
||||||
|
table: "Products",
|
||||||
|
type: "timestamp with time zone",
|
||||||
|
nullable: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "CreatedAt",
|
||||||
|
table: "Products");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "Metadata",
|
||||||
|
table: "Products");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "UpdatedAt",
|
||||||
|
table: "Products");
|
||||||
|
|
||||||
|
migrationBuilder.AlterColumn<bool>(
|
||||||
|
name: "Active",
|
||||||
|
table: "Products",
|
||||||
|
type: "boolean",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: true,
|
||||||
|
oldClrType: typeof(bool),
|
||||||
|
oldType: "boolean",
|
||||||
|
oldDefaultValue: false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
// <auto-generated />
|
// <auto-generated />
|
||||||
using System;
|
using System;
|
||||||
using LiteCharms.Features.Shop.Postgres;
|
using LiteCharms.Features.Shop.Postgres;
|
||||||
|
using LiteCharms.Features.Shop.Products.Models;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
@@ -386,7 +387,12 @@ namespace LiteCharms.Features.Shop.Postgres.Migrations
|
|||||||
b.Property<bool>("Active")
|
b.Property<bool>("Active")
|
||||||
.ValueGeneratedOnAdd()
|
.ValueGeneratedOnAdd()
|
||||||
.HasColumnType("boolean")
|
.HasColumnType("boolean")
|
||||||
.HasDefaultValue(true);
|
.HasDefaultValue(false);
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasDefaultValueSql("now()");
|
||||||
|
|
||||||
b.Property<string>("Description")
|
b.Property<string>("Description")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
@@ -397,6 +403,9 @@ namespace LiteCharms.Features.Shop.Postgres.Migrations
|
|||||||
.HasMaxLength(2048)
|
.HasMaxLength(2048)
|
||||||
.HasColumnType("character varying(2048)");
|
.HasColumnType("character varying(2048)");
|
||||||
|
|
||||||
|
b.Property<ProductMetadata>("Metadata")
|
||||||
|
.HasColumnType("jsonb");
|
||||||
|
|
||||||
b.Property<string>("Name")
|
b.Property<string>("Name")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasColumnType("text");
|
.HasColumnType("text");
|
||||||
@@ -409,6 +418,9 @@ namespace LiteCharms.Features.Shop.Postgres.Migrations
|
|||||||
b.PrimitiveCollection<string>("Thumbnails")
|
b.PrimitiveCollection<string>("Thumbnails")
|
||||||
.HasColumnType("jsonb");
|
.HasColumnType("jsonb");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("UpdatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
b.HasKey("Id");
|
b.HasKey("Id");
|
||||||
|
|
||||||
b.ToTable("Products", (string)null);
|
b.ToTable("Products", (string)null);
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
namespace LiteCharms.Features.Shop.Postgres;
|
using static LiteCharms.Features.Extensions.Postgres;
|
||||||
|
|
||||||
|
namespace LiteCharms.Features.Shop.Postgres;
|
||||||
|
|
||||||
public class ShopDbContextFactory : IDesignTimeDbContextFactory<ShopDbContext>
|
public class ShopDbContextFactory : IDesignTimeDbContextFactory<ShopDbContext>
|
||||||
{
|
{
|
||||||
@@ -12,7 +14,7 @@ public class ShopDbContextFactory : IDesignTimeDbContextFactory<ShopDbContext>
|
|||||||
.Build();
|
.Build();
|
||||||
|
|
||||||
var optionsBuilder = new DbContextOptionsBuilder<ShopDbContext>();
|
var optionsBuilder = new DbContextOptionsBuilder<ShopDbContext>();
|
||||||
optionsBuilder.UseNpgsql(configuration.GetConnectionString("PostgresShop"));
|
optionsBuilder.UseNpgsql(configuration.GetConnectionString(ShopDbConfigName));
|
||||||
|
|
||||||
return new ShopDbContext(optionsBuilder.Options);
|
return new ShopDbContext(optionsBuilder.Options);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,10 +8,13 @@ public class ProductConfiguration : IEntityTypeConfiguration<Product>
|
|||||||
|
|
||||||
builder.HasKey(f => f.Id);
|
builder.HasKey(f => f.Id);
|
||||||
builder.Property(f => f.Name).IsRequired();
|
builder.Property(f => f.Name).IsRequired();
|
||||||
|
builder.Property(f => f.CreatedAt).IsRequired().ValueGeneratedOnAdd().HasDefaultValueSql("now()");
|
||||||
|
builder.Property(f => f.UpdatedAt).IsRequired(false);
|
||||||
builder.Property(f => f.Summary).IsRequired().HasMaxLength(512);
|
builder.Property(f => f.Summary).IsRequired().HasMaxLength(512);
|
||||||
builder.Property(f => f.Description).IsRequired().HasMaxLength(2048);
|
builder.Property(f => f.Description).IsRequired().HasMaxLength(2048);
|
||||||
builder.Property(f => f.ImageUrl).IsRequired(false).HasMaxLength(2048);
|
builder.Property(f => f.ImageUrl).IsRequired(false).HasMaxLength(2048);
|
||||||
builder.Property(f => f.Thumbnails).HasColumnType("jsonb").IsRequired(false);
|
builder.Property(f => f.Thumbnails).HasColumnType("jsonb").IsRequired(false);
|
||||||
builder.Property(f => f.Active).HasDefaultValue(true);
|
builder.Property(f => f.Active).HasDefaultValue(false);
|
||||||
|
builder.Property(f => f.Metadata).HasColumnType("jsonb").IsRequired(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ public class ProductPriceConfiguration : IEntityTypeConfiguration<ProductPrice>
|
|||||||
builder.ToTable("ProductPrices");
|
builder.ToTable("ProductPrices");
|
||||||
|
|
||||||
builder.HasKey(f => f.Id);
|
builder.HasKey(f => f.Id);
|
||||||
builder.Property(f => f.CreatedAt).ValueGeneratedOnAdd().HasDefaultValueSql("now()");
|
builder.Property(f => f.CreatedAt).IsRequired().ValueGeneratedOnAdd().HasDefaultValueSql("now()");
|
||||||
builder.Property(f => f.UpdatedAt).IsRequired(false);
|
builder.Property(f => f.UpdatedAt).IsRequired(false);
|
||||||
builder.Property(f => f.ProductId).IsRequired();
|
builder.Property(f => f.ProductId).IsRequired();
|
||||||
builder.Property(f => f.Price).IsRequired().HasPrecision(18, 2);
|
builder.Property(f => f.Price).IsRequired().HasPrecision(18, 2);
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
namespace LiteCharms.Features.Shop.Products.Models;
|
||||||
|
|
||||||
|
public class CreateProductModel
|
||||||
|
{
|
||||||
|
[MaxLength(128)]
|
||||||
|
[Required(ErrorMessage = "Product name is required.")]
|
||||||
|
public string? Name { get; set; }
|
||||||
|
|
||||||
|
[MaxLength(512)]
|
||||||
|
[Required(ErrorMessage = "Summary is required.")]
|
||||||
|
public string? Summary { get; set; }
|
||||||
|
|
||||||
|
[MaxLength(2048)]
|
||||||
|
[Required(ErrorMessage = "Description is required.")]
|
||||||
|
public string? Description { get; set; }
|
||||||
|
|
||||||
|
[Range(0.01, double.MaxValue, ErrorMessage = "Price must be greater than zero.")]
|
||||||
|
public decimal Price { get; set; }
|
||||||
|
|
||||||
|
[MaxLength(128)]
|
||||||
|
[Required(ErrorMessage = "Author metadata is required.")]
|
||||||
|
public string? Author { get; set; }
|
||||||
|
|
||||||
|
[Required(ErrorMessage = "Publication Date is required.")]
|
||||||
|
public DateTime PublishDate { get; set; } = DateTime.Today;
|
||||||
|
|
||||||
|
[MaxLength(255)]
|
||||||
|
[Required(ErrorMessage = "Copyright Information field is required.")]
|
||||||
|
public string? CopyrightInfo { get; set; }
|
||||||
|
|
||||||
|
[MaxLength(128)]
|
||||||
|
[Required(ErrorMessage = "ISBN code is required.")]
|
||||||
|
[RegularExpression(@"^(?=(?:\D*\d){10}(?:(?:\D*\d){3})?$)[\d-]+$", ErrorMessage = "Please enter a valid ISBN-10 or ISBN-13 string.")]
|
||||||
|
public string? Isbn { get; set; }
|
||||||
|
|
||||||
|
[Required(ErrorMessage = "Primary image is required.")]
|
||||||
|
public string? ImageUrl { get; set; }
|
||||||
|
|
||||||
|
public List<string> Thumbnails { get; set; } = [];
|
||||||
|
}
|
||||||
@@ -4,6 +4,10 @@ public class Product
|
|||||||
{
|
{
|
||||||
public Guid Id { get; set; }
|
public Guid Id { get; set; }
|
||||||
|
|
||||||
|
public DateTime CreatedAt { get; set; }
|
||||||
|
|
||||||
|
public DateTime? UpdatedAt { get; set; }
|
||||||
|
|
||||||
public string? Name { get; set; }
|
public string? Name { get; set; }
|
||||||
|
|
||||||
public string? Summary { get; set; }
|
public string? Summary { get; set; }
|
||||||
@@ -15,4 +19,6 @@ public class Product
|
|||||||
public string[]? Thumbnails { get; set; }
|
public string[]? Thumbnails { get; set; }
|
||||||
|
|
||||||
public bool Active { get; set; }
|
public bool Active { get; set; }
|
||||||
|
|
||||||
|
public ProductMetadata? Metadata { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
namespace LiteCharms.Features.Shop.Products.Models;
|
||||||
|
|
||||||
|
public class ProductMetadata
|
||||||
|
{
|
||||||
|
public string? Manufacturer { get; set; }
|
||||||
|
|
||||||
|
public string? ManufactureDate { get; set; }
|
||||||
|
|
||||||
|
public string? CopyrightInfo { get; set; }
|
||||||
|
|
||||||
|
public string? SerialNumber { get; set; }
|
||||||
|
}
|
||||||
@@ -11,4 +11,6 @@ public record CreateProduct
|
|||||||
public required string ImageUrl { get; set; }
|
public required string ImageUrl { get; set; }
|
||||||
|
|
||||||
public string[]? Thumbnails { get; set; }
|
public string[]? Thumbnails { get; set; }
|
||||||
|
|
||||||
|
public ProductMetadata? Metadata { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -70,7 +70,8 @@ public class ProductService(IDbContextFactory<ShopDbContext> contextFactory)
|
|||||||
Summary = request.Summary,
|
Summary = request.Summary,
|
||||||
Description = request.Description,
|
Description = request.Description,
|
||||||
ImageUrl = request.ImageUrl,
|
ImageUrl = request.ImageUrl,
|
||||||
Thumbnails = request.Thumbnails
|
Thumbnails = request.Thumbnails,
|
||||||
|
Metadata = request.Metadata
|
||||||
});
|
});
|
||||||
|
|
||||||
return await context.SaveChangesAsync(cancellationToken) > 0
|
return await context.SaveChangesAsync(cancellationToken) > 0
|
||||||
@@ -206,7 +207,7 @@ public class ProductService(IDbContextFactory<ShopDbContext> contextFactory)
|
|||||||
|
|
||||||
var result = await CreateProductPriceAsync(existingPrice.ProductId, price, discount, cancellationToken);
|
var result = await CreateProductPriceAsync(existingPrice.ProductId, price, discount, cancellationToken);
|
||||||
|
|
||||||
if(result.IsFailed)
|
if (result.IsFailed)
|
||||||
{
|
{
|
||||||
var deactivatedPrice = await context.ProductPrices.FirstOrDefaultAsync(p => p.Id == productPriceId, cancellationToken);
|
var deactivatedPrice = await context.ProductPrices.FirstOrDefaultAsync(p => p.Id == productPriceId, cancellationToken);
|
||||||
|
|
||||||
@@ -225,4 +226,52 @@ public class ProductService(IDbContextFactory<ShopDbContext> contextFactory)
|
|||||||
return Result.Fail(new Error(ex.Message).CausedBy(ex));
|
return Result.Fail(new Error(ex.Message).CausedBy(ex));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async ValueTask<Result> SetProductPriceStatusAsync(Guid productPriceId, bool active, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
|
||||||
|
|
||||||
|
var productPrice = await context.ProductPrices.FirstOrDefaultAsync(p => p.Id == productPriceId, cancellationToken);
|
||||||
|
|
||||||
|
if (productPrice is null)
|
||||||
|
return Result.Fail($"Could not find product price with ID {productPriceId}");
|
||||||
|
|
||||||
|
productPrice.Active = active;
|
||||||
|
productPrice.UpdatedAt = DateTime.UtcNow;
|
||||||
|
|
||||||
|
return await context.SaveChangesAsync(cancellationToken) > 0
|
||||||
|
? Result.Ok()
|
||||||
|
: Result.Fail($"Failed to change product price status by ID {productPriceId}");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return Result.Fail(new Error(ex.Message).CausedBy(ex));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ValueTask<Result> UpdateProductMetadataAsync(Guid productId, ProductMetadata metadata, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
|
||||||
|
|
||||||
|
var product = await context.Products.FirstOrDefaultAsync(p => p.Id == productId, cancellationToken);
|
||||||
|
|
||||||
|
if (product is null)
|
||||||
|
return Result.Fail($"Could not find product with ID {productId}");
|
||||||
|
|
||||||
|
product.Metadata = metadata;
|
||||||
|
product.UpdatedAt = DateTime.UtcNow;
|
||||||
|
|
||||||
|
return await context.SaveChangesAsync(cancellationToken) > 0
|
||||||
|
? Result.Ok()
|
||||||
|
: Result.Fail($"Failed to update product metadata by ID {productId}");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return Result.Fail(new Error(ex.Message).CausedBy(ex));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user