Added Order API

This commit is contained in:
Charles Showalter 2022-05-24 15:35:03 -07:00
parent 2bca44ebe5
commit 344ecb8762
31 changed files with 1115 additions and 7 deletions

View File

@ -0,0 +1,56 @@
using System.Security.Claims;
using API.Dtos;
using API.Errors;
using API.Extensions;
using AutoMapper;
using Core.Entities.OrderAggregate;
using Core.Interfaces;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace API.Controllers
{
[Authorize]
public class OrdersController : BaseApiController
{
private readonly IOrderService _orderService;
private readonly IMapper _mapper;
public OrdersController(IOrderService orderService, IMapper mapper)
{
_mapper = mapper;
_orderService = orderService;
}
[HttpPost]
public async Task<ActionResult<Order>> CreateOrder(OrderDto orderDto){
var email = HttpContext.User.RetrieveEmailFromPrincipal();
var address = _mapper.Map<AddressDto, Address>(orderDto.ShipToAddress);
var order = await _orderService.CreateOrderAsync(email, orderDto.DeliveryMethodId, orderDto.BasketId, address);
if(order == null) return BadRequest(new ApiResponse(400, "Problem creating order"));
return Ok(order);
}
[HttpGet]
public async Task<ActionResult<IReadOnlyList<OrderDto>>> GetOrdersForUser()
{
var email = HttpContext.User.RetrieveEmailFromPrincipal();
var orders = await _orderService.GetOrdersForUserAsync(email);
return Ok(_mapper.Map<IReadOnlyList<Order>, IReadOnlyList<OrderToReturnDto>>(orders));
}
[HttpGet("{id}")]
public async Task<ActionResult<OrderToReturnDto>> GetOrderByIdForUser(int id)
{
var email = HttpContext.User.RetrieveEmailFromPrincipal();
var order = await _orderService.GetOrderByIdAsync(id, email);
if(order == null) return NotFound(new ApiResponse(404));
return _mapper.Map<Order, OrderToReturnDto>(order);
}
[HttpGet("deliveryMethods")]
public async Task<ActionResult<IReadOnlyList<DeliveryMethod>>> GetDeliveryMethod()
{
return Ok(await _orderService.GetDeliveryMethodsAsync());
}
}
}

14
API/Dtos/OrderDto.cs Normal file
View File

@ -0,0 +1,14 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace API.Dtos
{
public class OrderDto
{
public string BasketId { get; set; }
public int DeliveryMethodId { get; set; }
public AddressDto ShipToAddress { get; set; }
}
}

16
API/Dtos/OrderItemDto.cs Normal file
View File

@ -0,0 +1,16 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace API.Dtos
{
public class OrderItemDto
{
public int ProductId { get; set; }
public string ProductName { get; set; }
public string PictureUrl { get; set; }
public decimal Price { get; set; }
public int Quantity { get; set; }
}
}

View File

@ -0,0 +1,22 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Core.Entities.OrderAggregate;
namespace API.Dtos
{
public class OrderToReturnDto
{
public int Id { get; set; }
public string BuyerEmail { get; set; }
public DateTimeOffset OrderDate { get; set; }
public Address ShipToAddress { get; set; }
public string DeliveryMethod { get; set; }
public decimal ShippingPrice { get; set; }
public IReadOnlyList<OrderItemDto> OrderItems { get; set; }
public decimal Subtotal { get; set; }
public decimal Total { get; set; }
public string Status { get; set; }
}
}

View File

