Implement Store Backend

MODEL CHANGE COMMANDS:
`ALTER TABLE Users ADD ActiveProfileCosmetic int(11) NOT NULL;`
`CREATE TABLE OwnedStoreItems (Id int(11) NOT NULL, UserId varchar(255) NOT NULL, StoreItemId int(11) NOT NULL, FOREIGN KEY (UserId) REFERENCES Users(Id));`
This commit is contained in:
Alan Moon 2025-07-09 14:51:18 -07:00
parent 0000f3bfe4
commit 34a6ac92d3
13 changed files with 213 additions and 0 deletions

View File

@ -0,0 +1,54 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using qtc_api.Services.StoreService;
using System.Security.Claims;
namespace qtc_api.Controllers
{
[Route("api/store")]
[ApiController]
public class StoreController : ControllerBase
{
private readonly StoreService _storeService;
private readonly IUserService _userService;
public StoreController(StoreService storeService, IUserService userService)
{
_storeService = storeService;
_userService = userService;
}
[HttpGet]
[Route("all-items")]
public ActionResult<ServiceResponse<List<StoreItem>>> GetAllItems()
{
return Ok(_storeService.GetStoreItems());
}
[HttpPost]
[Route("buy-item")]
[Authorize]
public async Task<ActionResult<ServiceResponse<OwnedStoreItem>>> BuyStoreItem(int id)
{
var identity = HttpContext.User.Identity as ClaimsIdentity;
if (identity != null)
{
IEnumerable<Claim> claims = identity.Claims;
var userId = claims.First().Value;
if (userId != null)
{
var user = await _userService.GetUserById(userId);
if(user != null && user.Success && user.Data != null)
{
var result = await _storeService.BuyStoreItem(user.Data.Id, id);
return Ok(result);
}
else return Ok(new ServiceResponse<OwnedStoreItem> { Success = false, Message = "User Not Found In Auth Header" });
}
else return Ok(new ServiceResponse<OwnedStoreItem> { Success = false, Message = "No UserId In Auth Header" });
}
else return Ok(new ServiceResponse<OwnedStoreItem> { Success = false, Message = "No Auth Header" });
}
}
}

View File

@ -1,5 +1,6 @@
using Microsoft.EntityFrameworkCore.Diagnostics;
using Microsoft.Extensions.Options;
using Org.BouncyCastle.Asn1.Nist;
namespace qtc_api.Data
{
@ -13,6 +14,7 @@ namespace qtc_api.Data
public DbSet<Room> Rooms { get; set; }
public DbSet<RefreshToken> ValidRefreshTokens { get; set; }
public DbSet<Contact> Contacts { get; set; }
public DbSet<OwnedStoreItem> OwnedStoreItems { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder options)
{
@ -40,6 +42,7 @@ namespace qtc_api.Data
builder.Entity<User>().HasMany(e => e.ContactsList);
builder.Entity<User>().HasMany(e => e.ContactsMade);
builder.Entity<User>().HasMany(e => e.OwnedStoreItems);
// Rooms (no relations)
@ -60,6 +63,12 @@ namespace qtc_api.Data
builder.Entity<Contact>().HasOne(e => e.User)
.WithMany(e => e.ContactsList)
.HasForeignKey(e => e.UserId);
// Purchased Store Items
builder.Entity<OwnedStoreItem>().HasOne(e => e.User)
.WithMany(e => e.OwnedStoreItems)
.HasForeignKey(e => e.UserId);
}
}
}

View File

@ -11,5 +11,6 @@
public DateTime CreatedAt { get; set; } = new DateTime();
public int Status { get; set; } = 0;
public int CurrencyAmount { get; set; } = 0;
public int ProfileCosmeticId { get; set; } = 0;
}
}

View File

@ -6,5 +6,6 @@
public string Username { get; set; } = string.Empty;
public string Bio { get; set; } = string.Empty;
public DateTime DateOfBirth { get; set; } = new DateTime();
public int ProfileCosmeticId { get; set; } = 0;
}
}

View File

@ -0,0 +1,8 @@
namespace qtc_api.Enums
{
public enum StoreItemType
{
ProfileCosmetic = 1,
ClientCosmetic = 2
}
}

View File

@ -0,0 +1,14 @@
using System.ComponentModel.DataAnnotations;
namespace qtc_api.Models
{
public class OwnedStoreItem
{
[Key]
public int Id { get; set; }
public string UserId { get; set; } = string.Empty;
public int StoreItemId { get; set; }
public virtual User? User { get; }
}
}

View File

@ -15,9 +15,11 @@
public int CurrencyAmount { get; set; } = 0;
public int StockAmount { get; set; } = 0;
public DateTime LastCurrencySpin { get; set; }
public int ActiveProfileCosmetic { get; set; } = 0;
public virtual IEnumerable<RefreshToken>? RefreshTokens { get; }
public virtual IEnumerable<Contact>? ContactsMade { get; }
public virtual IEnumerable<Contact>? ContactsList { get; }
public virtual IEnumerable<OwnedStoreItem>? OwnedStoreItems { get; }
}
}

View File

