Added service ordering and implemented basic service creation

This commit is contained in:
Marcel Baumgartner 2023-10-19 11:35:14 +02:00
parent b19208b3b0
commit 48c95d4ec6
15 changed files with 370 additions and 19 deletions

View file

@ -0,0 +1,22 @@
using Moonlight.App.Database.Entities.Store;
using Moonlight.App.Models.Abstractions;
namespace Moonlight.App.Actions.Dummy;
public class DummyActions : ServiceActions
{
public override Task Create(IServiceProvider provider, Service service)
{
return Task.CompletedTask;
}
public override Task Update(IServiceProvider provider, Service service)
{
return Task.CompletedTask;
}
public override Task Delete(IServiceProvider provider, Service service)
{
return Task.CompletedTask;
}
}

View file

@ -16,7 +16,7 @@ public class Product
public int MaxPerUser { get; set; } public int MaxPerUser { get; set; }
public int Duration { get; set; } public int Duration { get; set; }
public ProductType Type { get; set; } public ServiceType Type { get; set; }
public string ConfigJson { get; set; } = "{}"; public string ConfigJson { get; set; } = "{}";
public DateTime CreatedAt { get; set; } = DateTime.UtcNow; public DateTime CreatedAt { get; set; } = DateTime.UtcNow;

View file

@ -1,6 +1,6 @@
namespace Moonlight.App.Database.Enums; namespace Moonlight.App.Database.Enums;
public enum ProductType public enum ServiceType
{ {
Server, Server,
Webspace, Webspace,

View file

@ -0,0 +1,10 @@
using Moonlight.App.Database.Entities.Store;
namespace Moonlight.App.Models.Abstractions;
public abstract class ServiceActions
{
public abstract Task Create(IServiceProvider provider, Service service);
public abstract Task Update(IServiceProvider provider, Service service);
public abstract Task Delete(IServiceProvider provider, Service service);
}

View file

@ -31,6 +31,6 @@ public class AddProductForm
[Range(0, double.MaxValue, ErrorMessage = "The duration needs to be equals or above 0")] [Range(0, double.MaxValue, ErrorMessage = "The duration needs to be equals or above 0")]
public int Duration { get; set; } public int Duration { get; set; }
public ProductType Type { get; set; } public ServiceType Type { get; set; }
public string ConfigJson { get; set; } = "{}"; public string ConfigJson { get; set; } = "{}";
} }

View file

@ -31,6 +31,6 @@ public class EditProductForm
[Range(0, double.MaxValue, ErrorMessage = "The duration needs to be equals or above 0")] [Range(0, double.MaxValue, ErrorMessage = "The duration needs to be equals or above 0")]
public int Duration { get; set; } public int Duration { get; set; }
public ProductType Type { get; set; } public ServiceType Type { get; set; }
public string ConfigJson { get; set; } = "{}"; public string ConfigJson { get; set; } = "{}";
} }

View file

@ -0,0 +1,63 @@
using Microsoft.EntityFrameworkCore;
using Moonlight.App.Database.Entities;
using Moonlight.App.Database.Entities.Store;
using Moonlight.App.Database.Enums;
using Moonlight.App.Exceptions;
using Moonlight.App.Models.Abstractions;
using Moonlight.App.Repositories;
namespace Moonlight.App.Services.ServiceManage;
public class ServiceAdminService
{
public readonly Dictionary<ServiceType, ServiceActions> Actions = new();
private readonly IServiceScopeFactory ServiceScopeFactory;
public ServiceAdminService(IServiceScopeFactory serviceScopeFactory)
{
ServiceScopeFactory = serviceScopeFactory;
}
public async Task<Service> Create(User u, Product p, Action<Service>? modifyService = null)
{
if (!Actions.ContainsKey(p.Type))
throw new DisplayException($"The product type {p.Type} is not registered");
// Load models in new scope
using var scope = ServiceScopeFactory.CreateScope();
var userRepo = scope.ServiceProvider.GetRequiredService<Repository<User>>();
var productRepo = scope.ServiceProvider.GetRequiredService<Repository<Product>>();
var serviceRepo = scope.ServiceProvider.GetRequiredService<Repository<Service>>();
var user = userRepo.Get().First(x => x.Id == u.Id);
var product = productRepo.Get().First(x => x.Id == p.Id);
// Create database model
var service = new Service()
{
Product = product,
Owner = user,
Suspended = false,
CreatedAt = DateTime.UtcNow
};
// Allow further modifications
if(modifyService != null)
modifyService.Invoke(service);
// Add new service in database
var finishedService = serviceRepo.Add(service);
// Call the action for the logic behind the service type
var actions = Actions[product.Type];
await actions.Create(scope.ServiceProvider, finishedService);
return finishedService;
}
public Task RegisterAction(ServiceType type, ServiceActions actions) // Use this function to register service types
{
Actions.Add(type, actions);
return Task.CompletedTask;
}
}

