Implemented order ui and validation. Added coupon handling

This commit is contained in:
Marcel Baumgartner 2023-10-19 00:30:54 +02:00
parent f7a16fd287
commit 6410846afc
11 changed files with 1349 additions and 3 deletions

View file

@ -0,0 +1,8 @@
namespace Moonlight.App.Database.Entities.Store;
public class Transaction
{
public int Id { get; set; }
public double Price { get; set; }
public string Text { get; set; } = "";
}

View file

@ -1,4 +1,6 @@
namespace Moonlight.App.Database.Entities;
using Moonlight.App.Database.Entities.Store;
namespace Moonlight.App.Database.Entities;
public class User
{
@ -10,6 +12,11 @@ public class User
public string? TotpKey { get; set; } = null;
// Store
public double Balance { get; set; }
public List<Transaction> Transactions { get; set; } = new();
public List<CouponUse> CouponUses { get; set; } = new();
public List<GiftCodeUse> GiftCodeUses { get; set; } = new();
// Meta data
public string Flags { get; set; } = "";

View file

@ -0,0 +1,371 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Moonlight.App.Database;
#nullable disable
namespace Moonlight.App.Database.Migrations
{
[DbContext(typeof(DataContext))]
[Migration("20231018203522_AddedUserStoreData")]
partial class AddedUserStoreData
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "7.0.2");
modelBuilder.Entity("Moonlight.App.Database.Entities.Store.Category", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("Description")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("Slug")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("Categories");
});
modelBuilder.Entity("Moonlight.App.Database.Entities.Store.Coupon", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("Amount")
.HasColumnType("INTEGER");
b.Property<string>("Code")
.IsRequired()
.HasColumnType("TEXT");
b.Property<int>("Percent")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.ToTable("Coupons");
});
modelBuilder.Entity("Moonlight.App.Database.Entities.Store.CouponUse", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("CouponId")
.HasColumnType("INTEGER");
b.Property<int?>("UserId")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("CouponId");
b.HasIndex("UserId");
b.ToTable("CouponUses");
});
modelBuilder.Entity("Moonlight.App.Database.Entities.Store.GiftCode", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("Amount")
.HasColumnType("INTEGER");
b.Property<string>("Code")
.IsRequired()
.HasColumnType("TEXT");
b.Property<double>("Value")
.HasColumnType("REAL");
b.HasKey("Id");
b.ToTable("GiftCodes");
});
modelBuilder.Entity("Moonlight.App.Database.Entities.Store.GiftCodeUse", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("GiftCodeId")
.HasColumnType("INTEGER");
b.Property<int?>("UserId")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("GiftCodeId");
b.HasIndex("UserId");
b.ToTable("GiftCodeUses");
});
modelBuilder.Entity("Moonlight.App.Database.Entities.Store.Product", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("CategoryId")
.HasColumnType("INTEGER");
b.Property<string>("ConfigJson")
.IsRequired()
.HasColumnType("TEXT");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT");
b.Property<string>("Description")
.IsRequired()
.HasColumnType("TEXT");
b.Property<int>("Duration")
.HasColumnType("INTEGER");
b.Property<int>("MaxPerUser")
.HasColumnType("INTEGER");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("TEXT");
b.Property<double>("Price")
.HasColumnType("REAL");
b.Property<string>("Slug")
.IsRequired()
.HasColumnType("TEXT");
b.Property<int>("Stock")
.HasColumnType("INTEGER");
b.Property<int>("Type")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("CategoryId");
b.ToTable("Products");
});
modelBuilder.Entity("Moonlight.App.Database.Entities.Store.Service", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("ConfigJsonOverride")
.HasColumnType("TEXT");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT");
b.Property<string>("Nickname")
.HasColumnType("TEXT");
b.Property<int>("OwnerId")
.HasColumnType("INTEGER");
b.Property<int>("ProductId")
.HasColumnType("INTEGER");
b.Property<DateTime>("RenewAt")
.HasColumnType("TEXT");
b.Property<bool>("Suspended")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("OwnerId");
b.HasIndex("ProductId");
b.ToTable("Services");
});
modelBuilder.Entity("Moonlight.App.Database.Entities.Store.ServiceShare", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int?>("ServiceId")
.HasColumnType("INTEGER");
b.Property<int>("UserId")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("ServiceId");
b.HasIndex("UserId");
b.ToTable("ServiceShares");
});
modelBuilder.Entity("Moonlight.App.Database.Entities.User", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("Avatar")
.HasColumnType("TEXT");
b.Property<double>("Balance")
.HasColumnType("REAL");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT");
b.Property<string>("Email")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("Flags")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("Password")
.IsRequired()
.HasColumnType("TEXT");
b.Property<int>("Permissions")
.HasColumnType("INTEGER");
b.Property<DateTime>("TokenValidTimestamp")
.HasColumnType("TEXT");
b.Property<string>("TotpKey")
.HasColumnType("TEXT");
b.Property<string>("Username")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("Users");
});
modelBuilder.Entity("Moonlight.App.Database.Entities.Store.CouponUse", b =>
{
b.HasOne("Moonlight.App.Database.Entities.Store.Coupon", "Coupon")
.WithMany()
.HasForeignKey("CouponId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Moonlight.App.Database.Entities.User", null)
.WithMany("CouponUses")
.HasForeignKey("UserId");
b.Navigation("Coupon");
});
modelBuilder.Entity("Moonlight.App.Database.Entities.Store.GiftCodeUse", b =>
{
b.HasOne("Moonlight.App.Database.Entities.Store.GiftCode", "GiftCode")
.WithMany()
.HasForeignKey("GiftCodeId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Moonlight.App.Database.Entities.User", null)
.WithMany("GiftCodeUses")
.HasForeignKey("UserId");
b.Navigation("GiftCode");
});
modelBuilder.Entity("Moonlight.App.Database.Entities.Store.Product", b =>
{
b.HasOne("Moonlight.App.Database.Entities.Store.Category", "Category")
.WithMany()
.HasForeignKey("CategoryId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Category");
});
modelBuilder.Entity("Moonlight.App.Database.Entities.Store.Service", b =>
{
b.HasOne("Moonlight.App.Database.Entities.User", "Owner")
.WithMany()
.HasForeignKey("OwnerId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Moonlight.App.Database.Entities.Store.Product", "Product")
.WithMany()
.HasForeignKey("ProductId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Owner");
b.Navigation("Product");
});
modelBuilder.Entity("Moonlight.App.Database.Entities.Store.ServiceShare", b =>
{
b.HasOne("Moonlight.App.Database.Entities.Store.Service", null)
.WithMany("Shares")
.HasForeignKey("ServiceId");
b.HasOne("Moonlight.App.Database.Entities.User", "User")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("User");
});
modelBuilder.Entity("Moonlight.App.Database.Entities.Store.Service", b =>
{
b.Navigation("Shares");
});
modelBuilder.Entity("Moonlight.App.Database.Entities.User", b =>
{
b.Navigation("CouponUses");
b.Navigation("GiftCodeUses");
});
#pragma warning restore 612, 618
}
}
}