@ -3,6 +3,7 @@ global using Microsoft.EntityFrameworkCore;
global using Microsoft.AspNetCore.SignalR;
global using Microsoft.AspNetCore.Authorization;
global using qtc_api.Models;
global using qtc_api.Schema;
global using qtc_api.Data;
global using qtc_api.Dtos.User;
global using qtc_api.Dtos.Room;
@ -15,6 +16,7 @@ using qtc_api.Services.RoomService;
using qtc_api.Services.ContactService;
using qtc_api.Services.CurrencyGamesService;
using qtc_api.Services.GameRoomService;
using qtc_api.Services.StoreService;
var builder = WebApplication.CreateBuilder(args);
@ -57,6 +59,7 @@ builder.Services.AddScoped<IUserService, UserService>();
builder.Services.AddScoped<ITokenService, TokenService>();
builder.Services.AddScoped<IRoomService, RoomService>();
builder.Services.AddScoped<IContactService, ContactService>();
builder.Services.AddScoped<StoreService>();
builder.Services.AddSingleton<CurrencyGamesService>();
builder.Services.AddSingleton<GameRoomService>();

View File

@ -0,0 +1,11 @@
[
{
"Id": 1,
"Type": 1,
"Price": 100,
"Name": "Exmaple",
"Description": "Change Me!",
"AssetUrl": "https://cdn.alanmoon.net/qtc/cosmetics/test/test.gif",
"ThumbnailUrl": "https://cdn.alanmoon.net/qtc/cosmetics/test/thumbnail.jpg"
}
]

View File

@ -0,0 +1,23 @@
using qtc_api.Enums;
using System.Text.Json.Serialization;
namespace qtc_api.Schema
{
public class StoreItem
{
[JsonPropertyName("Id")]
public int Id { get; set; }
[JsonPropertyName("Type")]
public StoreItemType Type { get; set; }
[JsonPropertyName("Price")]
public int Price { get; set; }
[JsonPropertyName("Name")]
public string Name { get; set; } = string.Empty;
[JsonPropertyName("Description")]
public string Description { get; set; } = string.Empty;
[JsonPropertyName("AssetUrl")]
public string AssetUrl { get; set; } = string.Empty;
[JsonPropertyName("ThumbnailUrl")]
public string ThumbnailUrl { get; set; } = string.Empty;
}
}

View File

@ -0,0 +1,82 @@
using System.Text.Json;
namespace qtc_api.Services.StoreService
{
public class StoreService
{
public static List<StoreItem> StoreItems { get; set; } = [];
private readonly DataContext _ctx;
private readonly IUserService _userService;
public StoreService(DataContext ctx, IUserService userService)
{
_ctx = ctx;
_userService = userService;
StoreItem[]? storeItems = JsonSerializer.Deserialize<StoreItem[]>(File.ReadAllText("./Resources/store.json"));
if (storeItems != null && storeItems.Length > 0)
{
StoreItems = storeItems.ToList();
}
}
public ServiceResponse<List<StoreItem>> GetStoreItems()
{
return new ServiceResponse<List<StoreItem>> { Success = true, Data = StoreItems };
}
public ServiceResponse<OwnedStoreItem> GetBoughtStoreItemFromUser(string userId, int itemId)
{
// find item owned by user
var item = _ctx.OwnedStoreItems.FirstOrDefault(e => e.UserId == userId && e.StoreItemId == itemId);
if (item != null)
return new ServiceResponse<OwnedStoreItem> { Success = true, Data = item };
else return new ServiceResponse<OwnedStoreItem> { Success = false, Message = "Item Not Yet Purchased" };
}
public ServiceResponse<List<OwnedStoreItem>> GetBoughtStoreItemsFromUser(string userId)
{
// find items owned by user
var items = _ctx.OwnedStoreItems.Where(e => e.UserId == userId).ToList();
if (items != null && items.Count > 0)
return new ServiceResponse<List<OwnedStoreItem>> { Success = true, Data = items };
else return new ServiceResponse<List<OwnedStoreItem>> { Success = false, Message = "User Owns No Items" };
}
public async Task<ServiceResponse<OwnedStoreItem>> BuyStoreItem(string userId, int id)
{
// find item in store
var item = StoreItems.FirstOrDefault(e => e.Id == id);
if (item != null)
{
// deduct currency from user
var user = await _userService.GetUserById(userId);
if (user != null && user.Success && user.Data != null)
{
if (user.Data.CurrencyAmount >= item.Price)
{
// remove currency from user
await _userService.RemoveCurrencyFromUser(userId, item.Price);
// create owned item
OwnedStoreItem ownedStoreItem = new OwnedStoreItem
{
StoreItemId = item.Id,
UserId = userId
};
// add to table
_ctx.OwnedStoreItems.Add(ownedStoreItem);
await _ctx.SaveChangesAsync();
// return successful service response
return new ServiceResponse<OwnedStoreItem> { Success = true, Data = ownedStoreItem };
}
else return new ServiceResponse<OwnedStoreItem> { Success = false, Message = "Insufficient Currency" };
}
else return new ServiceResponse<OwnedStoreItem> { Success = false, Message = "User Not Found" };
}
else return new ServiceResponse<OwnedStoreItem> { Success = false, Message = "Item Not Found" };
}
}
}

View File

@ -174,6 +174,7 @@
dto.CreatedAt = user.CreatedAt;
dto.Status = user.Status;
dto.CurrencyAmount = user.CurrencyAmount;
dto.ProfileCosmeticId = user.ActiveProfileCosmetic;
serviceResponse.Success = true;
serviceResponse.Data = dto;
@ -219,6 +220,7 @@
infoDto.CreatedAt = dbUser.CreatedAt;
infoDto.Status = dbUser.Status;
infoDto.CurrencyAmount = dbUser.CurrencyAmount;
infoDto.ProfileCosmeticId = request.ProfileCosmeticId;
serviceResponse.Success = true;
serviceResponse.Data = infoDto;

View File

@ -31,6 +31,9 @@
</ItemGroup>
<ItemGroup>
<Content Update="Resources\store.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="ServerConfig.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>