View file

@ -0,0 +1,43 @@
using Microsoft.EntityFrameworkCore;
using Moonlight.App.Database.Entities;
using Moonlight.App.Database.Entities.Store;
using Moonlight.App.Repositories;
namespace Moonlight.App.Services.ServiceManage;
public class ServiceService // This service is used for managing services and create the connection to the actual logic behind a service type
{
private readonly IServiceProvider ServiceProvider;
private readonly Repository<Service> ServiceRepository;
public ServiceAdminService Admin => ServiceProvider.GetRequiredService<ServiceAdminService>();
public ServiceService(IServiceProvider serviceProvider, Repository<Service> serviceRepository)
{
ServiceProvider = serviceProvider;
ServiceRepository = serviceRepository;
}
public Task<Service[]> Get(User user)
{
var result = ServiceRepository
.Get()
.Include(x => x.Product)
.Where(x => x.Owner.Id == user.Id)
.ToArray();
return Task.FromResult(result);
}
public Task<Service[]> GetShared(User user)
{
var result = ServiceRepository
.Get()
.Include(x => x.Product)
.Include(x => x.Owner)
.Where(x => x.Shares.Any(y => y.User.Id == user.Id))
.ToArray();
return Task.FromResult(result);
}
}

View file

@ -31,7 +31,7 @@ public class StoreAdminService
return Task.FromResult(result); return Task.FromResult(result);
} }
public Task<Product> AddProduct(string name, string description, string slug, ProductType type, string configJson, public Task<Product> AddProduct(string name, string description, string slug, ServiceType type, string configJson,
Action<Product>? modifyProduct = null) Action<Product>? modifyProduct = null)
{ {
if (ProductRepository.Get().Any(x => x.Slug == slug)) if (ProductRepository.Get().Any(x => x.Slug == slug))

View file

@ -3,6 +3,7 @@ using Moonlight.App.Database.Entities;
using Moonlight.App.Database.Entities.Store; using Moonlight.App.Database.Entities.Store;
using Moonlight.App.Exceptions; using Moonlight.App.Exceptions;
using Moonlight.App.Repositories; using Moonlight.App.Repositories;
using Moonlight.App.Services.ServiceManage;
namespace Moonlight.App.Services.Store; namespace Moonlight.App.Services.Store;
@ -84,4 +85,33 @@ public class StoreOrderService
return Task.CompletedTask; return Task.CompletedTask;
} }
public async Task<Service> Process(User u, Product p, int durationMultiplier, Coupon? c)
{
// Validate to ensure we dont process an illegal order
await Validate(u, p, durationMultiplier, c);
// Create scope and get required services
using var scope = ServiceScopeFactory.CreateScope();
var serviceService = scope.ServiceProvider.GetRequiredService<ServiceService>();
var transactionService = scope.ServiceProvider.GetRequiredService<TransactionService>();
// Calculate price
var price = p.Price * durationMultiplier;
if (c != null)
price = Math.Round(price * c.Percent / 100, 2);
// Calculate duration
var duration = durationMultiplier * p.Duration;
// Add transaction
await transactionService.Add(u, -1 * price, $"Bought product '{p.Name}' for {duration} days");
// Create service
return await serviceService.Admin.Create(u, p, service =>
{
service.RenewAt = DateTime.UtcNow.AddDays(duration);
});
}
} }