View file

@ -0,0 +1,130 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Moonlight.App.Database.Migrations
{
/// <inheritdoc />
public partial class AddedUserStoreData : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_Products_Categories_CategoryId",
table: "Products");
migrationBuilder.AddColumn<double>(
name: "Balance",
table: "Users",
type: "REAL",
nullable: false,
defaultValue: 0.0);
migrationBuilder.AlterColumn<int>(
name: "CategoryId",
table: "Products",
type: "INTEGER",
nullable: false,
defaultValue: 0,
oldClrType: typeof(int),
oldType: "INTEGER",
oldNullable: true);
migrationBuilder.AddColumn<int>(
name: "UserId",
table: "GiftCodeUses",
type: "INTEGER",
nullable: true);
migrationBuilder.AddColumn<int>(
name: "UserId",
table: "CouponUses",
type: "INTEGER",
nullable: true);
migrationBuilder.CreateIndex(
name: "IX_GiftCodeUses_UserId",
table: "GiftCodeUses",
column: "UserId");
migrationBuilder.CreateIndex(
name: "IX_CouponUses_UserId",
table: "CouponUses",
column: "UserId");
migrationBuilder.AddForeignKey(
name: "FK_CouponUses_Users_UserId",
table: "CouponUses",
column: "UserId",
principalTable: "Users",
principalColumn: "Id");
migrationBuilder.AddForeignKey(
name: "FK_GiftCodeUses_Users_UserId",
table: "GiftCodeUses",
column: "UserId",
principalTable: "Users",
principalColumn: "Id");
migrationBuilder.AddForeignKey(
name: "FK_Products_Categories_CategoryId",
table: "Products",
column: "CategoryId",
principalTable: "Categories",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_CouponUses_Users_UserId",
table: "CouponUses");
migrationBuilder.DropForeignKey(
name: "FK_GiftCodeUses_Users_UserId",
table: "GiftCodeUses");
migrationBuilder.DropForeignKey(
name: "FK_Products_Categories_CategoryId",
table: "Products");
migrationBuilder.DropIndex(
name: "IX_GiftCodeUses_UserId",
table: "GiftCodeUses");
migrationBuilder.DropIndex(
name: "IX_CouponUses_UserId",
table: "CouponUses");
migrationBuilder.DropColumn(
name: "Balance",
table: "Users");
migrationBuilder.DropColumn(
name: "UserId",
table: "GiftCodeUses");
migrationBuilder.DropColumn(
name: "UserId",
table: "CouponUses");
migrationBuilder.AlterColumn<int>(
name: "CategoryId",
table: "Products",
type: "INTEGER",
nullable: true,
oldClrType: typeof(int),
oldType: "INTEGER");
migrationBuilder.AddForeignKey(
name: "FK_Products_Categories_CategoryId",
table: "Products",
column: "CategoryId",
principalTable: "Categories",
principalColumn: "Id");
}
}
}