@ -13,6 +13,8 @@ namespace API.Extensions
services.AddScoped<ITokenService, TokenService>(); services.AddScoped<ITokenService, TokenService>();
services.AddScoped<iProductRepository, ProductRepository>(); services.AddScoped<iProductRepository, ProductRepository>();
services.AddScoped<IBasketRepository, BasketRepository>(); services.AddScoped<IBasketRepository, BasketRepository>();
services.AddScoped<IOrderService, OrderService>();
services.AddScoped<IUnitOfWork, UnitOfWork>();
services.AddScoped(typeof(IGenericRepository<>), (typeof(GenericRepository<>))); services.AddScoped(typeof(IGenericRepository<>), (typeof(GenericRepository<>)));
services.Configure<ApiBehaviorOptions>(options => services.Configure<ApiBehaviorOptions>(options =>
options.InvalidModelStateResponseFactory = actionContext => options.InvalidModelStateResponseFactory = actionContext =>

View File

@ -0,0 +1,15 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
namespace API.Extensions
{
public static class ClaimsPrincipalExtentensions
{
public static string RetrieveEmailFromPrincipal(this ClaimsPrincipal user){
return user.FindFirstValue(ClaimTypes.Email);
}
}
}

View File

@ -2,6 +2,7 @@ using API.Dtos;
using AutoMapper; using AutoMapper;
using Core.Entities; using Core.Entities;
using Core.Entities.Identity; using Core.Entities.Identity;
using Core.Entities.OrderAggregate;
namespace API.Helpers namespace API.Helpers
{ {
@ -10,13 +11,22 @@ namespace API.Helpers
public MappingProfiles() public MappingProfiles()
{ {
CreateMap<Product, ProductToReturnDto>() CreateMap<Product, ProductToReturnDto>()
.ForMember(d => d.ProductBrand, o => o.MapFrom(s => s.ProductBrand.Name)) .ForMember(d => d.ProductBrand, o => o.MapFrom(s => s.ProductBrand.Name))
.ForMember(d => d.ProductType, o => o.MapFrom(s => s.ProductType.Name)) .ForMember(d => d.ProductType, o => o.MapFrom(s => s.ProductType.Name))
.ForMember(d => d.PictureUrl, o => o.MapFrom<ProductUrlResolver>()); .ForMember(d => d.PictureUrl, o => o.MapFrom<ProductUrlResolver>());
CreateMap<Address, AddressDto>().ReverseMap(); CreateMap<Core.Entities.Identity.Address, AddressDto>().ReverseMap();
CreateMap<CustomerBasketDto, CustomerBasket>(); CreateMap<CustomerBasketDto, CustomerBasket>();
CreateMap<BasketItemDto, BasketItem>(); CreateMap<BasketItemDto, BasketItem>();
CreateMap<AddressDto, Core.Entities.OrderAggregate.Address>();
CreateMap<Order, OrderToReturnDto>()
.ForMember(d => d.DeliveryMethod, o => o.MapFrom(s => s.DeliveryMethod.ShortName))
.ForMember(d => d.ShippingPrice, o => o.MapFrom(s => s.DeliveryMethod.Price));
CreateMap<OrderItem, OrderItemDto>()
.ForMember(d => d.ProductId, o => o.MapFrom(s => s.ItemOrdered.ProductItemId))
.ForMember(d => d.ProductName, o => o.MapFrom(s => s.ItemOrdered.ProductName))
.ForMember(d => d.PictureUrl, o => o.MapFrom(s => s.ItemOrdered.PictureUrl))
.ForMember(d => d.PictureUrl, o => o.MapFrom<OrderItemUrlResolver>());
} }
} }
} }

View File

@ -0,0 +1,26 @@
using API.Dtos;
using AutoMapper;
using Core.Entities.OrderAggregate;
namespace API.Helpers
{
public class OrderItemUrlResolver : IValueResolver<OrderItem, OrderItemDto, string>
{
private readonly IConfiguration _config;
public OrderItemUrlResolver(IConfiguration config)
{
_config = config;
}
public string Resolve(OrderItem source, OrderItemDto destination, string destMember, ResolutionContext context)
{
if(!string.IsNullOrEmpty(source.ItemOrdered.PictureUrl))
{
return _config["ApiUrl"] + source.ItemOrdered.PictureUrl;
}
return null;
}
}
}

View File

@ -0,0 +1,32 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Core.Entities.OrderAggregate
{
public class Address
{
public Address()
{
}
public Address(string firstName, string lastName, string street, string city, string state, string zipCode)
{
FirstName = firstName;
LastName = lastName;
Street = street;
City = city;
State = state;
ZipCode = zipCode;
}
public string FirstName { get; set; }
public string LastName { get; set; }
public string Street { get; set; }
public string City { get; set; }
public string State { get; set; }
public string ZipCode { get; set; }
}
}

View File

@ -0,0 +1,15 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Core.Entities.OrderAggregate
{
public class DeliveryMethod : BaseEntity
{
public string ShortName { get; set; }
public string DeliveryTime { get; set; }
public string Description { get; set; }
public decimal Price { get; set; }
}
}

View File

