Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b369dad452 | |||
| ac31c6ada8 | |||
| c938bfec09 | |||
| 5eb6dbc8b2 | |||
| adc14038db | |||
| afc984f3bc | |||
| 61cb6c2228 | |||
| f1ef614cbb | |||
| 8fe129e19c | |||
| c4f73fd999 | |||
| 48f4cd45f1 | |||
| f88cc42a88 | |||
| 0e21ec283d | |||
| e7a798b5e9 | |||
| 494b806744 |
@@ -3,7 +3,7 @@ using LiteCharms.Features.MidrandBooks.Products;
|
|||||||
|
|
||||||
namespace LiteCharms.Features.MidrandBooks.Seed;
|
namespace LiteCharms.Features.MidrandBooks.Seed;
|
||||||
|
|
||||||
public class CategorySeederService(CategoryService categoryService, ProductService productService, IFeatureManager features,
|
public sealed class CategorySeederService(CategoryService categoryService, ProductService productService, IFeatureManager features,
|
||||||
ILogger<CategorySeederService> logger) : BackgroundService
|
ILogger<CategorySeederService> logger) : BackgroundService
|
||||||
{
|
{
|
||||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
namespace LiteCharms.Features.MidrandBooks.Seed.Configuration;
|
namespace LiteCharms.Features.MidrandBooks.Seed.Configuration;
|
||||||
|
|
||||||
public class CdnSettings
|
public sealed class CdnSettings
|
||||||
{
|
{
|
||||||
public string? BaseCdn { get; set; }
|
public string? BaseCdn { get; set; }
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ using LiteCharms.Features.MidrandBooks.Orders.Models;
|
|||||||
|
|
||||||
namespace LiteCharms.Features.MidrandBooks.Seed;
|
namespace LiteCharms.Features.MidrandBooks.Seed;
|
||||||
|
|
||||||
public class CustomerSeederService(CustomerService customerService, OrderService orderService, IFeatureManager features,
|
public sealed class CustomerSeederService(CustomerService customerService, OrderService orderService, IFeatureManager features,
|
||||||
ILogger<CustomerSeederService> logger) : BackgroundService
|
ILogger<CustomerSeederService> logger) : BackgroundService
|
||||||
{
|
{
|
||||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ using LiteCharms.Features.MidrandBooks.Seed.Configuration;
|
|||||||
|
|
||||||
namespace LiteCharms.Features.MidrandBooks.Seed;
|
namespace LiteCharms.Features.MidrandBooks.Seed;
|
||||||
|
|
||||||
public class ProductsSeederService(ProductService productService, AuthorService authorService, BooksService booksService,
|
public sealed class ProductsSeederService(ProductService productService, AuthorService authorService, BooksService booksService,
|
||||||
IFeatureManager features, IOptions<CdnSettings> options, ILogger<ProductsSeederService> logger) : BackgroundService
|
IFeatureManager features, IOptions<CdnSettings> options, ILogger<ProductsSeederService> logger) : BackgroundService
|
||||||
{
|
{
|
||||||
private readonly CdnSettings cdnSettings = options.Value;
|
private readonly CdnSettings cdnSettings = options.Value;
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ using LiteCharms.Features.Models;
|
|||||||
|
|
||||||
namespace LiteCharms.Features.MidrandBooks.Tests;
|
namespace LiteCharms.Features.MidrandBooks.Tests;
|
||||||
|
|
||||||
public class AuthorServiceFeatureTests(Fixture fixture, ITestOutputHelper output) : IClassFixture<Fixture>
|
public class AuthorServiceFeatureTests(Fixture fixture) : IClassFixture<Fixture>
|
||||||
{
|
{
|
||||||
private readonly AuthorService authorService = fixture.Services.GetRequiredService<AuthorService>();
|
private readonly AuthorService authorService = fixture.Services.GetRequiredService<AuthorService>();
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
using LiteCharms.Features.Extensions;
|
using LiteCharms.Features.Extensions;
|
||||||
using LiteCharms.Features.MidrandBooks.Abstractions;
|
|
||||||
using LiteCharms.Features.MidrandBooks.Extensions;
|
using LiteCharms.Features.MidrandBooks.Extensions;
|
||||||
|
|
||||||
namespace LiteCharms.Features.MidrandBooks.Tests.Common;
|
namespace LiteCharms.Features.MidrandBooks.Tests.Common;
|
||||||
|
|||||||
@@ -0,0 +1,98 @@
|
|||||||
|
using LiteCharms.Features.MidrandBooks.Payments;
|
||||||
|
using LiteCharms.Features.MidrandBooks.Payments.Models;
|
||||||
|
using LiteCharms.Features.MidrandBooks.Tests.Common;
|
||||||
|
|
||||||
|
namespace LiteCharms.Features.MidrandBooks.Tests;
|
||||||
|
|
||||||
|
public sealed class PaymentServiceFeatureTests(Fixture fixture) : IClassFixture<Fixture>
|
||||||
|
{
|
||||||
|
private readonly PaymentService paymentService = fixture.Services.GetRequiredService<PaymentService>();
|
||||||
|
|
||||||
|
[IntegrationFact]
|
||||||
|
public async Task CreateRefundAsync_ShouldReturn_ResultWithRefundId()
|
||||||
|
{
|
||||||
|
var request = new CreateRefund
|
||||||
|
{
|
||||||
|
Amount = 50,
|
||||||
|
OrderId = 2,
|
||||||
|
Type = RefundTypes.Partial,
|
||||||
|
Reason = "Returned damaged book",
|
||||||
|
Status = RefundStatus.Completed,
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = await paymentService.CreateRefundAsync(request, fixture.CancellationToken);
|
||||||
|
|
||||||
|
Assert.True(result.IsSuccess);
|
||||||
|
Assert.True(result.Value > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[IntegrationFact]
|
||||||
|
public async Task WriteLedgerEntryAsync_ShouldReturn_ResultWithSuccess()
|
||||||
|
{
|
||||||
|
var request = new CreateLedgerEntry
|
||||||
|
{
|
||||||
|
CustomerId = 1,
|
||||||
|
OrderId = 1,
|
||||||
|
PaymentGatewayId = 1,
|
||||||
|
PaymentGatewayReference = "TEST REFERENCE",
|
||||||
|
PaymentId = 1,
|
||||||
|
Status = LedgerStatuses.Received,
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = await paymentService.WriteLedgerEntryAsync(request, fixture.CancellationToken);
|
||||||
|
|
||||||
|
Assert.True(result.IsSuccess);
|
||||||
|
}
|
||||||
|
|
||||||
|
[IntegrationFact]
|
||||||
|
public async Task GetPaymentGatewayAsync_ShouldReturn_ResultWithPaymentGateway()
|
||||||
|
{
|
||||||
|
var result = await paymentService.GetPaymentGatewayAsync(1, fixture.CancellationToken);
|
||||||
|
|
||||||
|
Assert.True(result.IsSuccess);
|
||||||
|
Assert.NotNull(result.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
[IntegrationFact]
|
||||||
|
public async Task CreatePaymentGatewayAsync_ShouldReturn_ResultWithGatewayId()
|
||||||
|
{
|
||||||
|
var request = new CreatePaymentGateway
|
||||||
|
{
|
||||||
|
IsSandbox = true,
|
||||||
|
MerchantId = "10049307",
|
||||||
|
MerchantKey = "ju6navn0jcbf0",
|
||||||
|
Name = "Payfast",
|
||||||
|
Website = "https://sandbox.payfast.co.za/eng/process",
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = await paymentService.CreatePaymentGatewayAsync(request, fixture.CancellationToken);
|
||||||
|
|
||||||
|
Assert.True(result.IsSuccess);
|
||||||
|
Assert.True(result.Value > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[IntegrationFact]
|
||||||
|
public async Task CompletePaymentAsync_ShouldReturn_ResultWithSuccess()
|
||||||
|
{
|
||||||
|
var result = await paymentService.CompletePaymentAsync(1, PaymentStatuses.Paid, fixture.CancellationToken);
|
||||||
|
|
||||||
|
Assert.True(result.IsSuccess);
|
||||||
|
}
|
||||||
|
|
||||||
|
[IntegrationFact]
|
||||||
|
public async Task UpdatePaymentAsync_ShouldReturn_ResultWithSuccess()
|
||||||
|
{
|
||||||
|
var result = await paymentService.UpdatePaymentAsync(1, 200, fixture.CancellationToken);
|
||||||
|
|
||||||
|
Assert.True(result.IsSuccess);
|
||||||
|
}
|
||||||
|
|
||||||
|
[IntegrationFact]
|
||||||
|
public async Task CreatePaymentAsync_ShouldReturn_ResultWithPaymentId()
|
||||||
|
{
|
||||||
|
var result = await paymentService.CreatePaymentAsync(100, 1, "HASHEDID", fixture.CancellationToken);
|
||||||
|
|
||||||
|
Assert.True(result.IsSuccess);
|
||||||
|
Assert.True(result.Value > 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,6 +9,47 @@ public class ProductServiceFeatureTests(Fixture fixture, ITestOutputHelper outpu
|
|||||||
{
|
{
|
||||||
private readonly ProductService productService = fixture.Services.GetRequiredService<ProductService>();
|
private readonly ProductService productService = fixture.Services.GetRequiredService<ProductService>();
|
||||||
|
|
||||||
|
[IntegrationFact]
|
||||||
|
public async Task CheckProductStockAvailabilityAsync_ShouldReturn_ResultWithProductInventory()
|
||||||
|
{
|
||||||
|
var result = await productService.CheckProductStockAvailabilityAsync(1, 1, fixture.CancellationToken);
|
||||||
|
|
||||||
|
Assert.True(result.IsSuccess);
|
||||||
|
Assert.NotNull(result.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
[IntegrationFact]
|
||||||
|
public async Task ReserveProductInventoryAsync_ShouldReturn_ResultWithSuccess()
|
||||||
|
{
|
||||||
|
var request = new ReserveStock
|
||||||
|
{
|
||||||
|
ProductId = 1,
|
||||||
|
ProductPriceId = 1,
|
||||||
|
Reservation = 100,
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = await productService.ReserveProductInventoryAsync(request, fixture.CancellationToken);
|
||||||
|
|
||||||
|
Assert.True(result.IsSuccess);
|
||||||
|
Assert.True(result.Value > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[IntegrationFact]
|
||||||
|
public async Task AllocateProductInventoryAsync_ShouldReturn_ResultWithSuccess()
|
||||||
|
{
|
||||||
|
var request = new AllocateStock
|
||||||
|
{
|
||||||
|
ProductId = 1,
|
||||||
|
ProductPriceId = 1,
|
||||||
|
Allocation = 500,
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = await productService.AllocateProductInventoryAsync(request, fixture.CancellationToken);
|
||||||
|
|
||||||
|
Assert.True(result.IsSuccess);
|
||||||
|
Assert.True(result.Value > 0);
|
||||||
|
}
|
||||||
|
|
||||||
[IntegrationFact]
|
[IntegrationFact]
|
||||||
public async Task AddProductCategoryAsync_ShouldReturn_ResultWithId()
|
public async Task AddProductCategoryAsync_ShouldReturn_ResultWithId()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
namespace LiteCharms.Features.MidrandBooks.Abstractions;
|
|
||||||
|
|
||||||
public interface IService;
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
using LiteCharms.Features.MidrandBooks.Abstractions;
|
using LiteCharms.Features.Abstractions;
|
||||||
using LiteCharms.Features.MidrandBooks.AuthorBooks.Models;
|
using LiteCharms.Features.MidrandBooks.AuthorBooks.Models;
|
||||||
using LiteCharms.Features.MidrandBooks.Extensions;
|
using LiteCharms.Features.MidrandBooks.Extensions;
|
||||||
using LiteCharms.Features.MidrandBooks.Postgres;
|
using LiteCharms.Features.MidrandBooks.Postgres;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using LiteCharms.Features.MidrandBooks.Abstractions;
|
using LiteCharms.Features.Abstractions;
|
||||||
using LiteCharms.Features.MidrandBooks.Authors.Models;
|
using LiteCharms.Features.MidrandBooks.Authors.Models;
|
||||||
using LiteCharms.Features.MidrandBooks.Extensions;
|
using LiteCharms.Features.MidrandBooks.Extensions;
|
||||||
using LiteCharms.Features.MidrandBooks.Postgres;
|
using LiteCharms.Features.MidrandBooks.Postgres;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using LiteCharms.Features.MidrandBooks.Abstractions;
|
using LiteCharms.Features.Abstractions;
|
||||||
using LiteCharms.Features.MidrandBooks.Categories.Models;
|
using LiteCharms.Features.MidrandBooks.Categories.Models;
|
||||||
using LiteCharms.Features.MidrandBooks.Extensions;
|
using LiteCharms.Features.MidrandBooks.Extensions;
|
||||||
using LiteCharms.Features.MidrandBooks.Postgres;
|
using LiteCharms.Features.MidrandBooks.Postgres;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using LiteCharms.Features.MidrandBooks.Abstractions;
|
using LiteCharms.Features.Abstractions;
|
||||||
using LiteCharms.Features.MidrandBooks.Customers.Models;
|
using LiteCharms.Features.MidrandBooks.Customers.Models;
|
||||||
using LiteCharms.Features.MidrandBooks.Extensions;
|
using LiteCharms.Features.MidrandBooks.Extensions;
|
||||||
using LiteCharms.Features.MidrandBooks.Postgres;
|
using LiteCharms.Features.MidrandBooks.Postgres;
|
||||||
|
|||||||
@@ -1,62 +0,0 @@
|
|||||||
namespace LiteCharms.Features.MidrandBooks;
|
|
||||||
|
|
||||||
public enum PublisherTypes : int
|
|
||||||
{
|
|
||||||
Individual = 0,
|
|
||||||
Company = 1,
|
|
||||||
Organization = 2,
|
|
||||||
SelfPublished = 3,
|
|
||||||
UniversityPress = 4,
|
|
||||||
GovernmentAgency = 5,
|
|
||||||
NonProfit = 6,
|
|
||||||
Independent = 7
|
|
||||||
}
|
|
||||||
|
|
||||||
public enum BookTypes : int
|
|
||||||
{
|
|
||||||
Fiction = 0,
|
|
||||||
NonFiction = 1,
|
|
||||||
Academic = 2,
|
|
||||||
SelfHelp = 3,
|
|
||||||
Biography = 4,
|
|
||||||
Poetry = 5,
|
|
||||||
Children = 6,
|
|
||||||
YoungAdult = 7,
|
|
||||||
ScienceFiction = 8,
|
|
||||||
Fantasy = 9
|
|
||||||
}
|
|
||||||
|
|
||||||
public enum BookContentTypes : int
|
|
||||||
{
|
|
||||||
Text = 0,
|
|
||||||
Image = 1,
|
|
||||||
Video = 2,
|
|
||||||
Audio = 3,
|
|
||||||
Interactive = 4,
|
|
||||||
Markdown = 5,
|
|
||||||
Html = 6,
|
|
||||||
Json = 7,
|
|
||||||
Yaml = 8
|
|
||||||
}
|
|
||||||
|
|
||||||
public enum BookPageTypes : int
|
|
||||||
{
|
|
||||||
Cover = 0,
|
|
||||||
Preface = 1,
|
|
||||||
Introduction = 2,
|
|
||||||
Content = 3,
|
|
||||||
Closing = 4,
|
|
||||||
Referencer = 5,
|
|
||||||
Credits = 6,
|
|
||||||
BackCover = 7
|
|
||||||
}
|
|
||||||
|
|
||||||
public enum ProductTypes : int
|
|
||||||
{
|
|
||||||
Book = 1,
|
|
||||||
Journal = 2,
|
|
||||||
Magazine = 3,
|
|
||||||
EBook = 4,
|
|
||||||
Audiobook = 5,
|
|
||||||
Accessory = 6
|
|
||||||
}
|
|
||||||
@@ -4,12 +4,73 @@ using LiteCharms.Features.MidrandBooks.Categories.Models;
|
|||||||
using LiteCharms.Features.MidrandBooks.Customers.Models;
|
using LiteCharms.Features.MidrandBooks.Customers.Models;
|
||||||
using LiteCharms.Features.MidrandBooks.Orders.Models;
|
using LiteCharms.Features.MidrandBooks.Orders.Models;
|
||||||
using LiteCharms.Features.MidrandBooks.Pages.Models;
|
using LiteCharms.Features.MidrandBooks.Pages.Models;
|
||||||
|
using LiteCharms.Features.MidrandBooks.Payments.Models;
|
||||||
using LiteCharms.Features.MidrandBooks.Products.Models;
|
using LiteCharms.Features.MidrandBooks.Products.Models;
|
||||||
|
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||||
|
|
||||||
namespace LiteCharms.Features.MidrandBooks.Extensions;
|
namespace LiteCharms.Features.MidrandBooks.Extensions;
|
||||||
|
|
||||||
public static class Mappers
|
public static class Mappers
|
||||||
{
|
{
|
||||||
|
public static Refund ToModel(this Payments.Entities.Refund entity) => new()
|
||||||
|
{
|
||||||
|
CreatedAt = entity.CreatedAt,
|
||||||
|
Amount = entity.Amount,
|
||||||
|
Id = entity.Id,
|
||||||
|
OrderId = entity.OrderId,
|
||||||
|
Reason = entity.Reason,
|
||||||
|
Status = entity.Status,
|
||||||
|
Type = entity.Type,
|
||||||
|
UpdatedAt = entity.UpdatedAt,
|
||||||
|
};
|
||||||
|
|
||||||
|
public static PaymentLedger ToModel(this Payments.Entities.PaymentLedger entity) => new()
|
||||||
|
{
|
||||||
|
Id = entity.Id,
|
||||||
|
CreatedAt = entity.CreatedAt,
|
||||||
|
CustomerId = entity.CustomerId,
|
||||||
|
OrderId = entity.OrderId,
|
||||||
|
PaymentGatewayId = entity.PaymentGatewayId,
|
||||||
|
PaymentGatewayReference = entity.PaymentGatewayReference,
|
||||||
|
PaymentId = entity.PaymentId,
|
||||||
|
Status = entity.Status,
|
||||||
|
};
|
||||||
|
|
||||||
|
public static PaymentGateway ToModel(this Payments.Entities.PaymentGateway entity) => new()
|
||||||
|
{
|
||||||
|
Id = entity.Id,
|
||||||
|
CreatedAt = entity.CreatedAt,
|
||||||
|
UpdatedAt = entity.UpdatedAt,
|
||||||
|
Enabled = entity.Enabled,
|
||||||
|
IsSandbox = entity.IsSandbox,
|
||||||
|
MerchantId = entity.MerchantId,
|
||||||
|
MerchantKey = entity.MerchantKey,
|
||||||
|
Name = entity.Name,
|
||||||
|
Website = entity.Website,
|
||||||
|
};
|
||||||
|
|
||||||
|
public static Payment ToModel(this Payments.Entities.Payment entity) => new()
|
||||||
|
{
|
||||||
|
Id = entity.Id,
|
||||||
|
Amount = entity.Amount,
|
||||||
|
CreatedAt = entity.CreatedAt,
|
||||||
|
OrderId = entity.OrderId,
|
||||||
|
Reference = entity.Reference,
|
||||||
|
Status = entity.Status,
|
||||||
|
UpdatedAt = entity.UpdatedAt,
|
||||||
|
};
|
||||||
|
|
||||||
|
public static ProductInventory ToModel(this Products.Entities.ProductInventory entity) => new()
|
||||||
|
{
|
||||||
|
Id = entity.Id,
|
||||||
|
CreatedAt = entity.CreatedAt,
|
||||||
|
ProductId = entity.ProductId,
|
||||||
|
ProductPriceId = entity.ProductPriceId,
|
||||||
|
Status = entity.Status,
|
||||||
|
TotalAllocated = entity.TotalAllocated,
|
||||||
|
TotalReserved = entity.TotalReserved,
|
||||||
|
};
|
||||||
|
|
||||||
public static Category ToModel(this Categories.Entities.Category entity) => new()
|
public static Category ToModel(this Categories.Entities.Category entity) => new()
|
||||||
{
|
{
|
||||||
Id = entity.Id,
|
Id = entity.Id,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using LiteCharms.Features.MidrandBooks.Abstractions;
|
using LiteCharms.Features.Abstractions;
|
||||||
|
|
||||||
namespace LiteCharms.Features.MidrandBooks.Extensions;
|
namespace LiteCharms.Features.MidrandBooks.Extensions;
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using LiteCharms.Features.MidrandBooks.Abstractions;
|
using LiteCharms.Features.Abstractions;
|
||||||
using LiteCharms.Features.MidrandBooks.Extensions;
|
using LiteCharms.Features.MidrandBooks.Extensions;
|
||||||
using LiteCharms.Features.MidrandBooks.Orders.Models;
|
using LiteCharms.Features.MidrandBooks.Orders.Models;
|
||||||
using LiteCharms.Features.MidrandBooks.Postgres;
|
using LiteCharms.Features.MidrandBooks.Postgres;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using LiteCharms.Features.MidrandBooks.Abstractions;
|
using LiteCharms.Features.Abstractions;
|
||||||
using LiteCharms.Features.MidrandBooks.Extensions;
|
using LiteCharms.Features.MidrandBooks.Extensions;
|
||||||
using LiteCharms.Features.MidrandBooks.Pages.Models;
|
using LiteCharms.Features.MidrandBooks.Pages.Models;
|
||||||
using LiteCharms.Features.MidrandBooks.Postgres;
|
using LiteCharms.Features.MidrandBooks.Postgres;
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
using LiteCharms.Features.MidrandBooks.Orders.Entities;
|
||||||
|
|
||||||
|
namespace LiteCharms.Features.MidrandBooks.Payments.Entities;
|
||||||
|
|
||||||
|
[EntityTypeConfiguration<PaymentConfiguration, Payment>]
|
||||||
|
public class Payment : Models.Payment
|
||||||
|
{
|
||||||
|
public virtual Order? Order { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
namespace LiteCharms.Features.MidrandBooks.Payments.Entities;
|
||||||
|
|
||||||
|
public sealed class PaymentConfiguration : IEntityTypeConfiguration<Payment>
|
||||||
|
{
|
||||||
|
public void Configure(EntityTypeBuilder<Payment> builder)
|
||||||
|
{
|
||||||
|
builder.ToTable("Payments");
|
||||||
|
|
||||||
|
builder.HasKey(f => f.Id);
|
||||||
|
builder.Property(f => f.CreatedAt).IsRequired().ValueGeneratedOnAdd().HasDefaultValueSql("now()");
|
||||||
|
builder.Property(f => f.UpdatedAt);
|
||||||
|
builder.Property(f => f.Status).IsRequired();
|
||||||
|
builder.Property(f => f.Reference).IsRequired();
|
||||||
|
builder.Property(f => f.OrderId).IsRequired();
|
||||||
|
builder.Property(f => f.Amount).IsRequired().HasPrecision(18, 2);
|
||||||
|
|
||||||
|
builder.HasOne(f => f.Order)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey(f => f.OrderId)
|
||||||
|
.OnDelete(DeleteBehavior.Restrict);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
namespace LiteCharms.Features.MidrandBooks.Payments.Entities;
|
||||||
|
|
||||||
|
[EntityTypeConfiguration<PaymentGatewayConfiguration, PaymentGateway>]
|
||||||
|
public class PaymentGateway : Models.PaymentGateway;
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
namespace LiteCharms.Features.MidrandBooks.Payments.Entities;
|
||||||
|
|
||||||
|
public sealed class PaymentGatewayConfiguration : IEntityTypeConfiguration<PaymentGateway>
|
||||||
|
{
|
||||||
|
public void Configure(EntityTypeBuilder<PaymentGateway> builder)
|
||||||
|
{
|
||||||
|
builder.ToTable("Gateways");
|
||||||
|
|
||||||
|
builder.HasKey(f => f.Id);
|
||||||
|
builder.Property(f => f.CreatedAt).IsRequired().ValueGeneratedOnAdd().HasDefaultValueSql("now()");
|
||||||
|
builder.Property(f => f.UpdatedAt);
|
||||||
|
builder.Property(f => f.Website).IsRequired(false);
|
||||||
|
builder.Property(f => f.IsSandbox);
|
||||||
|
builder.Property(f => f.MerchantKey).IsRequired();
|
||||||
|
builder.Property(f => f.MerchantId).IsRequired();
|
||||||
|
builder.Property(f => f.Enabled);
|
||||||
|
builder.Property(f => f.Name).IsRequired();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
using LiteCharms.Features.MidrandBooks.Customers.Entities;
|
||||||
|
using LiteCharms.Features.MidrandBooks.Orders.Entities;
|
||||||
|
|
||||||
|
namespace LiteCharms.Features.MidrandBooks.Payments.Entities;
|
||||||
|
|
||||||
|
[EntityTypeConfiguration<PaymentLedgerConfiguration, PaymentLedger>]
|
||||||
|
public class PaymentLedger : Models.PaymentLedger
|
||||||
|
{
|
||||||
|
public virtual Payment? Payment { get; set; }
|
||||||
|
|
||||||
|
public virtual Order? Order { get; set; }
|
||||||
|
|
||||||
|
public virtual Customer? Customer { get; set; }
|
||||||
|
|
||||||
|
public virtual PaymentGateway? Gateway { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
namespace LiteCharms.Features.MidrandBooks.Payments.Entities;
|
||||||
|
|
||||||
|
public sealed class PaymentLedgerConfiguration : IEntityTypeConfiguration<PaymentLedger>
|
||||||
|
{
|
||||||
|
public void Configure(EntityTypeBuilder<PaymentLedger> builder)
|
||||||
|
{
|
||||||
|
builder.ToTable("Ledger");
|
||||||
|
|
||||||
|
builder.HasKey(f => f.Id);
|
||||||
|
builder.Property(f => f.CreatedAt).IsRequired().ValueGeneratedOnAdd().HasDefaultValueSql("now()");
|
||||||
|
builder.Property(f => f.Status).IsRequired();
|
||||||
|
builder.Property(f => f.PaymentGatewayReference).IsRequired(false);
|
||||||
|
builder.Property(f => f.PaymentGatewayId).IsRequired(false);
|
||||||
|
builder.Property(f => f.OrderId).IsRequired();
|
||||||
|
builder.Property(f => f.CustomerId).IsRequired();
|
||||||
|
builder.Property(f => f.PaymentId).IsRequired();
|
||||||
|
|
||||||
|
builder.HasOne(f => f.Payment)
|
||||||
|
.WithMany()
|
||||||
|
.IsRequired()
|
||||||
|
.HasForeignKey(f => f.PaymentId)
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
|
||||||
|
builder.HasOne(f => f.Order)
|
||||||
|
.WithMany()
|
||||||
|
.IsRequired()
|
||||||
|
.HasForeignKey(f => f.OrderId)
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
|
||||||
|
builder.HasOne(f => f.Customer)
|
||||||
|
.WithMany()
|
||||||
|
.IsRequired()
|
||||||
|
.HasForeignKey(f => f.CustomerId);
|
||||||
|
|
||||||
|
builder.HasOne(f => f.Gateway)
|
||||||
|
.WithMany()
|
||||||
|
.IsRequired(false)
|
||||||
|
.HasForeignKey(f => f.PaymentGatewayId)
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
namespace LiteCharms.Features.MidrandBooks.Payments.Models;
|
||||||
|
|
||||||
|
public class Payment
|
||||||
|
{
|
||||||
|
public long Id { get; set; }
|
||||||
|
|
||||||
|
public DateTime CreatedAt { get; set; }
|
||||||
|
|
||||||
|
public DateTime? UpdatedAt { get; set; }
|
||||||
|
|
||||||
|
public decimal Amount { get; set; }
|
||||||
|
|
||||||
|
public long OrderId { get; set; }
|
||||||
|
|
||||||
|
public string? Reference { get; set; }
|
||||||
|
|
||||||
|
public PaymentStatuses Status { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
namespace LiteCharms.Features.MidrandBooks.Payments.Models;
|
||||||
|
|
||||||
|
public class PaymentGateway
|
||||||
|
{
|
||||||
|
public long Id { get; set; }
|
||||||
|
|
||||||
|
public DateTime CreatedAt { get; set; }
|
||||||
|
|
||||||
|
public DateTime? UpdatedAt { get; set; }
|
||||||
|
|
||||||
|
public string? Name { get; set; }
|
||||||
|
|
||||||
|
public string? Website { get; set; }
|
||||||
|
|
||||||
|
public string? MerchantId { get; set; }
|
||||||
|
|
||||||
|
public string? MerchantKey { get; set; }
|
||||||
|
|
||||||
|
public bool IsSandbox { get; set; }
|
||||||
|
|
||||||
|
public bool Enabled { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
namespace LiteCharms.Features.MidrandBooks.Payments.Models;
|
||||||
|
|
||||||
|
public class PaymentLedger
|
||||||
|
{
|
||||||
|
public long Id { get; set; }
|
||||||
|
|
||||||
|
public DateTime CreatedAt { get; set; }
|
||||||
|
|
||||||
|
public LedgerStatuses Status { get; set; }
|
||||||
|
|
||||||
|
public long OrderId { get; set; }
|
||||||
|
|
||||||
|
public long PaymentId { get; set; }
|
||||||
|
|
||||||
|
public long CustomerId { get; set; }
|
||||||
|
|
||||||
|
public string? PaymentGatewayReference { get; set; }
|
||||||
|
|
||||||
|
public long? PaymentGatewayId { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
namespace LiteCharms.Features.MidrandBooks.Payments.Models;
|
||||||
|
|
||||||
|
public sealed record UpdateRefund
|
||||||
|
{
|
||||||
|
public long OrderId { get; set; }
|
||||||
|
|
||||||
|
public RefundStatus Status { get; set; }
|
||||||
|
|
||||||
|
public string? Reason { get; set; }
|
||||||
|
|
||||||
|
public decimal Amount { get; set; }
|
||||||
|
};
|
||||||
|
|
||||||
|
public sealed record CreateRefund
|
||||||
|
{
|
||||||
|
public long OrderId { get; set; }
|
||||||
|
|
||||||
|
public RefundTypes Type { get; set; }
|
||||||
|
|
||||||
|
public RefundStatus Status { get; set; }
|
||||||
|
|
||||||
|
public string? Reason { get; set; }
|
||||||
|
|
||||||
|
public decimal Amount { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed record CreateLedgerEntry
|
||||||
|
{
|
||||||
|
public required LedgerStatuses Status { get; set; }
|
||||||
|
|
||||||
|
public required long OrderId { get; set; }
|
||||||
|
|
||||||
|
public required long PaymentId { get; set; }
|
||||||
|
|
||||||
|
public required long CustomerId { get; set; }
|
||||||
|
|
||||||
|
public string? PaymentGatewayReference { get; set; }
|
||||||
|
|
||||||
|
public long? PaymentGatewayId { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed record CreatePaymentGateway
|
||||||
|
{
|
||||||
|
public required string? Name { get; set; }
|
||||||
|
|
||||||
|
public string? Website { get; set; }
|
||||||
|
|
||||||
|
public required string? MerchantId { get; set; }
|
||||||
|
|
||||||
|
public required string? MerchantKey { get; set; }
|
||||||
|
|
||||||
|
public bool IsSandbox { get; set; }
|
||||||
|
}
|
||||||
@@ -1,7 +1,264 @@
|
|||||||
using LiteCharms.Features.MidrandBooks.Abstractions;
|
using LiteCharms.Features.Abstractions;
|
||||||
|
using LiteCharms.Features.MidrandBooks.Extensions;
|
||||||
|
using LiteCharms.Features.MidrandBooks.Payments.Models;
|
||||||
|
using LiteCharms.Features.MidrandBooks.Postgres;
|
||||||
|
|
||||||
namespace LiteCharms.Features.MidrandBooks.Payments;
|
namespace LiteCharms.Features.MidrandBooks.Payments;
|
||||||
|
|
||||||
public sealed class PaymentService : IService
|
public sealed class PaymentService(IDbContextFactory<MidrandBooksDbContext> contextFactory) : IService
|
||||||
{
|
{
|
||||||
|
public async ValueTask<Result<Refund>> GetRefundAsync(long refundId, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
|
||||||
|
|
||||||
|
var refund = await context.Refunds.AsNoTracking()
|
||||||
|
.FirstOrDefaultAsync(r => r.Id == refundId, cancellationToken);
|
||||||
|
|
||||||
|
return refund is not null
|
||||||
|
? Result.Ok(refund.ToModel())
|
||||||
|
: Result.Fail<Refund>("Could not find refund");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return Result.Fail<Refund>(new Error(ex.Message).CausedBy(ex));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ValueTask<Result> UpdateRefundAsync(long refundId, UpdateRefund request, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
|
||||||
|
|
||||||
|
if (!await context.Orders.AnyAsync(o => o.Id == request.OrderId, cancellationToken))
|
||||||
|
return Result.Fail("Order not found");
|
||||||
|
|
||||||
|
var updatedRows = await context.Refunds
|
||||||
|
.Where(r => r.Id == refundId && r.OrderId == request.OrderId)
|
||||||
|
.ExecuteUpdateAsync(setters => setters
|
||||||
|
.SetProperty(r => r.Status, request.Status)
|
||||||
|
.SetProperty(r => r.Reason, request.Reason)
|
||||||
|
.SetProperty(r => r.UpdatedAt, DateTime.UtcNow)
|
||||||
|
.SetProperty(r => r.Amount, request.Amount), cancellationToken);
|
||||||
|
|
||||||
|
return updatedRows > 0
|
||||||
|
? Result.Ok()
|
||||||
|
: Result.Fail("Failed to update refund");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return Result.Fail(new Error(ex.Message).CausedBy(ex));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ValueTask<Result<long>> CreateRefundAsync(CreateRefund request, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
|
||||||
|
|
||||||
|
var order = await context.Orders.AsNoTracking()
|
||||||
|
.FirstOrDefaultAsync(o => o.Id == request.OrderId
|
||||||
|
&& o.Status == OrderStatus.Completed, cancellationToken);
|
||||||
|
|
||||||
|
if (order is null) return Result.Fail("Order not found");
|
||||||
|
|
||||||
|
if (request.Amount > order.Total)
|
||||||
|
return Result.Fail<long>("Refund amount cannot be greater than order total");
|
||||||
|
|
||||||
|
var totalRefundsPaid = await context.Refunds
|
||||||
|
.Where(r => r.OrderId == request.OrderId)
|
||||||
|
.SumAsync(r => r.Amount, cancellationToken);
|
||||||
|
|
||||||
|
if (request.Amount > (order.Total - totalRefundsPaid))
|
||||||
|
return Result.Fail<long>("Refund amount exceeds amount available for refund");
|
||||||
|
|
||||||
|
var refund = context.Refunds.Add(new Entities.Refund
|
||||||
|
{
|
||||||
|
Amount = request.Amount,
|
||||||
|
CreatedAt = DateTime.UtcNow,
|
||||||
|
OrderId = request.OrderId,
|
||||||
|
Reason = request.Reason,
|
||||||
|
Status = request.Status,
|
||||||
|
Type = request.Type,
|
||||||
|
});
|
||||||
|
|
||||||
|
return await context.SaveChangesAsync(cancellationToken) > 0
|
||||||
|
? Result.Ok(refund.Entity.Id)
|
||||||
|
: Result.Fail<long>("Failed to create refund");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return Result.Fail(new Error(ex.Message).CausedBy(ex));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ValueTask<Result> WriteLedgerEntryAsync(CreateLedgerEntry request, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
|
||||||
|
|
||||||
|
if (!await context.Orders.AnyAsync(o => o.Id == request.OrderId, cancellationToken))
|
||||||
|
return Result.Fail("Order not found");
|
||||||
|
|
||||||
|
if (!await context.Customers.AnyAsync(o => o.Id == request.CustomerId, cancellationToken))
|
||||||
|
return Result.Fail("Customer not found");
|
||||||
|
|
||||||
|
if (!await context.Orders.AnyAsync(oc => oc.Id == request.OrderId && oc.CustomerId == request.CustomerId, cancellationToken))
|
||||||
|
return Result.Fail("Customer does not match the order");
|
||||||
|
|
||||||
|
if (!await context.Payments.AnyAsync(o => o.Id == request.PaymentId && o.OrderId == request.OrderId, cancellationToken))
|
||||||
|
return Result.Fail("Payment not found");
|
||||||
|
|
||||||
|
if (request.PaymentGatewayId is not null)
|
||||||
|
if (!await context.Gateways.AnyAsync(o => o.Id == request.PaymentGatewayId, cancellationToken))
|
||||||
|
return Result.Fail("Gateway not found");
|
||||||
|
|
||||||
|
context.Ledger.Add(new Entities.PaymentLedger
|
||||||
|
{
|
||||||
|
CreatedAt = DateTime.UtcNow,
|
||||||
|
CustomerId = request.CustomerId,
|
||||||
|
OrderId = request.OrderId,
|
||||||
|
PaymentGatewayId = request.PaymentGatewayId,
|
||||||
|
PaymentGatewayReference = request.PaymentGatewayReference,
|
||||||
|
PaymentId = request.PaymentId,
|
||||||
|
Status = request.Status,
|
||||||
|
});
|
||||||
|
|
||||||
|
return await context.SaveChangesAsync(cancellationToken) > 0
|
||||||
|
? Result.Ok()
|
||||||
|
: Result.Fail("Failed to create ledger entry");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return Result.Fail(new Error(ex.Message).CausedBy(ex));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ValueTask<Result<PaymentGateway>> GetPaymentGatewayAsync(long paymentGatewayId, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
|
||||||
|
|
||||||
|
var gateway = await context.Gateways.AsNoTracking().FirstOrDefaultAsync(g => g.Id == paymentGatewayId, cancellationToken);
|
||||||
|
|
||||||
|
return gateway is not null
|
||||||
|
? Result.Ok(gateway.ToModel())
|
||||||
|
: Result.Fail<PaymentGateway>("Could not find gateway");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return Result.Fail<PaymentGateway>(new Error(ex.Message).CausedBy(ex));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ValueTask<Result<long>> CreatePaymentGatewayAsync(CreatePaymentGateway request, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
|
||||||
|
|
||||||
|
if (await context.Gateways.AnyAsync(g => g.MerchantId == request.MerchantId && g.MerchantKey == request.MerchantKey, cancellationToken))
|
||||||
|
return Result.Fail<long>("A gateway with the same credentials already exists");
|
||||||
|
|
||||||
|
var gateway = context.Gateways.Add(new Entities.PaymentGateway
|
||||||
|
{
|
||||||
|
CreatedAt = DateTime.UtcNow,
|
||||||
|
Enabled = true,
|
||||||
|
IsSandbox = request.IsSandbox,
|
||||||
|
MerchantId = request.MerchantId,
|
||||||
|
MerchantKey = request.MerchantKey,
|
||||||
|
Name = request.Name,
|
||||||
|
Website = request.Website,
|
||||||
|
});
|
||||||
|
|
||||||
|
return await context.SaveChangesAsync(cancellationToken) > 0
|
||||||
|
? Result.Ok(gateway.Entity.Id)
|
||||||
|
: Result.Fail<long>("Failed to create payment gateway");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return Result.Fail<long>(new Error(ex.Message).CausedBy(ex));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ValueTask<Result> CompletePaymentAsync(long paymentId, PaymentStatuses status, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
|
||||||
|
|
||||||
|
if (status == PaymentStatuses.NotPaid)
|
||||||
|
return Result.Fail("Cannot finalise a payment using NotPaid status");
|
||||||
|
|
||||||
|
var updatedRecords = await context.Payments
|
||||||
|
.Where(p => p.Id == paymentId && p.Status != PaymentStatuses.Paid && p.Status != status)
|
||||||
|
.ExecuteUpdateAsync(setters => setters
|
||||||
|
.SetProperty(u => u.Status, status)
|
||||||
|
.SetProperty(u => u.UpdatedAt, DateTime.UtcNow), cancellationToken);
|
||||||
|
|
||||||
|
return updatedRecords > 0
|
||||||
|
? Result.Ok()
|
||||||
|
: Result.Fail("Failed to update payment");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return Result.Fail(new Error(ex.Message).CausedBy(ex));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ValueTask<Result> UpdatePaymentAsync(long paymentId, decimal amount, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
|
||||||
|
|
||||||
|
var updatedRecords = await context.Payments
|
||||||
|
.Where(p => p.Id == paymentId && p.Status == PaymentStatuses.NotPaid)
|
||||||
|
.ExecuteUpdateAsync(setters => setters
|
||||||
|
.SetProperty(u => u.Amount, amount)
|
||||||
|
.SetProperty(u => u.Status, PaymentStatuses.NotPaid)
|
||||||
|
.SetProperty(u => u.UpdatedAt, DateTime.UtcNow), cancellationToken);
|
||||||
|
|
||||||
|
return updatedRecords > 0
|
||||||
|
? Result.Ok()
|
||||||
|
: Result.Fail("Failed to update payment");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return Result.Fail(new Error(ex.Message).CausedBy(ex));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ValueTask<Result<long>> CreatePaymentAsync(decimal amount, long orderId, string reference, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
|
||||||
|
|
||||||
|
if (await context.Payments.AnyAsync(p => p.OrderId == orderId && p.Amount == amount && p.Status != PaymentStatuses.Paid, cancellationToken))
|
||||||
|
return Result.Fail<long>("An order with the same amount already exists in the system");
|
||||||
|
|
||||||
|
var payment = context.Payments.Add(new Entities.Payment
|
||||||
|
{
|
||||||
|
CreatedAt = DateTime.UtcNow,
|
||||||
|
Amount = amount,
|
||||||
|
OrderId = orderId,
|
||||||
|
Reference = reference,
|
||||||
|
Status = PaymentStatuses.NotPaid,
|
||||||
|
});
|
||||||
|
|
||||||
|
return await context.SaveChangesAsync(cancellationToken) > 0
|
||||||
|
? Result.Ok(payment.Entity.Id)
|
||||||
|
: Result.Fail<long>("Failed to make payment");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return Result.Fail<long>(new Error(ex.Message).CausedBy(ex));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,4 +40,12 @@ public sealed class MidrandBooksDbContext(DbContextOptions<MidrandBooksDbContext
|
|||||||
public DbSet<Category> Categories => Set<Category>();
|
public DbSet<Category> Categories => Set<Category>();
|
||||||
|
|
||||||
public DbSet<ProductCategory> ProductCategories => Set<ProductCategory>();
|
public DbSet<ProductCategory> ProductCategories => Set<ProductCategory>();
|
||||||
|
|
||||||
|
public DbSet<ProductInventory> Inventories => Set<ProductInventory>();
|
||||||
|
|
||||||
|
public DbSet<Payment> Payments => Set<Payment>();
|
||||||
|
|
||||||
|
public DbSet<PaymentGateway> Gateways => Set<PaymentGateway>();
|
||||||
|
|
||||||
|
public DbSet<PaymentLedger> Ledger => Set<PaymentLedger>();
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+1235
File diff suppressed because it is too large
Load Diff
+185
@@ -0,0 +1,185 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddedPaymentObjects : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "Gateways",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<long>(type: "bigint", nullable: false)
|
||||||
|
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||||
|
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"),
|
||||||
|
UpdatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
|
||||||
|
Name = table.Column<string>(type: "text", nullable: false),
|
||||||
|
Website = table.Column<string>(type: "text", nullable: true),
|
||||||
|
MerchantId = table.Column<string>(type: "text", nullable: false),
|
||||||
|
MerchantKey = table.Column<string>(type: "text", nullable: false),
|
||||||
|
Passphrase = table.Column<string>(type: "text", nullable: false),
|
||||||
|
IsSandbox = table.Column<bool>(type: "boolean", nullable: false),
|
||||||
|
Enabled = table.Column<bool>(type: "boolean", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_Gateways", x => x.Id);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "Inventories",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<long>(type: "bigint", nullable: false)
|
||||||
|
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||||
|
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"),
|
||||||
|
Status = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
ProductId = table.Column<long>(type: "bigint", nullable: false),
|
||||||
|
ProductPriceId = table.Column<long>(type: "bigint", nullable: false),
|
||||||
|
TotalAllocated = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
TotalReserved = table.Column<int>(type: "integer", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_Inventories", x => x.Id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_Inventories_Prices_ProductPriceId",
|
||||||
|
column: x => x.ProductPriceId,
|
||||||
|
principalTable: "Prices",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_Inventories_Products_ProductId",
|
||||||
|
column: x => x.ProductId,
|
||||||
|
principalTable: "Products",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "Payments",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<long>(type: "bigint", nullable: false)
|
||||||
|
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||||
|
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"),
|
||||||
|
UpdatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
|
||||||
|
Amount = table.Column<decimal>(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false),
|
||||||
|
OrderId = table.Column<long>(type: "bigint", nullable: false),
|
||||||
|
Reference = table.Column<string>(type: "text", nullable: false),
|
||||||
|
Status = table.Column<int>(type: "integer", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_Payments", x => x.Id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_Payments_Orders_OrderId",
|
||||||
|
column: x => x.OrderId,
|
||||||
|
principalTable: "Orders",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Restrict);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "Ledger",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<long>(type: "bigint", nullable: false)
|
||||||
|
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||||
|
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false, defaultValueSql: "now()"),
|
||||||
|
Status = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
OrderId = table.Column<long>(type: "bigint", nullable: false),
|
||||||
|
PaymentId = table.Column<long>(type: "bigint", nullable: false),
|
||||||
|
CustomerId = table.Column<long>(type: "bigint", nullable: false),
|
||||||
|
PaymentGatewayReference = table.Column<string>(type: "text", nullable: true),
|
||||||
|
PaymentGatewayId = table.Column<long>(type: "bigint", nullable: true)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_Ledger", x => x.Id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_Ledger_Customers_CustomerId",
|
||||||
|
column: x => x.CustomerId,
|
||||||
|
principalTable: "Customers",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_Ledger_Gateways_PaymentGatewayId",
|
||||||
|
column: x => x.PaymentGatewayId,
|
||||||
|
principalTable: "Gateways",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_Ledger_Orders_OrderId",
|
||||||
|
column: x => x.OrderId,
|
||||||
|
principalTable: "Orders",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_Ledger_Payments_PaymentId",
|
||||||
|
column: x => x.PaymentId,
|
||||||
|
principalTable: "Payments",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_Inventories_ProductId",
|
||||||
|
table: "Inventories",
|
||||||
|
column: "ProductId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_Inventories_ProductPriceId",
|
||||||
|
table: "Inventories",
|
||||||
|
column: "ProductPriceId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_Ledger_CustomerId",
|
||||||
|
table: "Ledger",
|
||||||
|
column: "CustomerId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_Ledger_OrderId",
|
||||||
|
table: "Ledger",
|
||||||
|
column: "OrderId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_Ledger_PaymentGatewayId",
|
||||||
|
table: "Ledger",
|
||||||
|
column: "PaymentGatewayId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_Ledger_PaymentId",
|
||||||
|
table: "Ledger",
|
||||||
|
column: "PaymentId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_Payments_OrderId",
|
||||||
|
table: "Payments",
|
||||||
|
column: "OrderId");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "Inventories");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "Ledger");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "Gateways");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "Payments");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+1231
File diff suppressed because it is too large
Load Diff
+29
@@ -0,0 +1,29 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class RemovedPassphraseFromPaymentGateway : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "Passphrase",
|
||||||
|
table: "Gateways");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "Passphrase",
|
||||||
|
table: "Gateways",
|
||||||
|
type: "text",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: "");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+224
@@ -536,6 +536,129 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations
|
|||||||
b.ToTable("BookPages", (string)null);
|
b.ToTable("BookPages", (string)null);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Payments.Entities.Payment", b =>
|
||||||
|
{
|
||||||
|
b.Property<long>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
|
||||||
|
|
||||||
|
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<long>("OrderId")
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
b.Property<string>("Reference")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<int>("Status")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("UpdatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("OrderId");
|
||||||
|
|
||||||
|
b.ToTable("Payments", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Payments.Entities.PaymentGateway", b =>
|
||||||
|
{
|
||||||
|
b.Property<long>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasDefaultValueSql("now()");
|
||||||
|
|
||||||
|
b.Property<bool>("Enabled")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<bool>("IsSandbox")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<string>("MerchantId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("MerchantKey")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("UpdatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<string>("Website")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.ToTable("Gateways", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Payments.Entities.PaymentLedger", b =>
|
||||||
|
{
|
||||||
|
b.Property<long>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasDefaultValueSql("now()");
|
||||||
|
|
||||||
|
b.Property<long>("CustomerId")
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
b.Property<long>("OrderId")
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
b.Property<long?>("PaymentGatewayId")
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
b.Property<string>("PaymentGatewayReference")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<long>("PaymentId")
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
b.Property<int>("Status")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("CustomerId");
|
||||||
|
|
||||||
|
b.HasIndex("OrderId");
|
||||||
|
|
||||||
|
b.HasIndex("PaymentGatewayId");
|
||||||
|
|
||||||
|
b.HasIndex("PaymentId");
|
||||||
|
|
||||||
|
b.ToTable("Ledger", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Payments.Entities.Refund", b =>
|
modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Payments.Entities.Refund", b =>
|
||||||
{
|
{
|
||||||
b.Property<long>("Id")
|
b.Property<long>("Id")
|
||||||
@@ -653,6 +776,43 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations
|
|||||||
b.ToTable("ProductCategories", (string)null);
|
b.ToTable("ProductCategories", (string)null);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.ProductInventory", b =>
|
||||||
|
{
|
||||||
|
b.Property<long>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasDefaultValueSql("now()");
|
||||||
|
|
||||||
|
b.Property<long>("ProductId")
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
b.Property<long>("ProductPriceId")
|
||||||
|
.HasColumnType("bigint");
|
||||||
|
|
||||||
|
b.Property<int>("Status")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<int>("TotalAllocated")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<int>("TotalReserved")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("ProductId");
|
||||||
|
|
||||||
|
b.HasIndex("ProductPriceId");
|
||||||
|
|
||||||
|
b.ToTable("Inventories", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.ProductPrice", b =>
|
modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.ProductPrice", b =>
|
||||||
{
|
{
|
||||||
b.Property<long>("Id")
|
b.Property<long>("Id")
|
||||||
@@ -891,6 +1051,51 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations
|
|||||||
b.Navigation("References");
|
b.Navigation("References");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Payments.Entities.Payment", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("LiteCharms.Features.MidrandBooks.Orders.Entities.Order", "Order")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("OrderId")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Order");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Payments.Entities.PaymentLedger", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("LiteCharms.Features.MidrandBooks.Customers.Entities.Customer", "Customer")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("CustomerId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("LiteCharms.Features.MidrandBooks.Orders.Entities.Order", "Order")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("OrderId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("LiteCharms.Features.MidrandBooks.Payments.Entities.PaymentGateway", "Gateway")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("PaymentGatewayId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
|
||||||
|
b.HasOne("LiteCharms.Features.MidrandBooks.Payments.Entities.Payment", "Payment")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("PaymentId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Customer");
|
||||||
|
|
||||||
|
b.Navigation("Gateway");
|
||||||
|
|
||||||
|
b.Navigation("Order");
|
||||||
|
|
||||||
|
b.Navigation("Payment");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Payments.Entities.Refund", b =>
|
modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Payments.Entities.Refund", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("LiteCharms.Features.MidrandBooks.Orders.Entities.Order", "Order")
|
b.HasOne("LiteCharms.Features.MidrandBooks.Orders.Entities.Order", "Order")
|
||||||
@@ -950,6 +1155,25 @@ namespace LiteCharms.Features.MidrandBooks.Postgres.Migrations
|
|||||||
b.Navigation("Product");
|
b.Navigation("Product");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.ProductInventory", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("LiteCharms.Features.MidrandBooks.Products.Entities.Product", "Product")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("ProductId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("LiteCharms.Features.MidrandBooks.Products.Entities.ProductPrice", "Price")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("ProductPriceId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Price");
|
||||||
|
|
||||||
|
b.Navigation("Product");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.ProductPrice", b =>
|
modelBuilder.Entity("LiteCharms.Features.MidrandBooks.Products.Entities.ProductPrice", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("LiteCharms.Features.MidrandBooks.Products.Entities.Product", "Product")
|
b.HasOne("LiteCharms.Features.MidrandBooks.Products.Entities.Product", "Product")
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
namespace LiteCharms.Features.MidrandBooks.Products.Entities;
|
||||||
|
|
||||||
|
[EntityTypeConfiguration<ProductInventoryConfiguration, ProductInventory>]
|
||||||
|
public class ProductInventory : Models.ProductInventory
|
||||||
|
{
|
||||||
|
public virtual Product? Product { get; set; }
|
||||||
|
|
||||||
|
public virtual ProductPrice? Price { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
namespace LiteCharms.Features.MidrandBooks.Products.Entities;
|
||||||
|
|
||||||
|
public sealed class ProductInventoryConfiguration : IEntityTypeConfiguration<ProductInventory>
|
||||||
|
{
|
||||||
|
public void Configure(EntityTypeBuilder<ProductInventory> builder)
|
||||||
|
{
|
||||||
|
builder.ToTable("Inventories");
|
||||||
|
|
||||||
|
builder.HasKey(f => f.Id);
|
||||||
|
builder.Property(f => f.CreatedAt).IsRequired().ValueGeneratedOnAdd().HasDefaultValueSql("now()");
|
||||||
|
builder.Property(f => f.Status).IsRequired();
|
||||||
|
builder.Property(f => f.TotalAllocated).IsRequired();
|
||||||
|
builder.Property(f => f.TotalReserved).IsRequired();
|
||||||
|
builder.Property(f => f.ProductId).IsRequired();
|
||||||
|
builder.Property(f => f.ProductPriceId).IsRequired();
|
||||||
|
|
||||||
|
builder.HasOne(f => f.Product)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey(f => f.ProductId)
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
|
||||||
|
builder.HasOne(f => f.Price)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey(f => f.ProductPriceId)
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
namespace LiteCharms.Features.MidrandBooks.Products.Models;
|
||||||
|
|
||||||
|
public class ProductInventory
|
||||||
|
{
|
||||||
|
public long Id { get; set; }
|
||||||
|
|
||||||
|
public DateTime CreatedAt { get; set; }
|
||||||
|
|
||||||
|
public InventoryStatuses Status { get; set; }
|
||||||
|
|
||||||
|
public long ProductId { get; set; }
|
||||||
|
|
||||||
|
public long ProductPriceId { get; set; }
|
||||||
|
|
||||||
|
public int TotalAllocated { get; set; }
|
||||||
|
|
||||||
|
public int TotalReserved { get; set; }
|
||||||
|
}
|
||||||
@@ -2,6 +2,24 @@
|
|||||||
|
|
||||||
namespace LiteCharms.Features.MidrandBooks.Products.Models;
|
namespace LiteCharms.Features.MidrandBooks.Products.Models;
|
||||||
|
|
||||||
|
public sealed record ReserveStock
|
||||||
|
{
|
||||||
|
public required long ProductId { get; set; }
|
||||||
|
|
||||||
|
public required long ProductPriceId { get; set; }
|
||||||
|
|
||||||
|
public int Reservation { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed record AllocateStock
|
||||||
|
{
|
||||||
|
public required long ProductId { get; set; }
|
||||||
|
|
||||||
|
public required long ProductPriceId { get; set; }
|
||||||
|
|
||||||
|
public int Allocation { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
public sealed record CreateProduct
|
public sealed record CreateProduct
|
||||||
{
|
{
|
||||||
public required ProductTypes Type { get; set; }
|
public required ProductTypes Type { get; set; }
|
||||||
|
|||||||
@@ -1,14 +1,132 @@
|
|||||||
using LiteCharms.Features.MidrandBooks.Abstractions;
|
using LiteCharms.Features.Abstractions;
|
||||||
using LiteCharms.Features.MidrandBooks.Categories.Models;
|
using LiteCharms.Features.MidrandBooks.Categories.Models;
|
||||||
using LiteCharms.Features.MidrandBooks.Extensions;
|
using LiteCharms.Features.MidrandBooks.Extensions;
|
||||||
using LiteCharms.Features.MidrandBooks.Postgres;
|
using LiteCharms.Features.MidrandBooks.Postgres;
|
||||||
using LiteCharms.Features.MidrandBooks.Products.Models;
|
using LiteCharms.Features.MidrandBooks.Products.Models;
|
||||||
using LiteCharms.Features.Models;
|
using LiteCharms.Features.Models;
|
||||||
|
using Org.BouncyCastle.Asn1.Ocsp;
|
||||||
|
|
||||||
namespace LiteCharms.Features.MidrandBooks.Products;
|
namespace LiteCharms.Features.MidrandBooks.Products;
|
||||||
|
|
||||||
public sealed class ProductService(IDbContextFactory<MidrandBooksDbContext> contextFactory) : IService
|
public sealed class ProductService(IDbContextFactory<MidrandBooksDbContext> contextFactory) : IService
|
||||||
{
|
{
|
||||||
|
public async ValueTask<Result<ProductInventory>> CheckProductStockAvailabilityAsync(long productId, long productPriceId, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
|
||||||
|
|
||||||
|
var inventory = await context.Inventories
|
||||||
|
.AsNoTracking()
|
||||||
|
.Where(i => i.ProductPriceId == productPriceId && i.ProductId == productId)
|
||||||
|
.OrderByDescending(o => o.Id)
|
||||||
|
.FirstOrDefaultAsync(cancellationToken);
|
||||||
|
|
||||||
|
return inventory is not null
|
||||||
|
? Result.Ok(inventory.ToModel())
|
||||||
|
: Result.Fail<ProductInventory>("Product sold out");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return Result.Fail<ProductInventory>(new Error(ex.Message).CausedBy(ex));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ValueTask<Result<long>> ReserveProductInventoryAsync(ReserveStock request, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
|
||||||
|
|
||||||
|
var oldInventory = await context.Inventories
|
||||||
|
.AsNoTracking()
|
||||||
|
.Where(i => i.ProductPriceId == request.ProductPriceId && i.ProductId == request.ProductId)
|
||||||
|
.OrderByDescending(o => o.Id)
|
||||||
|
.FirstOrDefaultAsync(cancellationToken);
|
||||||
|
|
||||||
|
var newAllocation = 0;
|
||||||
|
var newReservation = 0;
|
||||||
|
|
||||||
|
if (oldInventory is not null)
|
||||||
|
{
|
||||||
|
newAllocation = oldInventory.TotalAllocated;
|
||||||
|
newReservation = oldInventory.TotalReserved + request.Reservation;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
newAllocation = 0;
|
||||||
|
newReservation = request.Reservation;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newAllocation - newReservation < 0)
|
||||||
|
return Result.Fail<long>("Allocation failure: The requested book quantity exceeds current physical inventory availability.");
|
||||||
|
|
||||||
|
var inventory = context.Inventories.Add(new Entities.ProductInventory
|
||||||
|
{
|
||||||
|
CreatedAt = DateTime.UtcNow,
|
||||||
|
ProductId = request.ProductId,
|
||||||
|
ProductPriceId = request.ProductPriceId,
|
||||||
|
Status = InventoryStatuses.Reserved,
|
||||||
|
TotalAllocated = newAllocation,
|
||||||
|
TotalReserved = newReservation,
|
||||||
|
});
|
||||||
|
|
||||||
|
return await context.SaveChangesAsync(cancellationToken) > 0
|
||||||
|
? Result.Ok(inventory.Entity.Id)
|
||||||
|
: Result.Fail<long>("Failed to create inventory entry");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return Result.Fail<long>(new Error(ex.Message).CausedBy(ex));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ValueTask<Result<long>> AllocateProductInventoryAsync(AllocateStock request, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);
|
||||||
|
|
||||||
|
var oldInventory = await context.Inventories
|
||||||
|
.AsNoTracking()
|
||||||
|
.Where(i => i.ProductPriceId == request.ProductPriceId && i.ProductId == request.ProductId)
|
||||||
|
.OrderByDescending(o => o.Id)
|
||||||
|
.FirstOrDefaultAsync(cancellationToken);
|
||||||
|
|
||||||
|
var newAllocation = 0;
|
||||||
|
var newReservation = 0;
|
||||||
|
|
||||||
|
if (oldInventory is not null)
|
||||||
|
{
|
||||||
|
newAllocation = oldInventory.TotalAllocated + request.Allocation;
|
||||||
|
newReservation = oldInventory.TotalReserved;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
newAllocation = request.Allocation;
|
||||||
|
newReservation = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
var inventory = context.Inventories.Add(new Entities.ProductInventory
|
||||||
|
{
|
||||||
|
CreatedAt = DateTime.UtcNow,
|
||||||
|
ProductId = request.ProductId,
|
||||||
|
ProductPriceId = request.ProductPriceId,
|
||||||
|
Status = InventoryStatuses.Adjustment,
|
||||||
|
TotalAllocated = newAllocation,
|
||||||
|
TotalReserved = newReservation,
|
||||||
|
});
|
||||||
|
|
||||||
|
return await context.SaveChangesAsync(cancellationToken) > 0
|
||||||
|
? Result.Ok(inventory.Entity.Id)
|
||||||
|
: Result.Fail<long>("Failed to create inventory entry");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return Result.Fail<long>(new Error(ex.Message).CausedBy(ex));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public async ValueTask<Result> AddProductCategoryAsync(long productId, long categoryId, CancellationToken cancellationToken = default)
|
public async ValueTask<Result> AddProductCategoryAsync(long productId, long categoryId, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
using LiteCharms.Features.Extensions;
|
using LiteCharms.Features.Hasher;
|
||||||
using LiteCharms.Features.Models;
|
using LiteCharms.Features.Models;
|
||||||
using LiteCharms.Features.TechShop.Extensions;
|
using LiteCharms.Features.TechShop.Extensions;
|
||||||
using LiteCharms.Features.TechShop.Leads.Models;
|
using LiteCharms.Features.TechShop.Leads.Models;
|
||||||
using LiteCharms.Features.TechShop.Postgres;
|
using LiteCharms.Features.TechShop.Postgres;
|
||||||
using static LiteCharms.Features.Extensions.Hash;
|
|
||||||
|
|
||||||
namespace LiteCharms.Features.TechShop.Leads;
|
namespace LiteCharms.Features.TechShop.Leads;
|
||||||
|
|
||||||
@@ -29,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 = StringToSha256Hash.Invoke($"{request.ClickId}{request.AppClickId}{request.WebClickId}")
|
AttributionHash = HashService.StringToSha256Hash($"{request.ClickId}{request.AppClickId}{request.WebClickId}")
|
||||||
});
|
});
|
||||||
|
|
||||||
return await context.SaveChangesAsync(cancellationToken) > 0
|
return await context.SaveChangesAsync(cancellationToken) > 0
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ public class Fixture : IDisposable
|
|||||||
.AddGarageS3(Configuration)
|
.AddGarageS3(Configuration)
|
||||||
.AddEmailServices(Configuration)
|
.AddEmailServices(Configuration)
|
||||||
.AddSingleton(Configuration)
|
.AddSingleton(Configuration)
|
||||||
|
.AddHashServices(Configuration)
|
||||||
.BuildServiceProvider();
|
.BuildServiceProvider();
|
||||||
|
|
||||||
Mediator = Services.GetRequiredService<IMediator>();
|
Mediator = Services.GetRequiredService<IMediator>();
|
||||||
|
|||||||
@@ -0,0 +1,155 @@
|
|||||||
|
using LiteCharms.Features.Hasher;
|
||||||
|
using LiteCharms.Features.Models;
|
||||||
|
using static LiteCharms.Features.Extensions.Hash;
|
||||||
|
|
||||||
|
namespace LiteCharms.Features.Tests;
|
||||||
|
|
||||||
|
public class HashServiceFeatureTests(Fixture fixture) : IClassFixture<Fixture>
|
||||||
|
{
|
||||||
|
private readonly HashService hashService = fixture.Services.GetRequiredService<HashService>();
|
||||||
|
private readonly string payfastPassphrase = fixture.Configuration.GetSection("HasherSettings:PayfastPassphrase").Value!;
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void StringToSha256Hash_Should_GenerateHash()
|
||||||
|
{
|
||||||
|
var input = "We are the best";
|
||||||
|
var expectedHash = "96E17275B53F6BEB7A0D1C4F789F226D3C71CBE398585F25B3028F2B432E78AB";
|
||||||
|
|
||||||
|
var result = HashService.StringToSha256Hash(input);
|
||||||
|
|
||||||
|
Assert.NotNull(result);
|
||||||
|
Assert.True(HashService.IsSha256Hash(result));
|
||||||
|
Assert.Equal(expectedHash, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void StreamToSha256Hash_Should_GenerateHash()
|
||||||
|
{
|
||||||
|
var input = "We are successful";
|
||||||
|
using var stream = new MemoryStream(Encoding.UTF8.GetBytes(input));
|
||||||
|
var expectedHash = "C27872EE494B09D72203C98FC858268F3CD3492D62AA7B766A873520C2C73AFB";
|
||||||
|
|
||||||
|
var result = HashService.StreamToSha256Hash(stream);
|
||||||
|
|
||||||
|
Assert.NotNull(result);
|
||||||
|
Assert.True(HashService.IsSha256Hash(result));
|
||||||
|
Assert.Equal(expectedHash, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void BytesToSha256Hash_Should_GenerateHash()
|
||||||
|
{
|
||||||
|
var inputBytes = Encoding.UTF8.GetBytes("We are wealthy");
|
||||||
|
var expectedHash = "3876BF98F6E4A8E42B22C40415687D6FF13F0E887F3F508B71852298FC665737";
|
||||||
|
|
||||||
|
var result = HashService.BytesToSha256Hash(inputBytes);
|
||||||
|
|
||||||
|
Assert.NotNull(result);
|
||||||
|
Assert.True(HashService.IsSha256Hash(result));
|
||||||
|
Assert.Equal(expectedHash, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ToMd5Hash_Should_GenerateHash()
|
||||||
|
{
|
||||||
|
var input = "We manifest our desired destiny";
|
||||||
|
var expectedMd5Lowercase = "6c7816869bcebe4634f7afe9c66dfa08";
|
||||||
|
|
||||||
|
var result = HashService.ToMd5Hash(input);
|
||||||
|
|
||||||
|
Assert.True(result.IsSuccess);
|
||||||
|
Assert.True(HashService.IsMd5Hash(result.Value));
|
||||||
|
Assert.Equal(expectedMd5Lowercase, result.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void VerifyPayfastWebhookSignature_Should_GenerateHash()
|
||||||
|
{
|
||||||
|
var paymentId = hashService.HashEncodeLongId(1001).Value;
|
||||||
|
|
||||||
|
var payload = new PayfastWebhookPayload
|
||||||
|
{
|
||||||
|
Amount = "350.00",
|
||||||
|
ItemName = "System Architecture Book",
|
||||||
|
MPaymentId = paymentId,
|
||||||
|
};
|
||||||
|
|
||||||
|
var rawPayload = payload.ToRawPayfastPayload(payfastPassphrase);
|
||||||
|
|
||||||
|
var generatedSignature = HashService.ToMd5Hash(rawPayload).Value;
|
||||||
|
|
||||||
|
var result = hashService.VerifyPayfastWebhookSignature(payload, generatedSignature);
|
||||||
|
|
||||||
|
Assert.True(result.IsSuccess);
|
||||||
|
Assert.True(result.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void HashEncodeHex_Should_GenerateHash()
|
||||||
|
{
|
||||||
|
var validHexInput = "DEADBEEF42";
|
||||||
|
|
||||||
|
var result = hashService.HashEncodeHex(validHexInput);
|
||||||
|
|
||||||
|
Assert.True(result.IsSuccess);
|
||||||
|
Assert.False(string.IsNullOrWhiteSpace(result.Value));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void HashEncodeIntId_Should_GenerateHash()
|
||||||
|
{
|
||||||
|
int targetId = 42;
|
||||||
|
|
||||||
|
var result = hashService.HashEncodeIntId(targetId);
|
||||||
|
|
||||||
|
Assert.True(result.IsSuccess);
|
||||||
|
Assert.True(result.Value.Length >= 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void HashEncodeLongId_Should_GenerateHash()
|
||||||
|
{
|
||||||
|
long targetId = 9904185012L;
|
||||||
|
|
||||||
|
var result = hashService.HashEncodeLongId(targetId);
|
||||||
|
|
||||||
|
Assert.True(result.IsSuccess);
|
||||||
|
Assert.True(result.Value.Length >= 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void DecodeIntIdHash_Should_GenerateHash()
|
||||||
|
{
|
||||||
|
int originalId = 88041;
|
||||||
|
var hashedString = hashService.HashEncodeIntId(originalId).Value;
|
||||||
|
|
||||||
|
var result = hashService.DecodeIntIdHash(hashedString);
|
||||||
|
|
||||||
|
Assert.True(result.IsSuccess);
|
||||||
|
Assert.Equal(originalId, result.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void DecodeLongIdHash_Should_GenerateHash()
|
||||||
|
{
|
||||||
|
long originalId = 9081230491823L;
|
||||||
|
var hashedString = hashService.HashEncodeLongId(originalId).Value;
|
||||||
|
|
||||||
|
var result = hashService.DecodeLongIdHash(hashedString);
|
||||||
|
|
||||||
|
Assert.True(result.IsSuccess);
|
||||||
|
Assert.Equal(originalId, result.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void DecodeHexHash_Should_GenerateHash()
|
||||||
|
{
|
||||||
|
var originalHex = "ABCDEF12345";
|
||||||
|
var hashedString = hashService.HashEncodeHex(originalHex).Value;
|
||||||
|
|
||||||
|
var result = hashService.DecodeHexHash(hashedString);
|
||||||
|
|
||||||
|
Assert.True(result.IsSuccess);
|
||||||
|
Assert.Equal(originalHex, result.Value);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -27,6 +27,8 @@
|
|||||||
|
|
||||||
<!-- Global Usings -->
|
<!-- Global Usings -->
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<Using Include="System.Net" />
|
||||||
|
<Using Include="System.Text" />
|
||||||
<Using Include="Mediator" />
|
<Using Include="Mediator" />
|
||||||
<Using Include="Xunit.Abstractions" />
|
<Using Include="Xunit.Abstractions" />
|
||||||
<Using Include="Microsoft.Extensions.DependencyInjection" />
|
<Using Include="Microsoft.Extensions.DependencyInjection" />
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
namespace LiteCharms.Features.Abstractions;
|
||||||
|
|
||||||
|
public interface IService;
|
||||||
@@ -1,5 +1,35 @@
|
|||||||
namespace LiteCharms.Features;
|
namespace LiteCharms.Features;
|
||||||
|
|
||||||
|
public enum InventoryStatuses : int
|
||||||
|
{
|
||||||
|
Adjustment = 0,
|
||||||
|
Reserved = 1,
|
||||||
|
Released = 2,
|
||||||
|
Sold = 3,
|
||||||
|
Replenished = 4,
|
||||||
|
Correction = 5,
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum LedgerStatuses : int
|
||||||
|
{
|
||||||
|
Changed = 0,
|
||||||
|
Sent = 1,
|
||||||
|
Received = 2,
|
||||||
|
Refunded = 3,
|
||||||
|
Cancelled = 4,
|
||||||
|
Failed = 5,
|
||||||
|
Partial = 6,
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum PaymentStatuses : int
|
||||||
|
{
|
||||||
|
NotPaid = 0,
|
||||||
|
Paid = 1,
|
||||||
|
Cancelled = 2,
|
||||||
|
Requested = 3,
|
||||||
|
Failed = 4,
|
||||||
|
}
|
||||||
|
|
||||||
public enum ShippingProviderTypes : int
|
public enum ShippingProviderTypes : int
|
||||||
{
|
{
|
||||||
Dsv = 0,
|
Dsv = 0,
|
||||||
@@ -115,3 +145,64 @@ public enum Priorities : int
|
|||||||
Medium = 1,
|
Medium = 1,
|
||||||
High = 2,
|
High = 2,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public enum PublisherTypes : int
|
||||||
|
{
|
||||||
|
Individual = 0,
|
||||||
|
Company = 1,
|
||||||
|
Organization = 2,
|
||||||
|
SelfPublished = 3,
|
||||||
|
UniversityPress = 4,
|
||||||
|
GovernmentAgency = 5,
|
||||||
|
NonProfit = 6,
|
||||||
|
Independent = 7
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum BookTypes : int
|
||||||
|
{
|
||||||
|
Fiction = 0,
|
||||||
|
NonFiction = 1,
|
||||||
|
Academic = 2,
|
||||||
|
SelfHelp = 3,
|
||||||
|
Biography = 4,
|
||||||
|
Poetry = 5,
|
||||||
|
Children = 6,
|
||||||
|
YoungAdult = 7,
|
||||||
|
ScienceFiction = 8,
|
||||||
|
Fantasy = 9
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum BookContentTypes : int
|
||||||
|
{
|
||||||
|
Text = 0,
|
||||||
|
Image = 1,
|
||||||
|
Video = 2,
|
||||||
|
Audio = 3,
|
||||||
|
Interactive = 4,
|
||||||
|
Markdown = 5,
|
||||||
|
Html = 6,
|
||||||
|
Json = 7,
|
||||||
|
Yaml = 8
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum BookPageTypes : int
|
||||||
|
{
|
||||||
|
Cover = 0,
|
||||||
|
Preface = 1,
|
||||||
|
Introduction = 2,
|
||||||
|
Content = 3,
|
||||||
|
Closing = 4,
|
||||||
|
Referencer = 5,
|
||||||
|
Credits = 6,
|
||||||
|
BackCover = 7
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum ProductTypes : int
|
||||||
|
{
|
||||||
|
Book = 1,
|
||||||
|
Journal = 2,
|
||||||
|
Magazine = 3,
|
||||||
|
EBook = 4,
|
||||||
|
Audiobook = 5,
|
||||||
|
Accessory = 6
|
||||||
|
}
|
||||||
@@ -1,13 +1,102 @@
|
|||||||
namespace LiteCharms.Features.Extensions;
|
using LiteCharms.Features.Hasher;
|
||||||
|
using LiteCharms.Features.Hasher.Configuration;
|
||||||
|
using LiteCharms.Features.Models;
|
||||||
|
|
||||||
|
namespace LiteCharms.Features.Extensions;
|
||||||
|
|
||||||
public static class Hash
|
public static class Hash
|
||||||
{
|
{
|
||||||
public static readonly Func<string?, string?> StringToSha256Hash = (input) =>
|
public const string HasherConfigSectionName = "HasherSettings";
|
||||||
Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(input!)));
|
|
||||||
|
|
||||||
public static readonly Func<Stream, string?> StreamToSha256Hash = (stream) =>
|
public static IServiceCollection AddHashServices(this IServiceCollection services, IConfiguration configuration)
|
||||||
Convert.ToHexString(SHA256.HashData(stream));
|
{
|
||||||
|
services.Configure<HasherSettings>(configuration.GetSection(HasherConfigSectionName));
|
||||||
|
|
||||||
public static readonly Func<byte[], string?> BytesToSha256Hash = (bytes) =>
|
var settings = configuration.GetSection(HasherConfigSectionName).Get<HasherSettings>();
|
||||||
Convert.ToHexString(SHA256.HashData(bytes));
|
|
||||||
|
services.AddSingleton<IHashids>(_ =>
|
||||||
|
new Hashids(settings!.Salt, minHashLength: settings.MinHashLength));
|
||||||
|
|
||||||
|
services.AddSingleton<HashService>();
|
||||||
|
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string ToRawPayfastPayload(this PayfastWebhookPayload input, string passphrase)
|
||||||
|
{
|
||||||
|
var parameters = new List<string>();
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(input.Amount))
|
||||||
|
parameters.Add($"amount={WebUtility.UrlEncode(input.Amount)}");
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(input.ItemName))
|
||||||
|
parameters.Add($"item_name={WebUtility.UrlEncode(input.ItemName)}");
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(input.MPaymentId))
|
||||||
|
parameters.Add($"m_payment_id={WebUtility.UrlEncode(input.MPaymentId)}");
|
||||||
|
|
||||||
|
string payload = string.Join("&", parameters);
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(passphrase))
|
||||||
|
payload += $"&passphrase={WebUtility.UrlEncode(passphrase)}";
|
||||||
|
|
||||||
|
return payload;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static (PayfastWebhookPayload Payload, string Passphrase) FromRawPayfastPayload(this string rawPayload)
|
||||||
|
{
|
||||||
|
string passphrase = string.Empty;
|
||||||
|
var payload = new PayfastWebhookPayload();
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(rawPayload)) return (payload, passphrase);
|
||||||
|
|
||||||
|
var segments = rawPayload.Split('&', StringSplitOptions.RemoveEmptyEntries);
|
||||||
|
|
||||||
|
foreach (var segment in segments)
|
||||||
|
{
|
||||||
|
int delimiterIndex = segment.IndexOf('=');
|
||||||
|
if (delimiterIndex == -1)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
string key = segment[..delimiterIndex].Trim();
|
||||||
|
string rawValue = segment[(delimiterIndex + 1)..];
|
||||||
|
|
||||||
|
string decodedValue = WebUtility.UrlDecode(rawValue);
|
||||||
|
|
||||||
|
switch (key.ToLowerInvariant())
|
||||||
|
{
|
||||||
|
case "amount":
|
||||||
|
payload.Amount = decodedValue;
|
||||||
|
break;
|
||||||
|
case "item_name":
|
||||||
|
payload.ItemName = decodedValue;
|
||||||
|
break;
|
||||||
|
case "m_payment_id":
|
||||||
|
payload.MPaymentId = decodedValue;
|
||||||
|
break;
|
||||||
|
case "passphrase":
|
||||||
|
passphrase = decodedValue;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (payload, passphrase);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static (PayfastWebhookPayload Payload, string Passphrase) FromRawPayfastPayload(this IFormCollection form)
|
||||||
|
{
|
||||||
|
string passphrase = string.Empty;
|
||||||
|
var payload = new PayfastWebhookPayload();
|
||||||
|
|
||||||
|
if (form.IsNullOrEmpty()) return (payload, passphrase);
|
||||||
|
|
||||||
|
payload = new PayfastWebhookPayload
|
||||||
|
{
|
||||||
|
Amount = form.TryGetValue("amount", out var amountValues) ? amountValues.ToString() : null,
|
||||||
|
ItemName = form.TryGetValue("item_name", out var itemValues) ? itemValues.ToString() : null,
|
||||||
|
MPaymentId = form.TryGetValue("m_payment_id", out var paymentIdValues) ? paymentIdValues.ToString() : null,
|
||||||
|
};
|
||||||
|
|
||||||
|
return (payload, passphrase);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
namespace LiteCharms.Features.Hasher.Configuration;
|
||||||
|
|
||||||
|
public sealed class HasherSettings
|
||||||
|
{
|
||||||
|
public string? Salt { get; set; }
|
||||||
|
|
||||||
|
public int MinHashLength { get; set; }
|
||||||
|
|
||||||
|
public string? PayfastPassphrase { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,131 @@
|
|||||||
|
using LiteCharms.Features.Abstractions;
|
||||||
|
using LiteCharms.Features.Hasher.Configuration;
|
||||||
|
using LiteCharms.Features.Models;
|
||||||
|
|
||||||
|
namespace LiteCharms.Features.Hasher;
|
||||||
|
|
||||||
|
public sealed partial class HashService(IHashids hasher, IOptions<HasherSettings> options) : IService
|
||||||
|
{
|
||||||
|
private readonly HasherSettings settings = options.Value;
|
||||||
|
|
||||||
|
[GeneratedRegex(@"\A\b[0-9a-fA-F]+\b\Z")]
|
||||||
|
private static partial Regex HexHashRegex { get; }
|
||||||
|
|
||||||
|
[GeneratedRegex(@"\A[0-9a-fA-F]{32}\Z", RegexOptions.None, matchTimeoutMilliseconds: 100)]
|
||||||
|
private static partial Regex Md5Regex { get; }
|
||||||
|
|
||||||
|
[GeneratedRegex(@"\A[0-9a-fA-F]{64}\Z", RegexOptions.None, matchTimeoutMilliseconds: 100)]
|
||||||
|
private static partial Regex Sha256Regex { get; }
|
||||||
|
|
||||||
|
public static bool IsMd5Hash(string? value) =>
|
||||||
|
!string.IsNullOrWhiteSpace(value) && Md5Regex.IsMatch(value);
|
||||||
|
|
||||||
|
public static bool IsSha256Hash(string? value) =>
|
||||||
|
!string.IsNullOrWhiteSpace(value) && Sha256Regex.IsMatch(value);
|
||||||
|
|
||||||
|
public static string? StringToSha256Hash(string? input) =>
|
||||||
|
string.IsNullOrEmpty(input) ? null : Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(input)));
|
||||||
|
|
||||||
|
public static string? StreamToSha256Hash(Stream stream) =>
|
||||||
|
stream is null ? null : Convert.ToHexString(SHA256.HashData(stream));
|
||||||
|
|
||||||
|
public static string? BytesToSha256Hash(byte[] bytes) =>
|
||||||
|
bytes is null ? null : Convert.ToHexString(SHA256.HashData(bytes));
|
||||||
|
|
||||||
|
public static Result<string> ToMd5Hash(string input)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(input))
|
||||||
|
return Result.Fail<string>("Input content cannot be null or empty for MD5 processing.");
|
||||||
|
|
||||||
|
byte[] bytes = MD5.HashData(Encoding.UTF8.GetBytes(input));
|
||||||
|
return Result.Ok(Convert.ToHexString(bytes).ToLowerInvariant());
|
||||||
|
}
|
||||||
|
|
||||||
|
public Result<bool> VerifyPayfastWebhookSignature(PayfastWebhookPayload payload, string incomingSignature)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(incomingSignature))
|
||||||
|
return Result.Fail<bool>("Validation failed: Missing signature string parameter.");
|
||||||
|
|
||||||
|
var parameters = new List<string>();
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(payload.Amount))
|
||||||
|
parameters.Add($"amount={WebUtility.UrlEncode(payload.Amount)}");
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(payload.ItemName))
|
||||||
|
parameters.Add($"item_name={WebUtility.UrlEncode(payload.ItemName)}");
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(payload.MPaymentId))
|
||||||
|
parameters.Add($"m_payment_id={WebUtility.UrlEncode(payload.MPaymentId)}");
|
||||||
|
|
||||||
|
string signatureString = string.Join("&", parameters);
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(settings.PayfastPassphrase))
|
||||||
|
signatureString += $"&passphrase={WebUtility.UrlEncode(settings.PayfastPassphrase)}";
|
||||||
|
|
||||||
|
var localHashResult = ToMd5Hash(signatureString);
|
||||||
|
|
||||||
|
if (!localHashResult.IsSuccess)
|
||||||
|
return Result.Fail<bool>(localHashResult.Errors);
|
||||||
|
|
||||||
|
bool isValid = string.Equals(localHashResult.Value, incomingSignature, StringComparison.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
return Result.Ok(isValid);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return Result.Fail<bool>(new Error("An error occurred during Payfast MD5 verification.").CausedBy(ex));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Result<string> HashEncodeHex(string input) => string.IsNullOrWhiteSpace(input) || !HexHashRegex.IsMatch(input)
|
||||||
|
? Result.Fail<string>("Input must be a valid hexadecimal string.")
|
||||||
|
: Result.Ok(hasher.EncodeHex(input));
|
||||||
|
|
||||||
|
public Result<string> HashEncodeIntId(int id) => id < 0
|
||||||
|
? Result.Fail<string>("Id cannot be negative.")
|
||||||
|
: Result.Ok(hasher.Encode(id));
|
||||||
|
|
||||||
|
public Result<string> HashEncodeLongId(long id) => id < 0
|
||||||
|
? Result.Fail<string>("Id cannot be negative.")
|
||||||
|
: Result.Ok(hasher.EncodeLong(id));
|
||||||
|
|
||||||
|
public Result<int> DecodeIntIdHash(string hash)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(hash)) return Result.Fail<int>("Invalid token layout.");
|
||||||
|
|
||||||
|
int[] decoded = hasher.Decode(hash);
|
||||||
|
|
||||||
|
return decoded.Length == 1 ? Result.Ok(decoded[0]) : Result.Fail<int>("Invalid or modified Int hash token.");
|
||||||
|
}
|
||||||
|
|
||||||
|
public Result<long> DecodeLongIdHash(string hash)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(hash)) return Result.Fail<long>("Invalid token layout.");
|
||||||
|
|
||||||
|
long[] decoded = hasher.DecodeLong(hash);
|
||||||
|
|
||||||
|
return decoded.Length == 1 ? Result.Ok(decoded[0]) : Result.Fail<long>("Invalid or modified Long hash token.");
|
||||||
|
}
|
||||||
|
|
||||||
|
public Result<string> DecodeHexHash(string hex)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
string decoded = hasher.DecodeHex(hex);
|
||||||
|
|
||||||
|
return string.IsNullOrEmpty(decoded)
|
||||||
|
? Result.Fail<string>("Invalid or corrupted hex hash.")
|
||||||
|
: Result.Ok(decoded);
|
||||||
|
}
|
||||||
|
catch (FormatException fex)
|
||||||
|
{
|
||||||
|
return Result.Fail<string>(new Error("Invalid hash structure.").CausedBy(fex));
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return Result.Fail<string>(new Error(ex.Message).CausedBy(ex));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -31,6 +31,7 @@
|
|||||||
|
|
||||||
<!-- Quartz Scheduler-->
|
<!-- Quartz Scheduler-->
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Hashids.net" Version="1.7.0" />
|
||||||
<PackageReference Include="Meziantou.Analyzer" Version="3.0.96">
|
<PackageReference Include="Meziantou.Analyzer" Version="3.0.96">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
@@ -146,6 +147,11 @@
|
|||||||
|
|
||||||
<!-- Shared Usings -->
|
<!-- Shared Usings -->
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<Using Include="Microsoft.IdentityModel.Tokens" />
|
||||||
|
<Using Include="Microsoft.AspNetCore.Http" />
|
||||||
|
<Using Include="HashidsNet" />
|
||||||
|
<Using Include="System.Net" />
|
||||||
|
<Using Include="System.Text.RegularExpressions" />
|
||||||
<Using Include="System.Globalization" />
|
<Using Include="System.Globalization" />
|
||||||
<Using Include="Microsoft.AspNetCore.Builder" />
|
<Using Include="Microsoft.AspNetCore.Builder" />
|
||||||
<Using Include="Microsoft.Extensions.Hosting" />
|
<Using Include="Microsoft.Extensions.Hosting" />
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
namespace LiteCharms.Features.Models;
|
||||||
|
|
||||||
|
public sealed class PayfastWebhookPayload
|
||||||
|
{
|
||||||
|
public string? Amount { get; set; }
|
||||||
|
public string? ItemName { get; set; }
|
||||||
|
public string? MPaymentId { get; set; }
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
using static LiteCharms.Features.Extensions.Hash;
|
using LiteCharms.Features.Hasher;
|
||||||
|
|
||||||
namespace LiteCharms.Features.S3.Abstractions;
|
namespace LiteCharms.Features.S3.Abstractions;
|
||||||
|
|
||||||
@@ -26,7 +26,7 @@ public abstract class S3ServiceBase(IAmazonS3 amazonS3)
|
|||||||
|
|
||||||
stream.Seek(0, SeekOrigin.Begin);
|
stream.Seek(0, SeekOrigin.Begin);
|
||||||
|
|
||||||
var fileHash = StreamToSha256Hash(stream);
|
var fileHash = HashService.StreamToSha256Hash(stream);
|
||||||
|
|
||||||
if(string.IsNullOrWhiteSpace(fileHash))
|
if(string.IsNullOrWhiteSpace(fileHash))
|
||||||
return Result.Fail<string>("Failed to compute file hash.");
|
return Result.Fail<string>("Failed to compute file hash.");
|
||||||
@@ -39,7 +39,7 @@ public abstract class S3ServiceBase(IAmazonS3 amazonS3)
|
|||||||
Key = fileKey,
|
Key = fileKey,
|
||||||
InputStream = stream,
|
InputStream = stream,
|
||||||
ContentType = contentType,
|
ContentType = contentType,
|
||||||
UseChunkEncoding = false
|
UseChunkEncoding = false,
|
||||||
};
|
};
|
||||||
|
|
||||||
stream.Seek(0, SeekOrigin.Begin);
|
stream.Seek(0, SeekOrigin.Begin);
|
||||||
|
|||||||
Reference in New Issue
Block a user