From 6410846afc2fd52989b2c0d24b78623f2403bd87 Mon Sep 17 00:00:00 2001 From: Marcel Baumgartner Date: Thu, 19 Oct 2023 00:30:54 +0200 Subject: [PATCH] Implemented order ui and validation. Added coupon handling --- .../Database/Entities/Store/Transaction.cs | 8 + Moonlight/App/Database/Entities/User.cs | 9 +- ...31018203522_AddedUserStoreData.Designer.cs | 371 ++++++++++++++++ .../20231018203522_AddedUserStoreData.cs | 130 ++++++ ...231018204737_AddedTransactions.Designer.cs | 403 ++++++++++++++++++ .../20231018204737_AddedTransactions.cs | 46 ++ .../Migrations/DataContextModelSnapshot.cs | 66 ++- .../App/Services/Store/StoreOrderService.cs | 87 ++++ Moonlight/App/Services/Store/StoreService.cs | 1 + Moonlight/Program.cs | 1 + Moonlight/Shared/Views/Store/Order.razor | 230 ++++++++++ 11 files changed, 1349 insertions(+), 3 deletions(-) create mode 100644 Moonlight/App/Database/Entities/Store/Transaction.cs create mode 100644 Moonlight/App/Database/Migrations/20231018203522_AddedUserStoreData.Designer.cs create mode 100644 Moonlight/App/Database/Migrations/20231018203522_AddedUserStoreData.cs create mode 100644 Moonlight/App/Database/Migrations/20231018204737_AddedTransactions.Designer.cs create mode 100644 Moonlight/App/Database/Migrations/20231018204737_AddedTransactions.cs create mode 100644 Moonlight/App/Services/Store/StoreOrderService.cs create mode 100644 Moonlight/Shared/Views/Store/Order.razor diff --git a/Moonlight/App/Database/Entities/Store/Transaction.cs b/Moonlight/App/Database/Entities/Store/Transaction.cs new file mode 100644 index 00000000..6ed8d389 --- /dev/null +++ b/Moonlight/App/Database/Entities/Store/Transaction.cs @@ -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; } = ""; +} \ No newline at end of file diff --git a/Moonlight/App/Database/Entities/User.cs b/Moonlight/App/Database/Entities/User.cs index bd757116..78bedeb1 100644 --- a/Moonlight/App/Database/Entities/User.cs +++ b/Moonlight/App/Database/Entities/User.cs @@ -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 Transactions { get; set; } = new(); + + public List CouponUses { get; set; } = new(); + public List GiftCodeUses { get; set; } = new(); // Meta data public string Flags { get; set; } = ""; diff --git a/Moonlight/App/Database/Migrations/20231018203522_AddedUserStoreData.Designer.cs b/Moonlight/App/Database/Migrations/20231018203522_AddedUserStoreData.Designer.cs new file mode 100644 index 00000000..b584d5fd --- /dev/null +++ b/Moonlight/App/Database/Migrations/20231018203522_AddedUserStoreData.Designer.cs @@ -0,0 +1,371 @@ +// +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 + { + /// + 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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Description") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Categories"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Store.Coupon", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Amount") + .HasColumnType("INTEGER"); + + b.Property("Code") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Percent") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("Coupons"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Store.CouponUse", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CouponId") + .HasColumnType("INTEGER"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Amount") + .HasColumnType("INTEGER"); + + b.Property("Code") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("REAL"); + + b.HasKey("Id"); + + b.ToTable("GiftCodes"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Store.GiftCodeUse", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("GiftCodeId") + .HasColumnType("INTEGER"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CategoryId") + .HasColumnType("INTEGER"); + + b.Property("ConfigJson") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Description") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Duration") + .HasColumnType("INTEGER"); + + b.Property("MaxPerUser") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Price") + .HasColumnType("REAL"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Stock") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("CategoryId"); + + b.ToTable("Products"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Store.Service", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ConfigJsonOverride") + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Nickname") + .HasColumnType("TEXT"); + + b.Property("OwnerId") + .HasColumnType("INTEGER"); + + b.Property("ProductId") + .HasColumnType("INTEGER"); + + b.Property("RenewAt") + .HasColumnType("TEXT"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ServiceId") + .HasColumnType("INTEGER"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Avatar") + .HasColumnType("TEXT"); + + b.Property("Balance") + .HasColumnType("REAL"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Email") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Flags") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Password") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Permissions") + .HasColumnType("INTEGER"); + + b.Property("TokenValidTimestamp") + .HasColumnType("TEXT"); + + b.Property("TotpKey") + .HasColumnType("TEXT"); + + b.Property("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 + } + } +} diff --git a/Moonlight/App/Database/Migrations/20231018203522_AddedUserStoreData.cs b/Moonlight/App/Database/Migrations/20231018203522_AddedUserStoreData.cs new file mode 100644 index 00000000..13c6ea26 --- /dev/null +++ b/Moonlight/App/Database/Migrations/20231018203522_AddedUserStoreData.cs @@ -0,0 +1,130 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Moonlight.App.Database.Migrations +{ + /// + public partial class AddedUserStoreData : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_Products_Categories_CategoryId", + table: "Products"); + + migrationBuilder.AddColumn( + name: "Balance", + table: "Users", + type: "REAL", + nullable: false, + defaultValue: 0.0); + + migrationBuilder.AlterColumn( + name: "CategoryId", + table: "Products", + type: "INTEGER", + nullable: false, + defaultValue: 0, + oldClrType: typeof(int), + oldType: "INTEGER", + oldNullable: true); + + migrationBuilder.AddColumn( + name: "UserId", + table: "GiftCodeUses", + type: "INTEGER", + nullable: true); + + migrationBuilder.AddColumn( + 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); + } + + /// + 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( + 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"); + } + } +} diff --git a/Moonlight/App/Database/Migrations/20231018204737_AddedTransactions.Designer.cs b/Moonlight/App/Database/Migrations/20231018204737_AddedTransactions.Designer.cs new file mode 100644 index 00000000..4a54145b --- /dev/null +++ b/Moonlight/App/Database/Migrations/20231018204737_AddedTransactions.Designer.cs @@ -0,0 +1,403 @@ +// +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 + { + /// + 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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Description") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Categories"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Store.Coupon", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Amount") + .HasColumnType("INTEGER"); + + b.Property("Code") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Percent") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("Coupons"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Store.CouponUse", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CouponId") + .HasColumnType("INTEGER"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Amount") + .HasColumnType("INTEGER"); + + b.Property("Code") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("REAL"); + + b.HasKey("Id"); + + b.ToTable("GiftCodes"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Store.GiftCodeUse", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("GiftCodeId") + .HasColumnType("INTEGER"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CategoryId") + .HasColumnType("INTEGER"); + + b.Property("ConfigJson") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Description") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Duration") + .HasColumnType("INTEGER"); + + b.Property("MaxPerUser") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Price") + .HasColumnType("REAL"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Stock") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("CategoryId"); + + b.ToTable("Products"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Store.Service", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ConfigJsonOverride") + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Nickname") + .HasColumnType("TEXT"); + + b.Property("OwnerId") + .HasColumnType("INTEGER"); + + b.Property("ProductId") + .HasColumnType("INTEGER"); + + b.Property("RenewAt") + .HasColumnType("TEXT"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ServiceId") + .HasColumnType("INTEGER"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Price") + .HasColumnType("REAL"); + + b.Property("Text") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Transaction"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Avatar") + .HasColumnType("TEXT"); + + b.Property("Balance") + .HasColumnType("REAL"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Email") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Flags") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Password") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Permissions") + .HasColumnType("INTEGER"); + + b.Property("TokenValidTimestamp") + .HasColumnType("TEXT"); + + b.Property("TotpKey") + .HasColumnType("TEXT"); + + b.Property("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 + } + } +} diff --git a/Moonlight/App/Database/Migrations/20231018204737_AddedTransactions.cs b/Moonlight/App/Database/Migrations/20231018204737_AddedTransactions.cs new file mode 100644 index 00000000..b8581b52 --- /dev/null +++ b/Moonlight/App/Database/Migrations/20231018204737_AddedTransactions.cs @@ -0,0 +1,46 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Moonlight.App.Database.Migrations +{ + /// + public partial class AddedTransactions : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Transaction", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Price = table.Column(type: "REAL", nullable: false), + Text = table.Column(type: "TEXT", nullable: false), + UserId = table.Column(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"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Transaction"); + } + } +} diff --git a/Moonlight/App/Database/Migrations/DataContextModelSnapshot.cs b/Moonlight/App/Database/Migrations/DataContextModelSnapshot.cs index 8d3a9e08..6cfcd7f2 100644 --- a/Moonlight/App/Database/Migrations/DataContextModelSnapshot.cs +++ b/Moonlight/App/Database/Migrations/DataContextModelSnapshot.cs @@ -70,10 +70,15 @@ namespace Moonlight.App.Database.Migrations b.Property("CouponId") .HasColumnType("INTEGER"); + b.Property("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("GiftCodeId") .HasColumnType("INTEGER"); + b.Property("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("CategoryId") + b.Property("CategoryId") .HasColumnType("INTEGER"); b.Property("ConfigJson") @@ -221,6 +231,29 @@ namespace Moonlight.App.Database.Migrations b.ToTable("ServiceShares"); }); + modelBuilder.Entity("Moonlight.App.Database.Entities.Store.Transaction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Price") + .HasColumnType("REAL"); + + b.Property("Text") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Transaction"); + }); + modelBuilder.Entity("Moonlight.App.Database.Entities.User", b => { b.Property("Id") @@ -230,6 +263,9 @@ namespace Moonlight.App.Database.Migrations b.Property("Avatar") .HasColumnType("TEXT"); + b.Property("Balance") + .HasColumnType("REAL"); + b.Property("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 } } diff --git a/Moonlight/App/Services/Store/StoreOrderService.cs b/Moonlight/App/Services/Store/StoreOrderService.cs new file mode 100644 index 00000000..3d4e0e06 --- /dev/null +++ b/Moonlight/App/Services/Store/StoreOrderService.cs @@ -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>(); + var userRepo = scope.ServiceProvider.GetRequiredService>(); + var serviceRepo = scope.ServiceProvider.GetRequiredService>(); + var couponRepo = scope.ServiceProvider.GetRequiredService>(); + + // 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; + } +} \ No newline at end of file diff --git a/Moonlight/App/Services/Store/StoreService.cs b/Moonlight/App/Services/Store/StoreService.cs index 6c35c2ea..1cbf94cf 100644 --- a/Moonlight/App/Services/Store/StoreService.cs +++ b/Moonlight/App/Services/Store/StoreService.cs @@ -5,6 +5,7 @@ public class StoreService private readonly IServiceProvider ServiceProvider; public StoreAdminService Admin => ServiceProvider.GetRequiredService(); + public StoreOrderService Order => ServiceProvider.GetRequiredService(); public StoreService(IServiceProvider serviceProvider) { diff --git a/Moonlight/Program.cs b/Moonlight/Program.cs index 2b187181..f59a6499 100644 --- a/Moonlight/Program.cs +++ b/Moonlight/Program.cs @@ -43,6 +43,7 @@ builder.Services.AddScoped(); // Services / Store builder.Services.AddScoped(); builder.Services.AddScoped(); +builder.Services.AddScoped(); // Services / Users builder.Services.AddScoped(); diff --git a/Moonlight/Shared/Views/Store/Order.razor b/Moonlight/Shared/Views/Store/Order.razor new file mode 100644 index 00000000..4a802dbe --- /dev/null +++ b/Moonlight/Shared/Views/Store/Order.razor @@ -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 ProductRepository +@inject Repository CouponRepository + + + @if (SelectedProduct == null) + { + @* +TODO: Add 404 here +*@ + } + else + { +
+
+
+
+

@(SelectedProduct.Name)

+

+ @Formatter.FormatLineBreaks(SelectedProduct.Description) +

+
+
+ +
+
+

Apply coupon codes

+
+ + +
+
+
+
+
+
+
+
+

Summary

+
+ + @{ + 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; + } + +
+
+
Today
+
+ @(currency) @(defaultPrice) +
+
+
+
Renew
+
+ @(currency) @(defaultPrice) +
+
+
+
Duration
+
+ @(SelectedProduct.Duration * DurationMultiplicator) days +
+
+
+
+
Discount
+
+ @(SelectedCoupon?.Percent ?? 0)% +
+
+
+
Coupon
+
+ @(SelectedCoupon?.Code ?? "None") +
+
+
+
+
Total
+
+ @(currency) @(actualPrice) +
+
+
+ @if (!CanBeOrdered && !string.IsNullOrEmpty(ErrorMessage)) + { +
+ @ErrorMessage +
+ } +
+
+ +
+
+
+
+ } +
+ +@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(); + } +} \ No newline at end of file