@ -0,0 +1,31 @@
namespace Core.Entities.OrderAggregate
{
public class Order : BaseEntity
{
public Order()
{
}
public Order(IReadOnlyList<OrderItem> orderItems, string buyerEmail, Address shipToAddress, DeliveryMethod deliveryMethod, decimal subtotal)
{
BuyerEmail = buyerEmail;
ShipToAddress = shipToAddress;
DeliveryMethod = deliveryMethod;
OrderItems = orderItems;
Subtotal = subtotal;
}
public string BuyerEmail { get; set; }
public DateTimeOffset OrderDate { get; set; } = DateTimeOffset.Now;
public Address ShipToAddress { get; set; }
public DeliveryMethod DeliveryMethod { get; set; }
public IReadOnlyList<OrderItem> OrderItems { get; set; }
public decimal Subtotal { get; set; }
public OrderStatus Status { get; set; } = OrderStatus.Pending;
public string PaymentIntentId { get; set; }
public decimal GetTotal(){
return Subtotal + DeliveryMethod.Price;
}
}
}

View File

@ -0,0 +1,25 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Core.Entities.OrderAggregate
{
public class OrderItem : BaseEntity
{
public OrderItem()
{
}
public OrderItem(ProductItemOrdered itemOrdered, decimal price, int quantity)
{
ItemOrdered = itemOrdered;
Price = price;
Quantity = quantity;
}
public ProductItemOrdered ItemOrdered { get; set; }
public decimal Price { get; set; }
public int Quantity { get; set; }
}
}

View File

@ -0,0 +1,18 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;
using System.Threading.Tasks;
namespace Core.Entities.OrderAggregate
{
public enum OrderStatus
{
[EnumMember(Value = "Pending")]
Pending,
[EnumMember(Value = "Payment Received")]
PaymentReceived,
[EnumMember(Value = "Payment Failed")]
PaymentFailed
}
}

View File

@ -0,0 +1,25 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Core.Entities.OrderAggregate
{
public class ProductItemOrdered
{
public ProductItemOrdered()
{
}
public ProductItemOrdered(int productItemId, string productName, string pictureUrl)
{
ProductItemId = productItemId;
ProductName = productName;
PictureUrl = pictureUrl;
}
public int ProductItemId { get; set; }
public string ProductName { get; set; }
public string PictureUrl { get; set; }
}
}

View File

@ -10,5 +10,9 @@ namespace Core.Interfaces
Task<T> GetEntityWithSpec(ISpecification<T> spec); Task<T> GetEntityWithSpec(ISpecification<T> spec);
Task<IReadOnlyList<T>> ListAsync(ISpecification<T> spec); Task<IReadOnlyList<T>> ListAsync(ISpecification<T> spec);
Task<int> CountAsync(ISpecification<T> spec); Task<int> CountAsync(ISpecification<T> spec);
void Add(T entity);
void Update(T entity);
void Delete(T entity);
} }
} }

View File

@ -0,0 +1,12 @@
using Core.Entities.OrderAggregate;
namespace Core.Interfaces
{
public interface IOrderService
{
Task<Order> CreateOrderAsync(string buyerEmail, int deliverMethod, string basketId, Address ShippingAddress);
Task<IReadOnlyList<Order>> GetOrdersForUserAsync(string buyerEmail);
Task<Order> GetOrderByIdAsync(int id, string buyerEmail);
Task<IReadOnlyList<DeliveryMethod>> GetDeliveryMethodsAsync();
}
}

View File

@ -0,0 +1,10 @@
using Core.Entities;
namespace Core.Interfaces
{
public interface IUnitOfWork : IDisposable
{
IGenericRepository<TEntity> Repository<TEntity>() where TEntity : BaseEntity;
Task<int> Complete();
}
}

View File

@ -0,0 +1,25 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Threading.Tasks;
using Core.Entities.OrderAggregate;
namespace Core.Specifications
{
public class OrdersWithItemsAndOrderingSpecification : BaseSpecification<Order>
{
public OrdersWithItemsAndOrderingSpecification(string email) : base(o => o.BuyerEmail == email)
{
AddInclude(o => o.OrderItems);
AddInclude(o => o.DeliveryMethod);
AddOrdeByDescending(o => o.OrderDate);
}
public OrdersWithItemsAndOrderingSpecification(int id, string email) : base(o => o.Id == id && o.BuyerEmail == email)
{
AddInclude(o => o.OrderItems);
AddInclude(o => o.DeliveryMethod);
}
}
}

View File

@ -0,0 +1,15 @@
using Core.Entities.OrderAggregate;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace Infrastructure.Data.Config
{
public class DeliveryMethodConfiguration : IEntityTypeConfiguration<DeliveryMethod>
{
public void Configure(EntityTypeBuilder<DeliveryMethod> builder)
{
builder.Property(d => d.Price)
.HasColumnType("decimal(18,2)");
}
}
}