View file

@ -0,0 +1,403 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Moonlight.App.Database;
#nullable disable
namespace Moonlight.App.Database.Migrations
{
[DbContext(typeof(DataContext))]
[Migration("20231018204737_AddedTransactions")]
partial class AddedTransactions
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "7.0.2");
modelBuilder.Entity("Moonlight.App.Database.Entities.Store.Category", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("Description")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("Slug")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("Categories");
});
modelBuilder.Entity("Moonlight.App.Database.Entities.Store.Coupon", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("Amount")
.HasColumnType("INTEGER");
b.Property<string>("Code")
.IsRequired()
.HasColumnType("TEXT");
b.Property<int>("Percent")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.ToTable("Coupons");
});
modelBuilder.Entity("Moonlight.App.Database.Entities.Store.CouponUse", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("CouponId")
.HasColumnType("INTEGER");
b.Property<int?>("UserId")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("CouponId");
b.HasIndex("UserId");
b.ToTable("CouponUses");
});
modelBuilder.Entity("Moonlight.App.Database.Entities.Store.GiftCode", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("Amount")
.HasColumnType("INTEGER");
b.Property<string>("Code")
.IsRequired()
.HasColumnType("TEXT");
b.Property<double>("Value")
.HasColumnType("REAL");
b.HasKey("Id");
b.ToTable("GiftCodes");
});
modelBuilder.Entity("Moonlight.App.Database.Entities.Store.GiftCodeUse", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("GiftCodeId")
.HasColumnType("INTEGER");
b.Property<int?>("UserId")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("GiftCodeId");
b.HasIndex("UserId");
b.ToTable("GiftCodeUses");
});
modelBuilder.Entity("Moonlight.App.Database.Entities.Store.Product", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("CategoryId")
.HasColumnType("INTEGER");
b.Property<string>("ConfigJson")
.IsRequired()
.HasColumnType("TEXT");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT");
b.Property<string>("Description")
.IsRequired()
.HasColumnType("TEXT");
b.Property<int>("Duration")
.HasColumnType("INTEGER");
b.Property<int>("MaxPerUser")
.HasColumnType("INTEGER");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("TEXT");
b.Property<double>("Price")
.HasColumnType("REAL");
b.Property<string>("Slug")
.IsRequired()
.HasColumnType("TEXT");
b.Property<int>("Stock")
.HasColumnType("INTEGER");
b.Property<int>("Type")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("CategoryId");
b.ToTable("Products");
});
modelBuilder.Entity("Moonlight.App.Database.Entities.Store.Service", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("ConfigJsonOverride")
.HasColumnType("TEXT");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT");
b.Property<string>("Nickname")
.HasColumnType("TEXT");
b.Property<int>("OwnerId")
.HasColumnType("INTEGER");
b.Property<int>("ProductId")
.HasColumnType("INTEGER");
b.Property<DateTime>("RenewAt")
.HasColumnType("TEXT");
b.Property<bool>("Suspended")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("OwnerId");
b.HasIndex("ProductId");
b.ToTable("Services");
});
modelBuilder.Entity("Moonlight.App.Database.Entities.Store.ServiceShare", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int?>("ServiceId")
.HasColumnType("INTEGER");
b.Property<int>("UserId")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("ServiceId");
b.HasIndex("UserId");
b.ToTable("ServiceShares");
});
modelBuilder.Entity("Moonlight.App.Database.Entities.Store.Transaction", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<double>("Price")
.HasColumnType("REAL");
b.Property<string>("Text")
.IsRequired()
.HasColumnType("TEXT");
b.Property<int?>("UserId")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("Transaction");
});
modelBuilder.Entity("Moonlight.App.Database.Entities.User", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("Avatar")
.HasColumnType("TEXT");
b.Property<double>("Balance")
.HasColumnType("REAL");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT");
b.Property<string>("Email")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("Flags")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("Password")
.IsRequired()
.HasColumnType("TEXT");
b.Property<int>("Permissions")
.HasColumnType("INTEGER");
b.Property<DateTime>("TokenValidTimestamp")
.HasColumnType("TEXT");
b.Property<string>("TotpKey")
.HasColumnType("TEXT");
b.Property<string>("Username")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("Users");
});
modelBuilder.Entity("Moonlight.App.Database.Entities.Store.CouponUse", b =>
{
b.HasOne("Moonlight.App.Database.Entities.Store.Coupon", "Coupon")
.WithMany()
.HasForeignKey("CouponId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Moonlight.App.Database.Entities.User", null)
.WithMany("CouponUses")
.HasForeignKey("UserId");
b.Navigation("Coupon");
});
modelBuilder.Entity("Moonlight.App.Database.Entities.Store.GiftCodeUse", b =>
{
b.HasOne("Moonlight.App.Database.Entities.Store.GiftCode", "GiftCode")
.WithMany()
.HasForeignKey("GiftCodeId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Moonlight.App.Database.Entities.User", null)
.WithMany("GiftCodeUses")
.HasForeignKey("UserId");
b.Navigation("GiftCode");
});
modelBuilder.Entity("Moonlight.App.Database.Entities.Store.Product", b =>
{
b.HasOne("Moonlight.App.Database.Entities.Store.Category", "Category")
.WithMany()
.HasForeignKey("CategoryId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Category");
});
modelBuilder.Entity("Moonlight.App.Database.Entities.Store.Service", b =>
{
b.HasOne("Moonlight.App.Database.Entities.User", "Owner")
.WithMany()
.HasForeignKey("OwnerId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Moonlight.App.Database.Entities.Store.Product", "Product")
.WithMany()
.HasForeignKey("ProductId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Owner");
b.Navigation("Product");
});
modelBuilder.Entity("Moonlight.App.Database.Entities.Store.ServiceShare", b =>
{
b.HasOne("Moonlight.App.Database.Entities.Store.Service", null)
.WithMany("Shares")
.HasForeignKey("ServiceId");
b.HasOne("Moonlight.App.Database.Entities.User", "User")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("User");
});
modelBuilder.Entity("Moonlight.App.Database.Entities.Store.Transaction", b =>
{
b.HasOne("Moonlight.App.Database.Entities.User", null)
.WithMany("Transactions")
.HasForeignKey("UserId");
});
modelBuilder.Entity("Moonlight.App.Database.Entities.Store.Service", b =>
{
b.Navigation("Shares");
});
modelBuilder.Entity("Moonlight.App.Database.Entities.User", b =>
{
b.Navigation("CouponUses");
b.Navigation("GiftCodeUses");
b.Navigation("Transactions");
});
#pragma warning restore 612, 618
}
}
}