View file

@ -0,0 +1,35 @@
using Moonlight.App.Database.Entities;
using Moonlight.App.Database.Entities.Store;
using Moonlight.App.Repositories;
namespace Moonlight.App.Services.Store;
public class TransactionService
{
private readonly Repository<User> UserRepository;
public TransactionService(Repository<User> userRepository)
{
UserRepository = userRepository;
}
public Task Add(User u, double amount, string message)
{
var user = UserRepository.Get().First(x => x.Id == u.Id); // Load user with current repo
user.Transactions.Add(new Transaction()
{
Text = message,
Price = amount
});
UserRepository.Update(user);
// We divide the call to ensure the transaction can be written to the database
user.Balance += amount;
UserRepository.Update(user);
return Task.CompletedTask;
}
}

View file

@ -1,5 +1,7 @@
using BlazorTable; using BlazorTable;
using Moonlight.App.Actions.Dummy;
using Moonlight.App.Database; using Moonlight.App.Database;
using Moonlight.App.Database.Enums;
using Moonlight.App.Extensions; using Moonlight.App.Extensions;
using Moonlight.App.Helpers; using Moonlight.App.Helpers;
using Moonlight.App.Helpers.LogMigrator; using Moonlight.App.Helpers.LogMigrator;
@ -7,6 +9,7 @@ using Moonlight.App.Repositories;
using Moonlight.App.Services; using Moonlight.App.Services;
using Moonlight.App.Services.Background; using Moonlight.App.Services.Background;
using Moonlight.App.Services.Interop; using Moonlight.App.Services.Interop;
using Moonlight.App.Services.ServiceManage;
using Moonlight.App.Services.Store; using Moonlight.App.Services.Store;
using Moonlight.App.Services.Users; using Moonlight.App.Services.Users;
using Moonlight.App.Services.Utils; using Moonlight.App.Services.Utils;
@ -44,6 +47,7 @@ builder.Services.AddScoped<AlertService>();
builder.Services.AddScoped<StoreService>(); builder.Services.AddScoped<StoreService>();
builder.Services.AddScoped<StoreAdminService>(); builder.Services.AddScoped<StoreAdminService>();
builder.Services.AddScoped<StoreOrderService>(); builder.Services.AddScoped<StoreOrderService>();
builder.Services.AddScoped<TransactionService>();
// Services / Users // Services / Users
builder.Services.AddScoped<UserService>(); builder.Services.AddScoped<UserService>();
@ -53,6 +57,10 @@ builder.Services.AddScoped<UserDetailsService>();
// Services / Background // Services / Background
builder.Services.AddSingleton<AutoMailSendService>(); builder.Services.AddSingleton<AutoMailSendService>();
// Services / ServiceManage
builder.Services.AddScoped<ServiceService>();
builder.Services.AddSingleton<ServiceAdminService>();
// Services // Services
builder.Services.AddScoped<IdentityService>(); builder.Services.AddScoped<IdentityService>();
builder.Services.AddSingleton<ConfigService>(); builder.Services.AddSingleton<ConfigService>();
@ -86,4 +94,7 @@ app.MapControllers();
// Auto start background services // Auto start background services
app.Services.GetRequiredService<AutoMailSendService>(); app.Services.GetRequiredService<AutoMailSendService>();
var serviceService = app.Services.GetRequiredService<ServiceAdminService>();
await serviceService.RegisterAction(ServiceType.Server, new DummyActions());
app.Run(); app.Run();

View file