View File

@ -0,0 +1,22 @@
using Core.Entities.OrderAggregate;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace Infrastructure.Data.Config
{
public class OrderConfiguration : IEntityTypeConfiguration<Order>
{
public void Configure(EntityTypeBuilder<Order> builder)
{
builder.OwnsOne(o => o.ShipToAddress, a => {
a.WithOwner();
});
builder.Property(s => s.Status)
.HasConversion(
o => o.ToString(),
o => (OrderStatus) Enum.Parse(typeof(OrderStatus), o)
);
builder.HasMany(o => o.OrderItems).WithOne().OnDelete(DeleteBehavior.Cascade);
}
}
}

View File

@ -0,0 +1,20 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Core.Entities.OrderAggregate;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace Infrastructure.Data.Config
{
public class OrderItemConfiguration : IEntityTypeConfiguration<OrderItem>
{
public void Configure(EntityTypeBuilder<OrderItem> builder)
{
builder.OwnsOne(i => i.ItemOrdered, io => {io.WithOwner();});
builder.Property(i => i.Price)
.HasColumnType("decimal(18,2)");
}
}
}

View File

@ -41,5 +41,21 @@ namespace Infrastructure.Data
{ {
return SpecificationEvaluator<T>.GetQuery(_context.Set<T>().AsQueryable(), spec); return SpecificationEvaluator<T>.GetQuery(_context.Set<T>().AsQueryable(), spec);
} }
public void Add(T entity)
{
_context.Set<T>().Add(entity);
}
public void Update(T entity)
{
_context.Set<T>().Attach(entity);
_context.Entry(entity).State = EntityState.Modified;
}
public void Delete(T entity)
{
_context.Set<T>().Remove(entity);
}
} }
} }

View File

@ -0,0 +1,265 @@
// <auto-generated />
using System;
using Infrastructure.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace Infrastructure.Data.Migrations
{
[DbContext(typeof(StoreContext))]
[Migration("20220524182205_OrderEntityAdded")]
partial class OrderEntityAdded
{
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "6.0.5");
modelBuilder.Entity("Core.Entities.OrderAggregate.DeliveryMethod", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("DeliveryTime")
.HasColumnType("TEXT");
b.Property<string>("Description")
.HasColumnType("TEXT");
b.Property<double>("Price")
.HasColumnType("decimal(18,2)");
b.Property<string>("ShortName")
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("DeliveryMethod");
});
modelBuilder.Entity("Core.Entities.OrderAggregate.Order", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("BuyerEmail")
.HasColumnType("TEXT");
b.Property<int?>("DeliveryMethodId")
.HasColumnType("INTEGER");
b.Property<DateTimeOffset>("OrderDate")
.HasColumnType("TEXT");
b.Property<string>("PaymentIntentId")
.HasColumnType("TEXT");
b.Property<string>("Status")
.IsRequired()
.HasColumnType("TEXT");
b.Property<double>("Subtotal")
.HasColumnType("REAL");
b.HasKey("Id");
b.HasIndex("DeliveryMethodId");
b.ToTable("Orders");
});
modelBuilder.Entity("Core.Entities.OrderAggregate.OrderItem", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int?>("OrderId")
.HasColumnType("INTEGER");
b.Property<double>("Price")
.HasColumnType("decimal(18,2)");
b.Property<int>("Quantity")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("OrderId");
b.ToTable("OrderItems");
});
modelBuilder.Entity("Core.Entities.Product", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("Description")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("TEXT");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("TEXT");
b.Property<string>("PictureUrl")
.IsRequired()
.HasColumnType("TEXT");
b.Property<double>("Price")
.HasColumnType("decimal(18,2)");
b.Property<int>("ProductBrandId")
.HasColumnType("INTEGER");
b.Property<int>("ProductTypeId")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("ProductBrandId");
b.HasIndex("ProductTypeId");
b.ToTable("Products");
});
modelBuilder.Entity("Core.Entities.ProductBrand", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("Name")
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("ProductBrands");
});
modelBuilder.Entity("Core.Entities.ProductType", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("Name")
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("ProductTypes");
});
modelBuilder.Entity("Core.Entities.OrderAggregate.Order", b =>
{
b.HasOne("Core.Entities.OrderAggregate.DeliveryMethod", "DeliveryMethod")
.WithMany()
.HasForeignKey("DeliveryMethodId");
b.OwnsOne("Core.Entities.OrderAggregate.Address", "ShipToAddress", b1 =>
{
b1.Property<int>("OrderId")
.HasColumnType("INTEGER");
b1.Property<string>("City")
.HasColumnType("TEXT");
b1.Property<string>("FirstName")
.HasColumnType("TEXT");
b1.Property<string>("LastName")
.HasColumnType("TEXT");
b1.Property<string>("State")
.HasColumnType("TEXT");
b1.Property<string>("Street")
.HasColumnType("TEXT");
b1.Property<string>("ZipCode")
.HasColumnType("TEXT");
b1.HasKey("OrderId");
b1.ToTable("Orders");
b1.WithOwner()
.HasForeignKey("OrderId");
});
b.Navigation("DeliveryMethod");
b.Navigation("ShipToAddress");
});
modelBuilder.Entity("Core.Entities.OrderAggregate.OrderItem", b =>
{
b.HasOne("Core.Entities.OrderAggregate.Order", null)
.WithMany("OrderItems")
.HasForeignKey("OrderId")
.OnDelete(DeleteBehavior.Cascade);
b.OwnsOne("Core.Entities.OrderAggregate.ProductItemOrdered", "ItemOrdered", b1 =>
{
b1.Property<int>("OrderItemId")
.HasColumnType("INTEGER");
b1.Property<string>("PictureUrl")
.HasColumnType("TEXT");
b1.Property<int>("ProductItemId")
.HasColumnType("INTEGER");
b1.Property<string>("ProductName")
.HasColumnType("TEXT");
b1.HasKey("OrderItemId");
b1.ToTable("OrderItems");
b1.WithOwner()
.HasForeignKey("OrderItemId");
});
b.Navigation("ItemOrdered");
});
modelBuilder.Entity("Core.Entities.Product", b =>
{
b.HasOne("Core.Entities.ProductBrand", "ProductBrand")
.WithMany()
.HasForeignKey("ProductBrandId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Core.Entities.ProductType", "ProductType")
.WithMany()
.HasForeignKey("ProductTypeId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("ProductBrand");
b.Navigation("ProductType");
});
modelBuilder.Entity("Core.Entities.OrderAggregate.Order", b =>
{
b.Navigation("OrderItems");
});
#pragma warning restore 612, 618
}
}
}