View file

@ -0,0 +1,46 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Moonlight.App.Database.Migrations
{
/// <inheritdoc />
public partial class AddedTransactions : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "Transaction",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
Price = table.Column<double>(type: "REAL", nullable: false),
Text = table.Column<string>(type: "TEXT", nullable: false),
UserId = table.Column<int>(type: "INTEGER", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_Transaction", x => x.Id);
table.ForeignKey(
name: "FK_Transaction_Users_UserId",
column: x => x.UserId,
principalTable: "Users",
principalColumn: "Id");
});
migrationBuilder.CreateIndex(
name: "IX_Transaction_UserId",
table: "Transaction",
column: "UserId");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "Transaction");
}
}
}

View file

@ -70,10 +70,15 @@ namespace Moonlight.App.Database.Migrations
b.Property<int>("CouponId")
.HasColumnType("INTEGER");
b.Property<int?>("UserId")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("CouponId");
b.HasIndex("UserId");
b.ToTable("CouponUses");
});
@ -107,10 +112,15 @@ namespace Moonlight.App.Database.Migrations
b.Property<int>("GiftCodeId")
.HasColumnType("INTEGER");
b.Property<int?>("UserId")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("GiftCodeId");
b.HasIndex("UserId");
b.ToTable("GiftCodeUses");
});
@ -120,7 +130,7 @@ namespace Moonlight.App.Database.Migrations
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int?>("CategoryId")
b.Property<int>("CategoryId")
.HasColumnType("INTEGER");
b.Property<string>("ConfigJson")
@ -221,6 +231,29 @@ namespace Moonlight.App.Database.Migrations
b.ToTable("ServiceShares");
});
modelBuilder.Entity("Moonlight.App.Database.Entities.Store.Transaction", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<double>("Price")
.HasColumnType("REAL");
b.Property<string>("Text")
.IsRequired()
.HasColumnType("TEXT");
b.Property<int?>("UserId")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("Transaction");
});
modelBuilder.Entity("Moonlight.App.Database.Entities.User", b =>
{
b.Property<int>("Id")
@ -230,6 +263,9 @@ namespace Moonlight.App.Database.Migrations
b.Property<string>("Avatar")
.HasColumnType("TEXT");
b.Property<double>("Balance")
.HasColumnType("REAL");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT");
@ -271,6 +307,10 @@ namespace Moonlight.App.Database.Migrations
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Moonlight.App.Database.Entities.User", null)
.WithMany("CouponUses")
.HasForeignKey("UserId");
b.Navigation("Coupon");
});
@ -282,6 +322,10 @@ namespace Moonlight.App.Database.Migrations
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Moonlight.App.Database.Entities.User", null)
.WithMany("GiftCodeUses")
.HasForeignKey("UserId");
b.Navigation("GiftCode");
});
@ -289,7 +333,9 @@ namespace Moonlight.App.Database.Migrations
{
b.HasOne("Moonlight.App.Database.Entities.Store.Category", "Category")
.WithMany()
.HasForeignKey("CategoryId");
.HasForeignKey("CategoryId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Category");
});
@ -328,10 +374,26 @@ namespace Moonlight.App.Database.Migrations
b.Navigation("User");
});
modelBuilder.Entity("Moonlight.App.Database.Entities.Store.Transaction", b =>
{
b.HasOne("Moonlight.App.Database.Entities.User", null)
.WithMany("Transactions")
.HasForeignKey("UserId");
});
modelBuilder.Entity("Moonlight.App.Database.Entities.Store.Service", b =>
{
b.Navigation("Shares");
});
modelBuilder.Entity("Moonlight.App.Database.Entities.User", b =>
{
b.Navigation("CouponUses");
b.Navigation("GiftCodeUses");
b.Navigation("Transactions");
});
#pragma warning restore 612, 618
}
}