@ -1,34 +1,49 @@
@if (loaded) @if (Loaded)
{ {
@ChildContent @ChildContent
} }
else else
{
if (ShowAsCard)
{
<div class="card card-body">
<div class="d-flex justify-content-center py-4">
<span class="fs-1 spinner-border spinner-border-lg align-middle me-2"></span>
<span class="mt-3 fs-5">@(Text)</span>
</div>
</div>
}
else
{ {
<div class="d-flex justify-content-center py-4"> <div class="d-flex justify-content-center py-4">
<span class="fs-1 spinner-border spinner-border-lg align-middle me-2"></span> <span class="fs-1 spinner-border spinner-border-lg align-middle me-2"></span>
<span class="mt-3 fs-5">@(Text)</span> <span class="mt-3 fs-5">@(Text)</span>
</div> </div>
} }
}
@code @code
{ {
[Parameter] [Parameter]
public RenderFragment ChildContent { get; set; } public RenderFragment ChildContent { get; set; }
[Parameter]
public bool ShowAsCard { get; set; } = false;
[Parameter] [Parameter]
public Func<LazyLoader, Task> Load { get; set; } public Func<LazyLoader, Task> Load { get; set; }
[Parameter] [Parameter]
public string Text { get; set; } = ""; public string Text { get; set; } = "";
private bool loaded = false; private bool Loaded = false;
protected override async Task OnAfterRenderAsync(bool firstRender) protected override async Task OnAfterRenderAsync(bool firstRender)
{ {
if (firstRender) if (firstRender)
{ {
await Load.Invoke(this); await Load.Invoke(this);
loaded = true; Loaded = true;
await InvokeAsync(StateHasChanged); await InvokeAsync(StateHasChanged);
} }
} }
@ -41,10 +56,10 @@ else
public async Task Reload() public async Task Reload()
{ {
loaded = false; Loaded = false;
await InvokeAsync(StateHasChanged); await InvokeAsync(StateHasChanged);
await Load.Invoke(this); await Load.Invoke(this);
loaded = true; Loaded = true;
await InvokeAsync(StateHasChanged); await InvokeAsync(StateHasChanged);
} }
} }

View file

@ -0,0 +1,101 @@
@page "/services"
@using Moonlight.App.Services.ServiceManage
@using Moonlight.App.Database.Entities.Store
@using Moonlight.App.Services
@inject ServiceService ServiceService
@inject IdentityService IdentityService
@inject ConfigService ConfigService
<LazyLoader ShowAsCard="true" Load="Load">
<div class="row mb-5">
@foreach (var service in MyServices)
{
<div class="col-md-3 col-12">
<div class="card">
<div class="card-header">
<h3 class="card-title align-items-start flex-column">
<span class="card-label fw-bold text-dark fs-3">@(service.Nickname ?? $"Service {service.Id}")</span>
<span class="text-gray-400 mt-1 fw-semibold fs-6">@(service.Product.Name)</span>
</h3>
</div>
<div class="card-body fs-6">
<div class="d-flex flex-stack">
<div class="text-gray-700 fw-semibold me-2">Price</div>
<div class="d-flex align-items-senter">
<span class="fw-bold">@(ConfigService.Get().Store.Currency) @(service.Product.Price)</span>
</div>
</div>
<div class="d-flex flex-stack">
<div class="text-gray-700 fw-semibold me-2">Renew at</div>
<div class="d-flex align-items-senter">
<span class="fw-bold">@(Formatter.FormatDate(service.RenewAt))</span>
</div>
</div>
<div class="d-flex flex-stack">
<div class="text-gray-700 fw-semibold me-2">Created at</div>
<div class="d-flex align-items-senter">
<span class="fw-bold">@(Formatter.FormatDate(service.CreatedAt))</span>
</div>
</div>
</div>
<div class="card-footer p-3">
<div class="btn-group w-100 mb-3">
<button class="btn btn-primary w-50 me-3">Manage</button>
<button class="btn btn-secondary w-50">Manage shares</button>
</div>
<div class="btn-group w-100">
<button class="btn btn-warning w-50 me-3">Renew</button>
<button class="btn btn-danger w-50">Delete</button>
</div>
</div>
</div>
</div>
}
@foreach (var service in SharedServices)
{
<div class="col-md-3 col-12">
<div class="card">
<div class="card-header">
<h3 class="card-title align-items-start flex-column">
<span class="card-label fw-bold text-dark fs-3">@(service.Nickname ?? $"Service {service.Id}")</span>
<span class="text-gray-400 mt-1 fw-semibold fs-6">@(service.Product.Name)</span>
</h3>
</div>
<div class="card-body fs-6">
<div class="d-flex flex-stack">
<div class="text-gray-700 fw-semibold me-2">Shared by</div>
<div class="d-flex align-items-senter">
<span class="fw-bold">@(service.Owner.Username)</span>
</div>
</div>
<div class="d-flex flex-stack">
<div class="text-gray-700 fw-semibold me-2">Created at</div>
<div class="d-flex align-items-senter">
<span class="fw-bold">@(Formatter.FormatDate(service.CreatedAt))</span>
</div>
</div>
</div>
<div class="card-footer p-3 text-center">
<button class="btn btn-primary">Manage</button>
</div>
</div>
</div>
}
</div>
</LazyLoader>
@code
{
private Service[] MyServices;
private Service[] SharedServices;
private async Task Load(LazyLoader _)
{
MyServices = await ServiceService.Get(IdentityService.CurrentUser);
SharedServices = await ServiceService.GetShared(IdentityService.CurrentUser);
}
}