View File

@ -0,0 +1,104 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Infrastructure.Data.Migrations
{
public partial class OrderEntityAdded : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "DeliveryMethod",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
ShortName = table.Column<string>(type: "TEXT", nullable: true),
DeliveryTime = table.Column<string>(type: "TEXT", nullable: true),
Description = table.Column<string>(type: "TEXT", nullable: true),
Price = table.Column<double>(type: "decimal(18,2)", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_DeliveryMethod", x => x.Id);
});
migrationBuilder.CreateTable(
name: "Orders",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
BuyerEmail = table.Column<string>(type: "TEXT", nullable: true),
OrderDate = table.Column<DateTimeOffset>(type: "TEXT", nullable: false),
ShipToAddress_FirstName = table.Column<string>(type: "TEXT", nullable: true),
ShipToAddress_LastName = table.Column<string>(type: "TEXT", nullable: true),
ShipToAddress_Street = table.Column<string>(type: "TEXT", nullable: true),
ShipToAddress_City = table.Column<string>(type: "TEXT", nullable: true),
ShipToAddress_State = table.Column<string>(type: "TEXT", nullable: true),
ShipToAddress_ZipCode = table.Column<string>(type: "TEXT", nullable: true),
DeliveryMethodId = table.Column<int>(type: "INTEGER", nullable: true),
Subtotal = table.Column<double>(type: "REAL", nullable: false),
Status = table.Column<string>(type: "TEXT", nullable: false),
PaymentIntentId = table.Column<string>(type: "TEXT", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_Orders", x => x.Id);
table.ForeignKey(
name: "FK_Orders_DeliveryMethod_DeliveryMethodId",
column: x => x.DeliveryMethodId,
principalTable: "DeliveryMethod",
principalColumn: "Id");
});
migrationBuilder.CreateTable(
name: "OrderItems",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
ItemOrdered_ProductItemId = table.Column<int>(type: "INTEGER", nullable: true),
ItemOrdered_ProductName = table.Column<string>(type: "TEXT", nullable: true),
ItemOrdered_PictureUrl = table.Column<string>(type: "TEXT", nullable: true),
Price = table.Column<double>(type: "decimal(18,2)", nullable: false),
Quantity = table.Column<int>(type: "INTEGER", nullable: false),
OrderId = table.Column<int>(type: "INTEGER", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_OrderItems", x => x.Id);
table.ForeignKey(
name: "FK_OrderItems_Orders_OrderId",
column: x => x.OrderId,
principalTable: "Orders",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_OrderItems_OrderId",
table: "OrderItems",
column: "OrderId");
migrationBuilder.CreateIndex(
name: "IX_Orders_DeliveryMethodId",
table: "Orders",
column: "DeliveryMethodId");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "OrderItems");
migrationBuilder.DropTable(
name: "Orders");
migrationBuilder.DropTable(
name: "DeliveryMethod");
}
}
}