View file

@ -0,0 +1,87 @@
using Microsoft.EntityFrameworkCore;
using Moonlight.App.Database.Entities;
using Moonlight.App.Database.Entities.Store;
using Moonlight.App.Exceptions;
using Moonlight.App.Repositories;
namespace Moonlight.App.Services.Store;
public class StoreOrderService
{
private readonly IServiceScopeFactory ServiceScopeFactory;
public StoreOrderService(IServiceScopeFactory serviceScopeFactory)
{
ServiceScopeFactory = serviceScopeFactory;
}
public Task Validate(User u, Product p, int durationMultiplier, Coupon? c)
{
using var scope = ServiceScopeFactory.CreateScope();
var productRepo = scope.ServiceProvider.GetRequiredService<Repository<Product>>();
var userRepo = scope.ServiceProvider.GetRequiredService<Repository<User>>();
var serviceRepo = scope.ServiceProvider.GetRequiredService<Repository<Service>>();
var couponRepo = scope.ServiceProvider.GetRequiredService<Repository<Coupon>>();
// Ensure the values are safe and loaded by using the created scope to bypass the cache
var user = userRepo
.Get()
.Include(x => x.CouponUses)
.ThenInclude(x => x.Coupon)
.FirstOrDefault(x => x.Id == u.Id);
if (user == null)
throw new DisplayException("Unsafe value detected. Please reload the page to proceed");
var product = productRepo
.Get()
.FirstOrDefault(x => x.Id == p.Id);
if (product == null)
throw new DisplayException("Unsafe value detected. Please reload the page to proceed");
Coupon? coupon = c;
if (coupon != null) // Only check if the coupon actually has a value
{
coupon = couponRepo
.Get()
.FirstOrDefault(x => x.Id == coupon.Id);
if(coupon == null)
throw new DisplayException("Unsafe value detected. Please reload the page to proceed");
}
// Perform checks on selected order
if (coupon != null && user.CouponUses.Any(x => x.Coupon.Id == coupon.Id))
throw new DisplayException("Coupon already used");
if (coupon != null && coupon.Amount == 0)
throw new DisplayException("No coupon uses left");
var price = product.Price * durationMultiplier;
if (coupon != null)
price = Math.Round(price * coupon.Percent / 100, 2);
if (user.Balance < price)
throw new DisplayException("Order is too expensive");
var userServices = serviceRepo
.Get()
.Where(x => x.Product.Id == product.Id)
.Count(x => x.Owner.Id == user.Id);
if (userServices >= product.MaxPerUser)
throw new DisplayException("The limit of this product on your account has been reached");
if (product.Stock < 1)
throw new DisplayException("The product is out of stock");
return Task.CompletedTask;
}
}