View file

@ -4,11 +4,14 @@
@using Moonlight.App.Services @using Moonlight.App.Services
@using Moonlight.App.Database.Entities.Store @using Moonlight.App.Database.Entities.Store
@using Moonlight.App.Repositories @using Moonlight.App.Repositories
@using Moonlight.App.Services.ServiceManage
@inject ConfigService ConfigService @inject ConfigService ConfigService
@inject StoreService StoreService @inject StoreService StoreService
@inject IdentityService IdentityService @inject IdentityService IdentityService
@inject AlertService AlertService @inject AlertService AlertService
@inject NavigationManager Navigation
@inject ServiceService ServiceService
@inject Repository<Product> ProductRepository @inject Repository<Product> ProductRepository
@inject Repository<Coupon> CouponRepository @inject Repository<Coupon> CouponRepository
@ -81,7 +84,7 @@ TODO: Add 404 here
<div class="d-flex flex-stack"> <div class="d-flex flex-stack">
<div class="text-gray-700 fw-semibold me-2">Today</div> <div class="text-gray-700 fw-semibold me-2">Today</div>
<div class="d-flex align-items-senter"> <div class="d-flex align-items-senter">
<span class="fw-bold">@(currency) @(defaultPrice)</span> <span class="fw-bold">@(currency) @(actualPrice)</span>
</div> </div>
</div> </div>
<div class="d-flex flex-stack"> <div class="d-flex flex-stack">
@ -136,7 +139,7 @@ TODO: Add 404 here
{ {
if (CanBeOrdered) if (CanBeOrdered)
{ {
<button class="btn btn-primary w-100">Order for @(currency) @(actualPrice)</button> <WButton OnClick="OnSubmit" Text="@($"Order for {currency} {actualPrice}")" CssClasses="btn btn-primary w-100"/>
} }
else else
{ {
@ -227,4 +230,22 @@ TODO: Add 404 here
await Revalidate(); await Revalidate();
} }
private async Task OnSubmit()
{
if (SelectedProduct == null) // Prevent processing null
return;
// Process the order with the selected values
var service = await StoreService
.Order
.Process(
IdentityService.CurrentUser,
SelectedProduct,
DurationMultiplicator,
SelectedCoupon
);
Navigation.NavigateTo("/service/" + service.Id);
}
} }