View File

@ -1,4 +1,5 @@
// <auto-generated /> // <auto-generated />
using System;
using Infrastructure.Data; using Infrastructure.Data;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Infrastructure;
@ -14,7 +15,84 @@ namespace Infrastructure.Data.Migrations
protected override void BuildModel(ModelBuilder modelBuilder) protected override void BuildModel(ModelBuilder modelBuilder)
{ {
#pragma warning disable 612, 618 #pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "6.0.4"); modelBuilder.HasAnnotation("ProductVersion", "6.0.5");
modelBuilder.Entity("Core.Entities.OrderAggregate.DeliveryMethod", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("DeliveryTime")
.HasColumnType("TEXT");
b.Property<string>("Description")
.HasColumnType("TEXT");
b.Property<double>("Price")
.HasColumnType("decimal(18,2)");
b.Property<string>("ShortName")
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("DeliveryMethod");
});
modelBuilder.Entity("Core.Entities.OrderAggregate.Order", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("BuyerEmail")
.HasColumnType("TEXT");
b.Property<int?>("DeliveryMethodId")
.HasColumnType("INTEGER");
b.Property<DateTimeOffset>("OrderDate")
.HasColumnType("TEXT");
b.Property<string>("PaymentIntentId")
.HasColumnType("TEXT");
b.Property<string>("Status")
.IsRequired()
.HasColumnType("TEXT");
b.Property<double>("Subtotal")
.HasColumnType("REAL");
b.HasKey("Id");
b.HasIndex("DeliveryMethodId");
b.ToTable("Orders");
});
modelBuilder.Entity("Core.Entities.OrderAggregate.OrderItem", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int?>("OrderId")
.HasColumnType("INTEGER");
b.Property<double>("Price")
.HasColumnType("decimal(18,2)");
b.Property<int>("Quantity")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("OrderId");
b.ToTable("OrderItems");
});
modelBuilder.Entity("Core.Entities.Product", b => modelBuilder.Entity("Core.Entities.Product", b =>
{ {
@ -36,7 +114,7 @@ namespace Infrastructure.Data.Migrations
.IsRequired() .IsRequired()
.HasColumnType("TEXT"); .HasColumnType("TEXT");
b.Property<decimal>("Price") b.Property<double>("Price")
.HasColumnType("decimal(18,2)"); .HasColumnType("decimal(18,2)");
b.Property<int>("ProductBrandId") b.Property<int>("ProductBrandId")
@ -82,6 +160,80 @@ namespace Infrastructure.Data.Migrations
b.ToTable("ProductTypes"); b.ToTable("ProductTypes");
}); });
modelBuilder.Entity("Core.Entities.OrderAggregate.Order", b =>
{
b.HasOne("Core.Entities.OrderAggregate.DeliveryMethod", "DeliveryMethod")
.WithMany()
.HasForeignKey("DeliveryMethodId");
b.OwnsOne("Core.Entities.OrderAggregate.Address", "ShipToAddress", b1 =>
{
b1.Property<int>("OrderId")
.HasColumnType("INTEGER");
b1.Property<string>("City")
.HasColumnType("TEXT");
b1.Property<string>("FirstName")
.HasColumnType("TEXT");
b1.Property<string>("LastName")
.HasColumnType("TEXT");
b1.Property<string>("State")
.HasColumnType("TEXT");
b1.Property<string>("Street")
.HasColumnType("TEXT");
b1.Property<string>("ZipCode")
.HasColumnType("TEXT");
b1.HasKey("OrderId");
b1.ToTable("Orders");
b1.WithOwner()
.HasForeignKey("OrderId");
});
b.Navigation("DeliveryMethod");
b.Navigation("ShipToAddress");
});
modelBuilder.Entity("Core.Entities.OrderAggregate.OrderItem", b =>
{
b.HasOne("Core.Entities.OrderAggregate.Order", null)
.WithMany("OrderItems")
.HasForeignKey("OrderId")
.OnDelete(DeleteBehavior.Cascade);
b.OwnsOne("Core.Entities.OrderAggregate.ProductItemOrdered", "ItemOrdered", b1 =>
{
b1.Property<int>("OrderItemId")
.HasColumnType("INTEGER");
b1.Property<string>("PictureUrl")
.HasColumnType("TEXT");
b1.Property<int>("ProductItemId")
.HasColumnType("INTEGER");
b1.Property<string>("ProductName")
.HasColumnType("TEXT");
b1.HasKey("OrderItemId");
b1.ToTable("OrderItems");
b1.WithOwner()
.HasForeignKey("OrderItemId");
});
b.Navigation("ItemOrdered");
});
modelBuilder.Entity("Core.Entities.Product", b => modelBuilder.Entity("Core.Entities.Product", b =>
{ {
b.HasOne("Core.Entities.ProductBrand", "ProductBrand") b.HasOne("Core.Entities.ProductBrand", "ProductBrand")
@ -100,6 +252,11 @@ namespace Infrastructure.Data.Migrations
b.Navigation("ProductType"); b.Navigation("ProductType");
}); });
modelBuilder.Entity("Core.Entities.OrderAggregate.Order", b =>
{
b.Navigation("OrderItems");
});
#pragma warning restore 612, 618 #pragma warning restore 612, 618
} }
} }