View file

@ -5,6 +5,7 @@ public class StoreService
private readonly IServiceProvider ServiceProvider;
public StoreAdminService Admin => ServiceProvider.GetRequiredService<StoreAdminService>();
public StoreOrderService Order => ServiceProvider.GetRequiredService<StoreOrderService>();
public StoreService(IServiceProvider serviceProvider)
{

View file

@ -43,6 +43,7 @@ builder.Services.AddScoped<AlertService>();
// Services / Store
builder.Services.AddScoped<StoreService>();
builder.Services.AddScoped<StoreAdminService>();
builder.Services.AddScoped<StoreOrderService>();
// Services / Users
builder.Services.AddScoped<UserService>();

View file

@ -0,0 +1,230 @@
@page "/store/order/{slug}"
@using Moonlight.App.Services.Store
@using Moonlight.App.Services
@using Moonlight.App.Database.Entities.Store
@using Moonlight.App.Repositories
@inject ConfigService ConfigService
@inject StoreService StoreService
@inject IdentityService IdentityService
@inject AlertService AlertService
@inject Repository<Product> ProductRepository
@inject Repository<Coupon> CouponRepository
<LazyLoader Load="Load">
@if (SelectedProduct == null)
{
@*
TODO: Add 404 here
*@
}
else
{
<div class="row">
<div class="col-md-7 col-12">
<div class="card mb-5">
<div class="card-body">
<h2 class="fs-2x fw-bold mb-10">@(SelectedProduct.Name)</h2>
<p class="text-gray-400 fs-4 fw-semibold">
@Formatter.FormatLineBreaks(SelectedProduct.Description)
</p>
</div>
</div>
<div class="row mb-5">
<div class="col-md-4 col-12">
<a @onclick="() => SetDurationMultiplicator(1)" @onclick:preventDefault href="#" class="card card-body bg-hover-secondary text-center @(DurationMultiplicator == 1 ? "border border-info" : "")">
<h4 class="fw-bold mb-0 align-middle">@(SelectedProduct.Duration * 1) days</h4>
</a>
</div>
<div class="col-md-4 col-12">
<a @onclick="() => SetDurationMultiplicator(2)" @onclick:preventDefault href="#" class="card card-body bg-hover-secondary text-center @(DurationMultiplicator == 2 ? "border border-info" : "")">
<h4 class="fw-bold mb-0 align-middle">@(SelectedProduct.Duration * 2) days</h4>
</a>
</div>
<div class="col-md-4 col-12">
<a @onclick="() => SetDurationMultiplicator(3)" @onclick:preventDefault href="#" class="card card-body bg-hover-secondary text-center @(DurationMultiplicator == 3 ? "border border-info" : "")">
<h4 class="fw-bold mb-0 align-middle">@(SelectedProduct.Duration * 3) days</h4>
</a>
</div>
</div>
<div class="card mb-5">
<div class="card-body">
<h3 class="fs-1x fw-bold mb-10">Apply coupon codes</h3>
<div class="input-group">
<input @bind="CouponCode" class="form-control form-control-solid" placeholder="Coupon code..."/>
<button @onclick="ApplyCoupon" class="btn btn-primary">Apply</button>
</div>
</div>
</div>
</div>
<div class="col-md-1 col-12"></div>
<div class="col-md-3 col-12">
<div class="card">
<div class="card-header">
<h3 class="card-title">Summary</h3>
</div>
@{
var defaultPrice = SelectedProduct.Price * DurationMultiplicator;
double actualPrice;
if (SelectedCoupon == null)
actualPrice = defaultPrice;
else
actualPrice = Math.Round(defaultPrice * SelectedCoupon.Percent / 100, 2);
var currency = ConfigService.Get().Store.Currency;
}
<div class="card-body fs-5">
<div class="d-flex flex-stack">
<div class="text-gray-700 fw-semibold me-2">Today</div>
<div class="d-flex align-items-senter">
<span class="fw-bold">@(currency) @(defaultPrice)</span>
</div>
</div>
<div class="d-flex flex-stack">
<div class="text-gray-700 fw-semibold me-2">Renew</div>
<div class="d-flex align-items-senter">
<span class="fw-bold">@(currency) @(defaultPrice)</span>
</div>
</div>
<div class="d-flex flex-stack">
<div class="text-gray-700 fw-semibold me-2">Duration</div>
<div class="d-flex align-items-senter">
<span class="fw-bold">@(SelectedProduct.Duration * DurationMultiplicator) days</span>
</div>
</div>
<div class="separator my-4"></div>
<div class="d-flex flex-stack">
<div class="text-gray-700 fw-semibold me-2">Discount</div>
<div class="d-flex align-items-senter">
<span class="fw-bold">@(SelectedCoupon?.Percent ?? 0)%</span>
</div>
</div>
<div class="d-flex flex-stack">
<div class="text-gray-700 fw-semibold me-2">Coupon</div>
<div class="d-flex align-items-senter">
<span class="fw-bold">@(SelectedCoupon?.Code ?? "None")</span>
</div>
</div>
<div class="separator my-4"></div>
<div class="d-flex flex-stack">
<div class="text-gray-700 fw-semibold me-2">Total</div>
<div class="d-flex align-items-senter">
<span class="fw-bold">@(currency) @(actualPrice)</span>
</div>
</div>
<div class="mt-7">
@if (!CanBeOrdered && !string.IsNullOrEmpty(ErrorMessage))
{
<div class="alert alert-warning bg-warning text-white text-center">
@ErrorMessage
</div>
}
</div>
</div>
<div class="card-footer">
@if (IsValidating)
{
<button class="btn btn-primary w-100 disabled" disabled="">
<span class="spinner-border spinner-border-sm align-middle"></span>
</button>
}
else
{
if (CanBeOrdered)
{
<button class="btn btn-primary w-100">Order for @(currency) @(actualPrice)</button>
}
else
{
<button class="btn btn-primary w-100 disabled" disabled="">Order for @(currency) @(actualPrice)</button>
}
}
</div>
</div>
</div>
<div class="col-md-1 col-12"></div>
</div>
}
</LazyLoader>
@code
{
[Parameter]
public string Slug { get; set; }
private Product? SelectedProduct;
private Coupon? SelectedCoupon;
private int DurationMultiplicator = 1;
private string CouponCode = "";
private bool CanBeOrdered = false;
private bool IsValidating = false;
private string ErrorMessage = "";
private async Task SetDurationMultiplicator(int i)
{
DurationMultiplicator = i;
await Revalidate();
}
private async Task ApplyCoupon()
{
SelectedCoupon = CouponRepository
.Get()
.FirstOrDefault(x => x.Code == CouponCode);
CouponCode = "";
await InvokeAsync(StateHasChanged);
if (SelectedCoupon == null)
{
await AlertService.Error("", "Invalid coupon code entered");
return;
}
await Revalidate();
}
private Task Revalidate()
{
if (SelectedProduct == null) // Prevent validating null
return Task.CompletedTask;
IsValidating = true;
InvokeAsync(StateHasChanged);
Task.Run(async () =>
{
try
{
await StoreService.Order.Validate(IdentityService.CurrentUser, SelectedProduct, 1, SelectedCoupon);
CanBeOrdered = true;
}
catch (DisplayException e)
{
CanBeOrdered = false;
ErrorMessage = e.Message;
}
IsValidating = false;
await InvokeAsync(StateHasChanged);
});
return Task.CompletedTask;
}
private async Task Load(LazyLoader _)
{
SelectedProduct = ProductRepository
.Get()
.FirstOrDefault(x => x.Slug == Slug);
await Revalidate();
}
}