feat:Initial commit
This commit is contained in:
76
StoreService/Controllers/OrdersController.cs
Normal file
76
StoreService/Controllers/OrdersController.cs
Normal file
@@ -0,0 +1,76 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using StoreService.Models;
|
||||
using StoreService.Services;
|
||||
|
||||
namespace StoreService.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// Endpoints for order lifecycle: create, query, pay and redeem.
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
public class OrdersController : ControllerBase
|
||||
{
|
||||
#region Fields
|
||||
private readonly IOrderService _orderService;
|
||||
#endregion
|
||||
|
||||
#region Ctor
|
||||
public OrdersController(IOrderService orderService)
|
||||
{
|
||||
_orderService = orderService;
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Endpoints
|
||||
/// <summary>
|
||||
/// Creates a new order for specified user and store items. Prices are calculated with active discounts.
|
||||
/// </summary>
|
||||
[HttpPost]
|
||||
[ProducesResponseType(typeof(OrderDto), StatusCodes.Status201Created)]
|
||||
public async Task<IActionResult> Create([FromBody] CreateOrderRequest request, CancellationToken ct)
|
||||
{
|
||||
var order = await _orderService.CreateOrderAsync(request, ct);
|
||||
return CreatedAtAction(nameof(Get), new { id = order.Id }, order);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves an order by id.
|
||||
/// </summary>
|
||||
[HttpGet("{id:long}")]
|
||||
[ProducesResponseType(typeof(OrderDto), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> Get([FromRoute] long id, CancellationToken ct)
|
||||
{
|
||||
var order = await _orderService.GetOrderAsync(id, ct);
|
||||
return order == null ? NotFound() : Ok(order);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Pays (confirms) an order. Sets the paid date. Idempotent except it fails if already paid.
|
||||
/// </summary>
|
||||
[HttpPost("{id:long}/pay")]
|
||||
[ProducesResponseType(typeof(OrderDto), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
[ProducesResponseType(StatusCodes.Status409Conflict)]
|
||||
public async Task<IActionResult> Pay([FromRoute] long id, CancellationToken ct)
|
||||
{
|
||||
var order = await _orderService.PayOrderAsync(id, ct);
|
||||
return Ok(order);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Marks items as redeemed (granted to user inventory) after payment.
|
||||
/// </summary>
|
||||
[HttpPost("{id:long}/redeem")]
|
||||
[ProducesResponseType(typeof(OrderDto), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
[ProducesResponseType(StatusCodes.Status409Conflict)]
|
||||
public async Task<IActionResult> Redeem([FromRoute] long id, CancellationToken ct)
|
||||
{
|
||||
var order = await _orderService.RedeemOrderItemsAsync(id, ct);
|
||||
return Ok(order);
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
|
||||
127
StoreService/Database/ApplicationContext.cs
Normal file
127
StoreService/Database/ApplicationContext.cs
Normal file
@@ -0,0 +1,127 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using StoreService.Database.Entities;
|
||||
|
||||
namespace StoreService.Database;
|
||||
|
||||
/// <summary>
|
||||
/// Entity Framework Core database context for the Store microservice.
|
||||
/// Defines DbSets and configures entity relationships & constraints.
|
||||
/// </summary>
|
||||
public class ApplicationContext : DbContext
|
||||
{
|
||||
#region Ctor
|
||||
public ApplicationContext(DbContextOptions<ApplicationContext> options) : base(options)
|
||||
{
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region DbSets
|
||||
public DbSet<StoreCategory> StoreCategories => Set<StoreCategory>();
|
||||
public DbSet<StoreItem> StoreItems => Set<StoreItem>();
|
||||
public DbSet<StoreDiscount> StoreDiscounts => Set<StoreDiscount>();
|
||||
public DbSet<StoreDiscountItem> StoreDiscountItems => Set<StoreDiscountItem>();
|
||||
public DbSet<StoreOrder> StoreOrders => Set<StoreOrder>();
|
||||
public DbSet<StoreOrderItem> StoreOrderItems => Set<StoreOrderItem>();
|
||||
public DbSet<StoreOrderItemDiscount> StoreOrderItemDiscounts => Set<StoreOrderItemDiscount>();
|
||||
#endregion
|
||||
|
||||
#region ModelCreating
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
base.OnModelCreating(modelBuilder);
|
||||
|
||||
// store_category
|
||||
modelBuilder.Entity<StoreCategory>(b =>
|
||||
{
|
||||
b.ToTable("store_category");
|
||||
b.HasKey(x => x.Id);
|
||||
b.Property(x => x.Title).HasMaxLength(256).IsRequired();
|
||||
});
|
||||
|
||||
// store_item
|
||||
modelBuilder.Entity<StoreItem>(b =>
|
||||
{
|
||||
b.ToTable("store_item");
|
||||
b.HasKey(x => x.Id);
|
||||
b.Property(x => x.ManaBuyPrice).IsRequired();
|
||||
b.Property(x => x.ManaSellPrice).IsRequired();
|
||||
b.Property(x => x.InventoryLimit).IsRequired();
|
||||
b.Property(x => x.UnlimitedPurchase).HasDefaultValue(false);
|
||||
b.HasOne(x => x.Category)
|
||||
.WithMany(c => c.Items)
|
||||
.HasForeignKey(x => x.StoreCategoryId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
});
|
||||
|
||||
// store_discount
|
||||
modelBuilder.Entity<StoreDiscount>(b =>
|
||||
{
|
||||
b.ToTable("store_discount");
|
||||
b.HasKey(x => x.Id);
|
||||
b.Property(x => x.Percentage).HasPrecision(5, 2); // up to 100.00
|
||||
b.Property(x => x.FromDate).IsRequired();
|
||||
b.Property(x => x.UntilDate).IsRequired();
|
||||
b.Property(x => x.IsCanceled).HasDefaultValue(false);
|
||||
});
|
||||
|
||||
// store_discount_item (many-to-many manual mapping)
|
||||
modelBuilder.Entity<StoreDiscountItem>(b =>
|
||||
{
|
||||
b.ToTable("store_discount_item");
|
||||
b.HasKey(x => x.Id);
|
||||
b.HasOne(x => x.Discount)
|
||||
.WithMany(d => d.DiscountItems)
|
||||
.HasForeignKey(x => x.StoreDiscountId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
b.HasOne(x => x.StoreItem)
|
||||
.WithMany(i => i.DiscountItems)
|
||||
.HasForeignKey(x => x.StoreItemId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
b.HasIndex(x => new { x.StoreDiscountId, x.StoreItemId }).IsUnique();
|
||||
});
|
||||
|
||||
// store_order
|
||||
modelBuilder.Entity<StoreOrder>(b =>
|
||||
{
|
||||
b.ToTable("store_order");
|
||||
b.HasKey(x => x.Id);
|
||||
b.Property(x => x.UserId).IsRequired();
|
||||
b.Property(x => x.CostUpdateDate).IsRequired();
|
||||
b.Property(x => x.ItemsRedeemed).HasDefaultValue(false);
|
||||
});
|
||||
|
||||
// store_order_item
|
||||
modelBuilder.Entity<StoreOrderItem>(b =>
|
||||
{
|
||||
b.ToTable("store_order_item");
|
||||
b.HasKey(x => x.Id);
|
||||
b.Property(x => x.CalculatedPrice).IsRequired();
|
||||
b.HasOne(x => x.Order)
|
||||
.WithMany(o => o.OrderItems)
|
||||
.HasForeignKey(x => x.StoreOrderId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
b.HasOne(x => x.StoreItem)
|
||||
.WithMany(i => i.OrderItems)
|
||||
.HasForeignKey(x => x.StoreItemId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
});
|
||||
|
||||
// store_order_item_discount
|
||||
modelBuilder.Entity<StoreOrderItemDiscount>(b =>
|
||||
{
|
||||
b.ToTable("store_order_item_discount");
|
||||
b.HasKey(x => x.Id);
|
||||
b.HasOne(x => x.OrderItem)
|
||||
.WithMany(oi => oi.AppliedDiscounts)
|
||||
.HasForeignKey(x => x.StoreOrderItemId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
b.HasOne(x => x.Discount)
|
||||
.WithMany(d => d.OrderItemDiscounts)
|
||||
.HasForeignKey(x => x.StoreDiscountId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
b.HasIndex(x => new { x.StoreOrderItemId, x.StoreDiscountId }).IsUnique();
|
||||
});
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
|
||||
17
StoreService/Database/Entities/StoreCategory.cs
Normal file
17
StoreService/Database/Entities/StoreCategory.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
namespace StoreService.Database.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// Category grouping store items.
|
||||
/// </summary>
|
||||
public class StoreCategory
|
||||
{
|
||||
#region Properties
|
||||
public long Id { get; set; }
|
||||
public string Title { get; set; } = string.Empty;
|
||||
#endregion
|
||||
|
||||
#region Navigation
|
||||
public ICollection<StoreItem> Items { get; set; } = new List<StoreItem>();
|
||||
#endregion
|
||||
}
|
||||
|
||||
36
StoreService/Database/Entities/StoreDiscount.cs
Normal file
36
StoreService/Database/Entities/StoreDiscount.cs
Normal file
@@ -0,0 +1,36 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace StoreService.Database.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// Percentage discount that can apply to one or more store items within a time window.
|
||||
/// </summary>
|
||||
public class StoreDiscount
|
||||
{
|
||||
#region Properties
|
||||
public long Id { get; set; }
|
||||
public decimal Percentage { get; set; } // 0 - 100 (%)
|
||||
public string? Description { get; set; }
|
||||
public DateTime FromDate { get; set; }
|
||||
public DateTime UntilDate { get; set; }
|
||||
public bool IsCanceled { get; set; }
|
||||
#endregion
|
||||
|
||||
#region Navigation
|
||||
public ICollection<StoreDiscountItem> DiscountItems { get; set; } = new List<StoreDiscountItem>();
|
||||
public ICollection<StoreOrderItemDiscount> OrderItemDiscounts { get; set; } = new List<StoreOrderItemDiscount>();
|
||||
#endregion
|
||||
|
||||
#region Helpers
|
||||
/// <summary>
|
||||
/// Checks whether discount is active at provided moment (default: now) ignoring cancellation flag (if canceled returns false).
|
||||
/// </summary>
|
||||
public bool IsActive(DateTime? at = null)
|
||||
{
|
||||
if (IsCanceled) return false;
|
||||
var moment = at ?? DateTime.UtcNow;
|
||||
return moment >= FromDate && moment <= UntilDate;
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
|
||||
19
StoreService/Database/Entities/StoreDiscountItem.cs
Normal file
19
StoreService/Database/Entities/StoreDiscountItem.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
namespace StoreService.Database.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// Join entity linking discounts to store items.
|
||||
/// </summary>
|
||||
public class StoreDiscountItem
|
||||
{
|
||||
#region Properties
|
||||
public long Id { get; set; }
|
||||
public long StoreDiscountId { get; set; }
|
||||
public long StoreItemId { get; set; }
|
||||
#endregion
|
||||
|
||||
#region Navigation
|
||||
public StoreDiscount? Discount { get; set; }
|
||||
public StoreItem? StoreItem { get; set; }
|
||||
#endregion
|
||||
}
|
||||
|
||||
25
StoreService/Database/Entities/StoreItem.cs
Normal file
25
StoreService/Database/Entities/StoreItem.cs
Normal file
@@ -0,0 +1,25 @@
|
||||
namespace StoreService.Database.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// Store item with pricing and purchase rules.
|
||||
/// </summary>
|
||||
public class StoreItem
|
||||
{
|
||||
#region Properties
|
||||
public long Id { get; set; }
|
||||
public long ItemId { get; set; } // FK to external Item service (not modeled here)
|
||||
public long StoreCategoryId { get; set; } // FK to StoreCategory
|
||||
public long? RankId { get; set; } // Minimum rank required to buy (external system)
|
||||
public int ManaBuyPrice { get; set; }
|
||||
public int ManaSellPrice { get; set; }
|
||||
public bool UnlimitedPurchase { get; set; }
|
||||
public int InventoryLimit { get; set; }
|
||||
#endregion
|
||||
|
||||
#region Navigation
|
||||
public StoreCategory? Category { get; set; }
|
||||
public ICollection<StoreDiscountItem> DiscountItems { get; set; } = new List<StoreDiscountItem>();
|
||||
public ICollection<StoreOrderItem> OrderItems { get; set; } = new List<StoreOrderItem>();
|
||||
#endregion
|
||||
}
|
||||
|
||||
20
StoreService/Database/Entities/StoreOrder.cs
Normal file
20
StoreService/Database/Entities/StoreOrder.cs
Normal file
@@ -0,0 +1,20 @@
|
||||
namespace StoreService.Database.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a purchase order created by a user.
|
||||
/// </summary>
|
||||
public class StoreOrder
|
||||
{
|
||||
#region Properties
|
||||
public long Id { get; set; }
|
||||
public long UserId { get; set; }
|
||||
public DateTime CostUpdateDate { get; set; } = DateTime.UtcNow; // updated when prices recalculated
|
||||
public DateTime? PaidDate { get; set; } // when payment succeeded
|
||||
public bool ItemsRedeemed { get; set; } // becomes true once items granted to inventory
|
||||
#endregion
|
||||
|
||||
#region Navigation
|
||||
public ICollection<StoreOrderItem> OrderItems { get; set; } = new List<StoreOrderItem>();
|
||||
#endregion
|
||||
}
|
||||
|
||||
21
StoreService/Database/Entities/StoreOrderItem.cs
Normal file
21
StoreService/Database/Entities/StoreOrderItem.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
namespace StoreService.Database.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// Item line inside an order with captured calculated price for history.
|
||||
/// </summary>
|
||||
public class StoreOrderItem
|
||||
{
|
||||
#region Properties
|
||||
public long Id { get; set; }
|
||||
public long StoreOrderId { get; set; }
|
||||
public long StoreItemId { get; set; }
|
||||
public int CalculatedPrice { get; set; }
|
||||
#endregion
|
||||
|
||||
#region Navigation
|
||||
public StoreOrder? Order { get; set; }
|
||||
public StoreItem? StoreItem { get; set; }
|
||||
public ICollection<StoreOrderItemDiscount> AppliedDiscounts { get; set; } = new List<StoreOrderItemDiscount>();
|
||||
#endregion
|
||||
}
|
||||
|
||||
19
StoreService/Database/Entities/StoreOrderItemDiscount.cs
Normal file
19
StoreService/Database/Entities/StoreOrderItemDiscount.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
namespace StoreService.Database.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// Captures which discounts were applied to order items at purchase time.
|
||||
/// </summary>
|
||||
public class StoreOrderItemDiscount
|
||||
{
|
||||
#region Properties
|
||||
public long Id { get; set; }
|
||||
public long StoreOrderItemId { get; set; }
|
||||
public long? StoreDiscountId { get; set; } // can be null if discount later removed but kept for history
|
||||
#endregion
|
||||
|
||||
#region Navigation
|
||||
public StoreOrderItem? OrderItem { get; set; }
|
||||
public StoreDiscount? Discount { get; set; }
|
||||
#endregion
|
||||
}
|
||||
|
||||
21
StoreService/Extensions/DatabaseExtensions.cs
Normal file
21
StoreService/Extensions/DatabaseExtensions.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using StoreService.Database;
|
||||
|
||||
namespace StoreService.Extensions;
|
||||
|
||||
/// <summary>
|
||||
/// Database related DI registrations.
|
||||
/// </summary>
|
||||
public static class DatabaseExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Registers the EF Core DbContext (PostgreSQL).
|
||||
/// </summary>
|
||||
public static IServiceCollection AddDatabase(this IServiceCollection services, IConfiguration configuration)
|
||||
{
|
||||
services.AddDbContext<ApplicationContext>(options =>
|
||||
options.UseNpgsql(configuration.GetConnectionString("StoreDb")));
|
||||
return services;
|
||||
}
|
||||
}
|
||||
|
||||
16
StoreService/Extensions/DomainServicesExtensions.cs
Normal file
16
StoreService/Extensions/DomainServicesExtensions.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
using StoreService.Services;
|
||||
|
||||
namespace StoreService.Extensions;
|
||||
|
||||
/// <summary>
|
||||
/// Domain service layer DI registrations.
|
||||
/// </summary>
|
||||
public static class DomainServicesExtensions
|
||||
{
|
||||
public static IServiceCollection AddDomainServices(this IServiceCollection services)
|
||||
{
|
||||
services.AddScoped<IOrderService, OrderService>();
|
||||
return services;
|
||||
}
|
||||
}
|
||||
|
||||
24
StoreService/Extensions/LoggingExtensions.cs
Normal file
24
StoreService/Extensions/LoggingExtensions.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
using Serilog;
|
||||
|
||||
namespace StoreService.Extensions;
|
||||
|
||||
/// <summary>
|
||||
/// Logging related extensions (Serilog configuration).
|
||||
/// </summary>
|
||||
public static class LoggingExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds Serilog configuration for the host using appsettings.json (Serilog section).
|
||||
/// </summary>
|
||||
public static IHostBuilder AddSerilogLogging(this IHostBuilder hostBuilder)
|
||||
{
|
||||
hostBuilder.UseSerilog((ctx, services, cfg) =>
|
||||
{
|
||||
cfg.ReadFrom.Configuration(ctx.Configuration)
|
||||
.ReadFrom.Services(services)
|
||||
.Enrich.FromLogContext();
|
||||
});
|
||||
return hostBuilder;
|
||||
}
|
||||
}
|
||||
|
||||
18
StoreService/Extensions/RepositoryExtensions.cs
Normal file
18
StoreService/Extensions/RepositoryExtensions.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
using StoreService.Repositories;
|
||||
|
||||
namespace StoreService.Extensions;
|
||||
|
||||
/// <summary>
|
||||
/// Repository & UnitOfWork registrations.
|
||||
/// </summary>
|
||||
public static class RepositoryExtensions
|
||||
{
|
||||
public static IServiceCollection AddRepositories(this IServiceCollection services)
|
||||
{
|
||||
// Open generic registration for repositories
|
||||
services.AddScoped(typeof(IGenericRepository<>), typeof(GenericRepository<>));
|
||||
// Unit of work (will receive repositories via constructor injection)
|
||||
services.AddScoped<IUnitOfWork, UnitOfWork>();
|
||||
return services;
|
||||
}
|
||||
}
|
||||
79
StoreService/Middleware/ErrorHandlingMiddleware.cs
Normal file
79
StoreService/Middleware/ErrorHandlingMiddleware.cs
Normal file
@@ -0,0 +1,79 @@
|
||||
using System.Net;
|
||||
using System.Text.Json;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace StoreService.Middleware;
|
||||
|
||||
/// <summary>
|
||||
/// Global error handling middleware capturing unhandled exceptions and returning structured JSON errors.
|
||||
/// </summary>
|
||||
public class ErrorHandlingMiddleware
|
||||
{
|
||||
#region Fields
|
||||
private readonly RequestDelegate _next;
|
||||
private readonly ILogger _logger;
|
||||
#endregion
|
||||
|
||||
#region Ctor
|
||||
public ErrorHandlingMiddleware(RequestDelegate next, ILogger<ErrorHandlingMiddleware > logger)
|
||||
{
|
||||
_next = next;
|
||||
_logger = logger;
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Invoke
|
||||
public async Task InvokeAsync(HttpContext context)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _next(context);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
await HandleExceptionAsync(context, ex);
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Helpers
|
||||
private async Task HandleExceptionAsync(HttpContext context, Exception ex)
|
||||
{
|
||||
var status = ex switch
|
||||
{
|
||||
ValidationException => (int)HttpStatusCode.BadRequest,
|
||||
ArgumentException => (int)HttpStatusCode.BadRequest,
|
||||
KeyNotFoundException => (int)HttpStatusCode.NotFound,
|
||||
InvalidOperationException => StatusCodes.Status409Conflict,
|
||||
_ => (int)HttpStatusCode.InternalServerError
|
||||
};
|
||||
|
||||
var errorId = Guid.NewGuid().ToString();
|
||||
if (status >= 500)
|
||||
{
|
||||
_logger.LogError(ex, "Unhandled server exception {ErrorId}", errorId);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogWarning(ex, "Handled domain exception {ErrorId} -> {Status}", errorId, status);
|
||||
}
|
||||
|
||||
var env = context.RequestServices.GetRequiredService<IHostEnvironment>();
|
||||
|
||||
var problem = new
|
||||
{
|
||||
traceId = context.TraceIdentifier,
|
||||
errorId,
|
||||
status,
|
||||
message = ex.Message,
|
||||
details = env.IsDevelopment() ? ex.StackTrace : null
|
||||
};
|
||||
|
||||
context.Response.ContentType = "application/json";
|
||||
context.Response.StatusCode = status;
|
||||
await context.Response.WriteAsync(JsonSerializer.Serialize(problem));
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
|
||||
|
||||
19
StoreService/Models/CreateOrderRequest.cs
Normal file
19
StoreService/Models/CreateOrderRequest.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace StoreService.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Request body to create a new order consisting of store item identifiers.
|
||||
/// </summary>
|
||||
public class CreateOrderRequest
|
||||
{
|
||||
/// <summary>Identifier of the user creating the order.</summary>
|
||||
[Required]
|
||||
public long UserId { get; set; }
|
||||
|
||||
/// <summary>Collection of store item ids to include (unique). Duplicates are ignored.</summary>
|
||||
[Required]
|
||||
[MinLength(1)]
|
||||
public List<long> StoreItemIds { get; set; } = new();
|
||||
}
|
||||
|
||||
15
StoreService/Models/OrderDto.cs
Normal file
15
StoreService/Models/OrderDto.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
namespace StoreService.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Result DTO representing an order summary.
|
||||
/// </summary>
|
||||
public class OrderDto
|
||||
{
|
||||
public long Id { get; set; }
|
||||
public long UserId { get; set; }
|
||||
public DateTime CostUpdateDate { get; set; }
|
||||
public DateTime? PaidDate { get; set; }
|
||||
public bool ItemsRedeemed { get; set; }
|
||||
public List<OrderItemDto> Items { get; set; } = new();
|
||||
}
|
||||
|
||||
13
StoreService/Models/OrderItemDto.cs
Normal file
13
StoreService/Models/OrderItemDto.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
namespace StoreService.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Order line DTO.
|
||||
/// </summary>
|
||||
public class OrderItemDto
|
||||
{
|
||||
public long Id { get; set; }
|
||||
public long StoreItemId { get; set; }
|
||||
public int CalculatedPrice { get; set; }
|
||||
public List<long> AppliedDiscountIds { get; set; } = new();
|
||||
}
|
||||
|
||||
3
StoreService/Models/OrderModels.cs
Normal file
3
StoreService/Models/OrderModels.cs
Normal file
@@ -0,0 +1,3 @@
|
||||
// Legacy aggregated models file kept intentionally empty after splitting into individual files.
|
||||
// CreateOrderRequest, OrderDto, and OrderItemDto now reside in separate files.
|
||||
|
||||
63
StoreService/Program.cs
Normal file
63
StoreService/Program.cs
Normal file
@@ -0,0 +1,63 @@
|
||||
using System.Reflection;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Serilog;
|
||||
using StoreService.Database;
|
||||
using StoreService.Middleware;
|
||||
using StoreService.Repositories;
|
||||
using StoreService.Services;
|
||||
using StoreService.Extensions; // added for DI extension
|
||||
using Newtonsoft.Json; // added
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
#region Serilog
|
||||
// Use new logging extension
|
||||
builder.Host.AddSerilogLogging();
|
||||
#endregion
|
||||
|
||||
#region Services
|
||||
builder.Services
|
||||
.AddControllers()
|
||||
.AddNewtonsoftJson(options =>
|
||||
{
|
||||
options.SerializerSettings.NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore;
|
||||
options.SerializerSettings.Formatting = Formatting.None;
|
||||
options.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore;
|
||||
});
|
||||
|
||||
builder.Services.AddEndpointsApiExplorer();
|
||||
builder.Services.AddSwaggerGen(options =>
|
||||
{
|
||||
var xmlFilename = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
|
||||
var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFilename);
|
||||
if (File.Exists(xmlPath))
|
||||
{
|
||||
options.IncludeXmlComments(xmlPath, includeControllerXmlComments: true);
|
||||
}
|
||||
});
|
||||
|
||||
// Granular DI registration chain
|
||||
builder.Services
|
||||
.AddDatabase(builder.Configuration)
|
||||
.AddRepositories()
|
||||
.AddDomainServices();
|
||||
#endregion
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
#region Middleware
|
||||
app.UseSerilogRequestLogging();
|
||||
app.UseGlobalErrorHandling();
|
||||
|
||||
if (app.Environment.IsDevelopment())
|
||||
{
|
||||
app.UseSwagger();
|
||||
app.UseSwaggerUI();
|
||||
}
|
||||
|
||||
app.UseHttpsRedirection();
|
||||
app.UseAuthorization();
|
||||
app.MapControllers();
|
||||
#endregion
|
||||
|
||||
app.Run();
|
||||
23
StoreService/Properties/launchSettings.json
Normal file
23
StoreService/Properties/launchSettings.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/launchsettings.json",
|
||||
"profiles": {
|
||||
"http": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": false,
|
||||
"applicationUrl": "http://localhost:5141",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
},
|
||||
"https": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": false,
|
||||
"applicationUrl": "https://localhost:7238;http://localhost:5141",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
79
StoreService/Repositories/GenericRepository.cs
Normal file
79
StoreService/Repositories/GenericRepository.cs
Normal file
@@ -0,0 +1,79 @@
|
||||
using System.Linq.Expressions;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using StoreService.Database;
|
||||
|
||||
namespace StoreService.Repositories;
|
||||
|
||||
/// <summary>
|
||||
/// Generic repository implementation wrapping EF Core DbSet.
|
||||
/// </summary>
|
||||
/// <typeparam name="TEntity">Entity type.</typeparam>
|
||||
public class GenericRepository<TEntity> : IGenericRepository<TEntity> where TEntity : class
|
||||
{
|
||||
#region Fields
|
||||
private readonly ApplicationContext _context;
|
||||
private readonly DbSet<TEntity> _dbSet;
|
||||
#endregion
|
||||
|
||||
#region Ctor
|
||||
public GenericRepository(ApplicationContext context)
|
||||
{
|
||||
_context = context;
|
||||
_dbSet = context.Set<TEntity>();
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Query
|
||||
public virtual IQueryable<TEntity> Get(
|
||||
Expression<Func<TEntity, bool>>? filter = null,
|
||||
Func<IQueryable<TEntity>, IOrderedQueryable<TEntity>>? orderBy = null,
|
||||
params Expression<Func<TEntity, object>>[] includes)
|
||||
{
|
||||
IQueryable<TEntity> query = _dbSet;
|
||||
|
||||
if (filter != null)
|
||||
query = query.Where(filter);
|
||||
|
||||
foreach (var include in includes)
|
||||
query = query.Include(include);
|
||||
|
||||
return orderBy != null ? orderBy(query) : query;
|
||||
}
|
||||
|
||||
public virtual async Task<TEntity?> FirstOrDefaultAsync(Expression<Func<TEntity, bool>> predicate, params Expression<Func<TEntity, object>>[] includes)
|
||||
{
|
||||
IQueryable<TEntity> query = _dbSet;
|
||||
foreach (var include in includes)
|
||||
query = query.Include(include);
|
||||
return await query.FirstOrDefaultAsync(predicate);
|
||||
}
|
||||
|
||||
public virtual TEntity? GetById(object id) => _dbSet.Find(id);
|
||||
|
||||
public virtual async Task<TEntity?> GetByIdAsync(object id) => await _dbSet.FindAsync(id);
|
||||
#endregion
|
||||
|
||||
#region Mutations
|
||||
public virtual void Add(TEntity entity) => _dbSet.Add(entity);
|
||||
public virtual async Task AddAsync(TEntity entity) => await _dbSet.AddAsync(entity);
|
||||
public virtual void AddRange(IEnumerable<TEntity> entities) => _dbSet.AddRange(entities);
|
||||
|
||||
public virtual void Update(TEntity entity) => _dbSet.Update(entity);
|
||||
|
||||
public virtual void Delete(object id)
|
||||
{
|
||||
var entity = _dbSet.Find(id) ?? throw new KeyNotFoundException($"Entity {typeof(TEntity).Name} with id '{id}' not found");
|
||||
Delete(entity);
|
||||
}
|
||||
|
||||
public virtual void Delete(TEntity entity)
|
||||
{
|
||||
if (_context.Entry(entity).State == EntityState.Detached)
|
||||
_dbSet.Attach(entity);
|
||||
_dbSet.Remove(entity);
|
||||
}
|
||||
|
||||
public virtual void DeleteRange(IEnumerable<TEntity> entities) => _dbSet.RemoveRange(entities);
|
||||
#endregion
|
||||
}
|
||||
|
||||
36
StoreService/Repositories/IGenericRepository.cs
Normal file
36
StoreService/Repositories/IGenericRepository.cs
Normal file
@@ -0,0 +1,36 @@
|
||||
using System.Linq.Expressions;
|
||||
|
||||
namespace StoreService.Repositories;
|
||||
|
||||
/// <summary>
|
||||
/// Generic repository abstraction for simple CRUD & query operations.
|
||||
/// </summary>
|
||||
/// <typeparam name="TEntity">Entity type.</typeparam>
|
||||
public interface IGenericRepository<TEntity> where TEntity : class
|
||||
{
|
||||
#region Query
|
||||
IQueryable<TEntity> Get(
|
||||
Expression<Func<TEntity, bool>>? filter = null,
|
||||
Func<IQueryable<TEntity>, IOrderedQueryable<TEntity>>? orderBy = null,
|
||||
params Expression<Func<TEntity, object>>[] includes);
|
||||
|
||||
Task<TEntity?> FirstOrDefaultAsync(Expression<Func<TEntity, bool>> predicate,
|
||||
params Expression<Func<TEntity, object>>[] includes);
|
||||
|
||||
TEntity? GetById(object id);
|
||||
Task<TEntity?> GetByIdAsync(object id);
|
||||
#endregion
|
||||
|
||||
#region Mutations
|
||||
void Add(TEntity entity);
|
||||
Task AddAsync(TEntity entity);
|
||||
void AddRange(IEnumerable<TEntity> entities);
|
||||
|
||||
void Update(TEntity entity);
|
||||
|
||||
void Delete(object id);
|
||||
void Delete(TEntity entity);
|
||||
void DeleteRange(IEnumerable<TEntity> entities);
|
||||
#endregion
|
||||
}
|
||||
|
||||
32
StoreService/Repositories/IUnitOfWork.cs
Normal file
32
StoreService/Repositories/IUnitOfWork.cs
Normal file
@@ -0,0 +1,32 @@
|
||||
using Microsoft.EntityFrameworkCore.Storage;
|
||||
using StoreService.Database.Entities;
|
||||
|
||||
namespace StoreService.Repositories;
|
||||
|
||||
/// <summary>
|
||||
/// Unit of work pattern abstraction encapsulating repositories and transactions.
|
||||
/// </summary>
|
||||
public interface IUnitOfWork : IAsyncDisposable, IDisposable
|
||||
{
|
||||
#region Repositories
|
||||
IGenericRepository<StoreCategory> StoreCategories { get; }
|
||||
IGenericRepository<StoreItem> StoreItems { get; }
|
||||
IGenericRepository<StoreDiscount> StoreDiscounts { get; }
|
||||
IGenericRepository<StoreDiscountItem> StoreDiscountItems { get; }
|
||||
IGenericRepository<StoreOrder> StoreOrders { get; }
|
||||
IGenericRepository<StoreOrderItem> StoreOrderItems { get; }
|
||||
IGenericRepository<StoreOrderItemDiscount> StoreOrderItemDiscounts { get; }
|
||||
#endregion
|
||||
|
||||
#region Save
|
||||
bool SaveChanges();
|
||||
Task<bool> SaveChangesAsync(CancellationToken ct = default);
|
||||
#endregion
|
||||
|
||||
#region Transactions
|
||||
Task BeginTransactionAsync(CancellationToken ct = default);
|
||||
Task CommitTransactionAsync(CancellationToken ct = default);
|
||||
Task RollbackTransactionAsync(CancellationToken ct = default);
|
||||
#endregion
|
||||
}
|
||||
|
||||
99
StoreService/Repositories/UnitOfWork.cs
Normal file
99
StoreService/Repositories/UnitOfWork.cs
Normal file
@@ -0,0 +1,99 @@
|
||||
using Microsoft.EntityFrameworkCore.Storage;
|
||||
using StoreService.Database;
|
||||
using StoreService.Database.Entities;
|
||||
|
||||
namespace StoreService.Repositories;
|
||||
|
||||
/// <summary>
|
||||
/// Coordinates repository access and database transactions.
|
||||
/// </summary>
|
||||
public class UnitOfWork : IUnitOfWork
|
||||
{
|
||||
#region Fields
|
||||
private readonly ApplicationContext _context;
|
||||
private IDbContextTransaction? _transaction;
|
||||
#endregion
|
||||
|
||||
#region Ctor
|
||||
public UnitOfWork(
|
||||
ApplicationContext context,
|
||||
IGenericRepository<StoreCategory> storeCategories,
|
||||
IGenericRepository<StoreItem> storeItems,
|
||||
IGenericRepository<StoreDiscount> storeDiscounts,
|
||||
IGenericRepository<StoreDiscountItem> storeDiscountItems,
|
||||
IGenericRepository<StoreOrder> storeOrders,
|
||||
IGenericRepository<StoreOrderItem> storeOrderItems,
|
||||
IGenericRepository<StoreOrderItemDiscount> storeOrderItemDiscounts)
|
||||
{
|
||||
_context = context;
|
||||
StoreCategories = storeCategories;
|
||||
StoreItems = storeItems;
|
||||
StoreDiscounts = storeDiscounts;
|
||||
StoreDiscountItems = storeDiscountItems;
|
||||
StoreOrders = storeOrders;
|
||||
StoreOrderItems = storeOrderItems;
|
||||
StoreOrderItemDiscounts = storeOrderItemDiscounts;
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Repositories
|
||||
public IGenericRepository<StoreCategory> StoreCategories { get; }
|
||||
public IGenericRepository<StoreItem> StoreItems { get; }
|
||||
public IGenericRepository<StoreDiscount> StoreDiscounts { get; }
|
||||
public IGenericRepository<StoreDiscountItem> StoreDiscountItems { get; }
|
||||
public IGenericRepository<StoreOrder> StoreOrders { get; }
|
||||
public IGenericRepository<StoreOrderItem> StoreOrderItems { get; }
|
||||
public IGenericRepository<StoreOrderItemDiscount> StoreOrderItemDiscounts { get; }
|
||||
#endregion
|
||||
|
||||
#region Save
|
||||
public bool SaveChanges() => _context.SaveChanges() > 0;
|
||||
|
||||
public async Task<bool> SaveChangesAsync(CancellationToken ct = default) => (await _context.SaveChangesAsync(ct)) > 0;
|
||||
#endregion
|
||||
|
||||
#region Transactions
|
||||
public async Task BeginTransactionAsync(CancellationToken ct = default)
|
||||
{
|
||||
if (_transaction != null) throw new InvalidOperationException("Transaction already started");
|
||||
_transaction = await _context.Database.BeginTransactionAsync(ct);
|
||||
}
|
||||
|
||||
public async Task CommitTransactionAsync(CancellationToken ct = default)
|
||||
{
|
||||
if (_transaction == null) throw new InvalidOperationException("No transaction started");
|
||||
try
|
||||
{
|
||||
await _transaction.CommitAsync(ct);
|
||||
}
|
||||
catch
|
||||
{
|
||||
await RollbackTransactionAsync(ct);
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
{
|
||||
await _transaction.DisposeAsync();
|
||||
_transaction = null;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task RollbackTransactionAsync(CancellationToken ct = default)
|
||||
{
|
||||
if (_transaction == null) return;
|
||||
await _transaction.RollbackAsync(ct);
|
||||
await _transaction.DisposeAsync();
|
||||
_transaction = null;
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Dispose
|
||||
public void Dispose() => _transaction?.Dispose();
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
if (_transaction != null)
|
||||
await _transaction.DisposeAsync();
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
17
StoreService/Services/IOrderService.cs
Normal file
17
StoreService/Services/IOrderService.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
using StoreService.Models;
|
||||
|
||||
namespace StoreService.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Service responsible for order lifecycle (create, pay, redeem) including price calculation.
|
||||
/// </summary>
|
||||
public interface IOrderService
|
||||
{
|
||||
#region Methods
|
||||
Task<OrderDto> CreateOrderAsync(CreateOrderRequest request, CancellationToken ct = default);
|
||||
Task<OrderDto?> GetOrderAsync(long id, CancellationToken ct = default);
|
||||
Task<OrderDto> PayOrderAsync(long id, CancellationToken ct = default);
|
||||
Task<OrderDto> RedeemOrderItemsAsync(long id, CancellationToken ct = default);
|
||||
#endregion
|
||||
}
|
||||
|
||||
172
StoreService/Services/OrderService.cs
Normal file
172
StoreService/Services/OrderService.cs
Normal file
@@ -0,0 +1,172 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using StoreService.Database.Entities;
|
||||
using StoreService.Models;
|
||||
using StoreService.Repositories;
|
||||
|
||||
namespace StoreService.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Implements order creation, payment, redemption and price calculation logic.
|
||||
/// </summary>
|
||||
public class OrderService : IOrderService
|
||||
{
|
||||
#region Fields
|
||||
private readonly IUnitOfWork _uow;
|
||||
private readonly ILogger<OrderService> _logger;
|
||||
#endregion
|
||||
|
||||
#region Ctor
|
||||
public OrderService(IUnitOfWork uow, ILogger<OrderService> logger)
|
||||
{
|
||||
_uow = uow;
|
||||
_logger = logger;
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Public Methods
|
||||
/// <inheritdoc/>
|
||||
public async Task<OrderDto> CreateOrderAsync(CreateOrderRequest request, CancellationToken ct = default)
|
||||
{
|
||||
if (request.StoreItemIds.Count == 0)
|
||||
throw new ArgumentException("No store items specified", nameof(request.StoreItemIds));
|
||||
|
||||
// Ensure uniqueness
|
||||
var uniqueIds = request.StoreItemIds.Distinct().ToList();
|
||||
|
||||
// Load items with potential discount items and discounts for calculation.
|
||||
var items = await _uow.StoreItems
|
||||
.Get(i => uniqueIds.Contains(i.Id), includes: i => i.DiscountItems)
|
||||
.Include(i => i.DiscountItems) // ensure collection loaded
|
||||
.ToListAsync(ct);
|
||||
|
||||
if (items.Count != uniqueIds.Count)
|
||||
{
|
||||
var missing = uniqueIds.Except(items.Select(i => i.Id)).ToArray();
|
||||
throw new KeyNotFoundException($"Store items not found: {string.Join(", ", missing)}");
|
||||
}
|
||||
|
||||
// Preload discounts referenced by items for efficient calculation.
|
||||
var discountIds = items.SelectMany(i => i.DiscountItems.Select(di => di.StoreDiscountId)).Distinct().ToList();
|
||||
var discounts = await _uow.StoreDiscounts
|
||||
.Get(d => discountIds.Contains(d.Id))
|
||||
.ToListAsync(ct);
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
var order = new StoreOrder
|
||||
{
|
||||
UserId = request.UserId,
|
||||
CostUpdateDate = now,
|
||||
OrderItems = new List<StoreOrderItem>()
|
||||
};
|
||||
|
||||
foreach (var item in items)
|
||||
{
|
||||
var applicableDiscounts = discounts
|
||||
.Where(d => item.DiscountItems.Any(di => di.StoreDiscountId == d.Id) && d.IsActive(now))
|
||||
.ToList();
|
||||
|
||||
var calculatedPrice = CalculatePrice(item.ManaBuyPrice, applicableDiscounts);
|
||||
|
||||
var orderItem = new StoreOrderItem
|
||||
{
|
||||
StoreItemId = item.Id,
|
||||
CalculatedPrice = calculatedPrice,
|
||||
AppliedDiscounts = applicableDiscounts.Select(d => new StoreOrderItemDiscount
|
||||
{
|
||||
StoreDiscountId = d.Id
|
||||
}).ToList()
|
||||
};
|
||||
|
||||
order.OrderItems.Add(orderItem);
|
||||
}
|
||||
|
||||
await _uow.StoreOrders.AddAsync(order);
|
||||
await _uow.SaveChangesAsync(ct);
|
||||
|
||||
_logger.LogInformation("Created order {OrderId} for user {UserId}", order.Id, order.UserId);
|
||||
|
||||
return MapOrder(order);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task<OrderDto?> GetOrderAsync(long id, CancellationToken ct = default)
|
||||
{
|
||||
var order = await _uow.StoreOrders
|
||||
.Get(o => o.Id == id, includes: o => o.OrderItems)
|
||||
.Include(o => o.OrderItems)
|
||||
.ThenInclude(oi => oi.AppliedDiscounts)
|
||||
.FirstOrDefaultAsync(ct);
|
||||
return order == null ? null : MapOrder(order);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task<OrderDto> PayOrderAsync(long id, CancellationToken ct = default)
|
||||
{
|
||||
var order = await _uow.StoreOrders
|
||||
.Get(o => o.Id == id, includes: o => o.OrderItems)
|
||||
.FirstOrDefaultAsync(ct) ?? throw new KeyNotFoundException($"Order {id} not found");
|
||||
|
||||
if (order.PaidDate != null)
|
||||
throw new InvalidOperationException("Order already paid");
|
||||
|
||||
order.PaidDate = DateTime.UtcNow;
|
||||
await _uow.SaveChangesAsync(ct);
|
||||
_logger.Information("Order {OrderId} paid", id);
|
||||
return await GetOrderRequiredAsync(id, ct);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task<OrderDto> RedeemOrderItemsAsync(long id, CancellationToken ct = default)
|
||||
{
|
||||
var order = await _uow.StoreOrders.Get(o => o.Id == id).FirstOrDefaultAsync(ct) ??
|
||||
throw new KeyNotFoundException($"Order {id} not found");
|
||||
|
||||
if (order.PaidDate == null)
|
||||
throw new InvalidOperationException("Order not paid yet");
|
||||
if (order.ItemsRedeemed)
|
||||
throw new InvalidOperationException("Order items already redeemed");
|
||||
|
||||
// TODO: integrate with external inventory service. For now we just toggle flag.
|
||||
order.ItemsRedeemed = true;
|
||||
await _uow.SaveChangesAsync(ct);
|
||||
_logger.Information("Order {OrderId} items redeemed", id);
|
||||
return await GetOrderRequiredAsync(id, ct);
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Helpers
|
||||
private int CalculatePrice(int basePrice, IEnumerable<StoreDiscount> discounts)
|
||||
{
|
||||
// Assumption: sum discount percentages, cap at 90% to avoid free unless explicitly 100%. Documented decision.
|
||||
var totalPercent = discounts.Sum(d => d.Percentage);
|
||||
if (totalPercent > 100m) totalPercent = 100m; // absolute cap
|
||||
var discounted = basePrice - (int)Math.Floor(basePrice * (decimal)totalPercent / 100m);
|
||||
return Math.Max(0, discounted);
|
||||
}
|
||||
|
||||
private async Task<OrderDto> GetOrderRequiredAsync(long id, CancellationToken ct)
|
||||
{
|
||||
var dto = await GetOrderAsync(id, ct);
|
||||
if (dto == null) throw new KeyNotFoundException($"Order {id} not found after update");
|
||||
return dto;
|
||||
}
|
||||
|
||||
private static OrderDto MapOrder(StoreOrder order) => new()
|
||||
{
|
||||
Id = order.Id,
|
||||
UserId = order.UserId,
|
||||
CostUpdateDate = order.CostUpdateDate,
|
||||
PaidDate = order.PaidDate,
|
||||
ItemsRedeemed = order.ItemsRedeemed,
|
||||
Items = order.OrderItems.Select(oi => new OrderItemDto
|
||||
{
|
||||
Id = oi.Id,
|
||||
StoreItemId = oi.StoreItemId,
|
||||
CalculatedPrice = oi.CalculatedPrice,
|
||||
AppliedDiscountIds = oi.AppliedDiscounts.Select(ad => ad.StoreDiscountId ?? 0).ToList()
|
||||
}).ToList()
|
||||
};
|
||||
#endregion
|
||||
}
|
||||
|
||||
42
StoreService/StoreService.csproj
Normal file
42
StoreService/StoreService.csproj
Normal file
@@ -0,0 +1,42 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<NoWarn>1591</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.9" />
|
||||
<!-- EF Core -->
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.0" PrivateAssets="All" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.0" />
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.0" />
|
||||
<!-- Serilog -->
|
||||
<PackageReference Include="Serilog.AspNetCore" Version="8.0.1" />
|
||||
<PackageReference Include="Serilog.Settings.Configuration" Version="8.0.0" />
|
||||
<PackageReference Include="Serilog.Sinks.Console" Version="5.0.1" />
|
||||
<PackageReference Include="Serilog.Sinks.File" Version="5.0.0" />
|
||||
<PackageReference Include="Serilog.Sinks.Debug" Version="2.0.0" />
|
||||
<PackageReference Include="Serilog.Sinks.OpenSearch" Version="1.3.0" />
|
||||
<PackageReference Include="Serilog.Enrichers.Environment" Version="3.0.1" />
|
||||
<PackageReference Include="Serilog.Enrichers.Process" Version="3.0.0" />
|
||||
<PackageReference Include="Serilog.Enrichers.Thread" Version="3.1.0" />
|
||||
<!-- Swagger -->
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.6.2" />
|
||||
<!-- Newtonsoft Json -->
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="9.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="Database/Entities/" />
|
||||
<Folder Include="Repositories/" />
|
||||
<Folder Include="Middleware/" />
|
||||
<Folder Include="Services/" />
|
||||
<Folder Include="Models/" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
6
StoreService/StoreService.http
Normal file
6
StoreService/StoreService.http
Normal file
@@ -0,0 +1,6 @@
|
||||
@StoreService_HostAddress = http://localhost:5141
|
||||
|
||||
GET {{StoreService_HostAddress}}/weatherforecast/
|
||||
Accept: application/json
|
||||
|
||||
###
|
||||
8
StoreService/appsettings.Development.json
Normal file
8
StoreService/appsettings.Development.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
}
|
||||
}
|
||||
47
StoreService/appsettings.json
Normal file
47
StoreService/appsettings.json
Normal file
@@ -0,0 +1,47 @@
|
||||
{
|
||||
"ConnectionStrings": {
|
||||
"StoreDb": "Host=localhost;Port=5432;Database=store_db;Username=store_user;Password=store_password"
|
||||
},
|
||||
"Serilog": {
|
||||
"Using": [ "Serilog.Sinks.Console", "Serilog.Sinks.File", "Serilog.Sinks.Debug", "Serilog.Sinks.OpenSearch" ],
|
||||
"MinimumLevel": {
|
||||
"Default": "Information",
|
||||
"Override": {
|
||||
"Microsoft": "Warning",
|
||||
"Microsoft.Hosting.Lifetime": "Information",
|
||||
"System": "Warning"
|
||||
}
|
||||
},
|
||||
"Enrich": [ "FromLogContext", "WithThreadId", "WithEnvironmentName", "WithProcessId" ],
|
||||
"WriteTo": [
|
||||
{ "Name": "Console" },
|
||||
{ "Name": "Debug" },
|
||||
{
|
||||
"Name": "File",
|
||||
"Args": {
|
||||
"path": "Logs/log-.txt",
|
||||
"rollingInterval": "Day",
|
||||
"retainedFileCountLimit": 14,
|
||||
"shared": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"Name": "OpenSearch",
|
||||
"Args": {
|
||||
"nodeUris": "http://localhost:9200",
|
||||
"indexFormat": "store-service-logs-{0:yyyy.MM.dd}",
|
||||
"autoRegisterTemplate": true,
|
||||
"numberOfShards": 1,
|
||||
"numberOfReplicas": 0
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*"
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
// <autogenerated />
|
||||
using System;
|
||||
using System.Reflection;
|
||||
[assembly: global::System.Runtime.Versioning.TargetFrameworkAttribute(".NETCoreApp,Version=v9.0", FrameworkDisplayName = ".NET 9.0")]
|
||||
22
StoreService/obj/Debug/net9.0/StoreService.AssemblyInfo.cs
Normal file
22
StoreService/obj/Debug/net9.0/StoreService.AssemblyInfo.cs
Normal file
@@ -0,0 +1,22 @@
|
||||
//------------------------------------------------------------------------------
|
||||
// <auto-generated>
|
||||
// This code was generated by a tool.
|
||||
//
|
||||
// Changes to this file may cause incorrect behavior and will be lost if
|
||||
// the code is regenerated.
|
||||
// </auto-generated>
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
using System;
|
||||
using System.Reflection;
|
||||
|
||||
[assembly: System.Reflection.AssemblyCompanyAttribute("StoreService")]
|
||||
[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
|
||||
[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
|
||||
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0")]
|
||||
[assembly: System.Reflection.AssemblyProductAttribute("StoreService")]
|
||||
[assembly: System.Reflection.AssemblyTitleAttribute("StoreService")]
|
||||
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]
|
||||
|
||||
// Generated by the MSBuild WriteCodeFragment class.
|
||||
|
||||
17
StoreService/obj/Debug/net9.0/StoreService.GlobalUsings.g.cs
Normal file
17
StoreService/obj/Debug/net9.0/StoreService.GlobalUsings.g.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
// <auto-generated/>
|
||||
global using global::Microsoft.AspNetCore.Builder;
|
||||
global using global::Microsoft.AspNetCore.Hosting;
|
||||
global using global::Microsoft.AspNetCore.Http;
|
||||
global using global::Microsoft.AspNetCore.Routing;
|
||||
global using global::Microsoft.Extensions.Configuration;
|
||||
global using global::Microsoft.Extensions.DependencyInjection;
|
||||
global using global::Microsoft.Extensions.Hosting;
|
||||
global using global::Microsoft.Extensions.Logging;
|
||||
global using global::System;
|
||||
global using global::System.Collections.Generic;
|
||||
global using global::System.IO;
|
||||
global using global::System.Linq;
|
||||
global using global::System.Net.Http;
|
||||
global using global::System.Net.Http.Json;
|
||||
global using global::System.Threading;
|
||||
global using global::System.Threading.Tasks;
|
||||
Reference in New Issue
Block a user