View File

@ -0,0 +1,30 @@
[
{
"Id": 1,
"ShortName": "UPS1",
"Description": "Fastest delivery time",
"DeliveryTime": "1-2 Days",
"Price": 10
},
{
"Id": 2,
"ShortName": "UPS2",
"Description": "Get it within 5 days",
"DeliveryTime": "2-5 Days",
"Price": 5
},
{
"Id": 3,
"ShortName": "UPS3",
"Description": "Slower but cheap",
"DeliveryTime": "5-10 Days",
"Price": 2
},
{
"Id": 4,
"ShortName": "FREE",
"Description": "Free! You get what you pay for",
"DeliveryTime": "1-2 Weeks",
"Price": 0
}
]

View File

@ -1,6 +1,8 @@
using System.Reflection; using System.Reflection;
using Core.Entities; using Core.Entities;
using Core.Entities.OrderAggregate;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
namespace Infrastructure.Data namespace Infrastructure.Data
{ {
@ -12,6 +14,9 @@ namespace Infrastructure.Data
public DbSet<Product> Products { get; set; } public DbSet<Product> Products { get; set; }
public DbSet<ProductBrand> ProductBrands { get; set; } public DbSet<ProductBrand> ProductBrands { get; set; }
public DbSet<ProductType> ProductTypes { get; set; } public DbSet<ProductType> ProductTypes { get; set; }
public DbSet<Order> Orders { get; set; }
public DbSet<OrderItem> OrderItems { get; set; }
public DbSet<DeliveryMethod> DeliveryMethod { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder) protected override void OnModelCreating(ModelBuilder modelBuilder)
{ {
base.OnModelCreating(modelBuilder); base.OnModelCreating(modelBuilder);
@ -22,10 +27,16 @@ namespace Infrastructure.Data
foreach (var entityType in modelBuilder.Model.GetEntityTypes()) foreach (var entityType in modelBuilder.Model.GetEntityTypes())
{ {
var properties = entityType.ClrType.GetProperties().Where(p => p.PropertyType == typeof(decimal)); var properties = entityType.ClrType.GetProperties().Where(p => p.PropertyType == typeof(decimal));
var dateTimeProperties = entityType.ClrType.GetProperties().Where(p => p.PropertyType == typeof(DateTimeOffset));
foreach (var property in properties) foreach (var property in properties)
{ {
modelBuilder.Entity(entityType.Name).Property(property.Name).HasConversion<double>(); modelBuilder.Entity(entityType.Name).Property(property.Name).HasConversion<double>();
} }
foreach (var property in dateTimeProperties)
{
modelBuilder.Entity(entityType.Name).Property(property.Name).HasConversion(new DateTimeOffsetToBinaryConverter());
}
} }
} }
} }

View File

@ -1,5 +1,6 @@
using System.Text.Json; using System.Text.Json;
using Core.Entities; using Core.Entities;
using Core.Entities.OrderAggregate;
using Infrastructure.Data; using Infrastructure.Data;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@ -46,6 +47,20 @@ namespace Infrastructure
await context.SaveChangesAsync(); await context.SaveChangesAsync();
} }
if (!context.DeliveryMethod.Any())
{
var dmData = File.ReadAllText("../Infrastructure/Data/SeedData/delivery.json");
var methods = JsonSerializer.Deserialize<List<DeliveryMethod>>(dmData);
foreach (var item in methods)
{
context.DeliveryMethod.Add(item);
}
await context.SaveChangesAsync();
}
} }
catch (Exception ex) catch (Exception ex)
{ {

View File

@ -0,0 +1,38 @@
using System.Collections;
using Core.Entities;
using Core.Interfaces;
namespace Infrastructure.Data
{
public class UnitOfWork : IUnitOfWork
{
private readonly StoreContext _context;
private Hashtable _repositories;
public UnitOfWork(StoreContext context)
{
_context = context;
}
public async Task<int> Complete()
{
return await _context.SaveChangesAsync();
}
public void Dispose()
{
_context.Dispose();
}
public IGenericRepository<TEntity> Repository<TEntity>() where TEntity : BaseEntity
{
if(_repositories == null) _repositories = new Hashtable();
var type = typeof(TEntity).Name;
if(!_repositories.ContainsKey(type)){
var repositoryType = typeof(GenericRepository<>);
var repositoryInstance = Activator.CreateInstance(repositoryType.MakeGenericType(typeof(TEntity)), _context);
_repositories.Add(type, repositoryInstance);
}
return (IGenericRepository<TEntity>) _repositories[type];
}
}
}

View File

@ -6,7 +6,7 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Identity" Version="2.2.0" /> <PackageReference Include="Microsoft.AspNetCore.Identity" Version="2.2.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="6.0.4" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="6.0.5" />
<PackageReference Include="Microsoft.IdentityModel.Tokens" Version="6.17.0" /> <PackageReference Include="Microsoft.IdentityModel.Tokens" Version="6.17.0" />
<PackageReference Include="StackExchange.Redis" Version="2.5.61" /> <PackageReference Include="StackExchange.Redis" Version="2.5.61" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="6.17.0" /> <PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="6.17.0" />

View File

@ -0,0 +1,57 @@
using Core.Entities;
using Core.Entities.OrderAggregate;
using Core.Interfaces;
using Core.Specifications;
namespace Infrastructure.Services
{
public class OrderService : IOrderService
{
private readonly IBasketRepository _basketRepo;
private readonly IUnitOfWork _unitOfWork;
public OrderService(IBasketRepository basketRepo, IUnitOfWork unitOfWork)
{
_unitOfWork = unitOfWork;
_basketRepo = basketRepo;
}
public async Task<Order> CreateOrderAsync(string buyerEmail, int deliverMethodId, string basketId, Address shippingAddress)
{
var basket = await _basketRepo.GetBasketAsync(basketId);
var items = new List<OrderItem>();
foreach (var item in basket.Items)
{
var productItem = await _unitOfWork.Repository<Product>().GetByIdAsync(item.Id);
var itemOrdered = new ProductItemOrdered(productItem.Id, productItem.Name, productItem.PictureUrl);
var orderItem = new OrderItem(itemOrdered, productItem.Price, item.Quantity);
items.Add(orderItem);
}
var deliveryMethod = await _unitOfWork.Repository<DeliveryMethod>().GetByIdAsync(deliverMethodId);
var subtotal = items.Sum(item => item.Price * item.Quantity);
var order = new Order(items, buyerEmail, shippingAddress, deliveryMethod, subtotal);
_unitOfWork.Repository<Order>().Add(order);
var result = await _unitOfWork.Complete();
if(result <= 0) return null;
await _basketRepo.DeleteBasketAsysnc(basketId);
return order;
}
public async Task<IReadOnlyList<DeliveryMethod>> GetDeliveryMethodsAsync()
{
return await _unitOfWork.Repository<DeliveryMethod>().ListAllAsync();
}
public async Task<Order> GetOrderByIdAsync(int id, string buyerEmail)
{
var spec = new OrdersWithItemsAndOrderingSpecification(id, buyerEmail);
return await _unitOfWork.Repository<Order>().GetEntityWithSpec(spec);
}
public async Task<IReadOnlyList<Order>> GetOrdersForUserAsync(string buyerEmail)
{
var spec = new OrdersWithItemsAndOrderingSpecification(buyerEmail);
return await _unitOfWork.Repository<Order>().ListAsync(spec);
}
}
}