Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 18 additions & 1 deletion src/Microsoft.eShopWeb.Web/Basket/Basket.Component.fs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,23 @@ module BasketComponent =
module private Template =

let itemTmpl index (item: BasketItem) =
let quantityControls =
div [ class' "d-flex align-items-center" ]
[ if item.Quantity > 1 then
Elem.form
[ method "post"; action "/basket/updatequantity"; class' "me-1" ]
[ input [ type' "hidden"; name "id"; value $"{item.CatalogItemId}" ]
input [ type' "hidden"; name "quantity"; value $"{item.Quantity - 1}" ]
input [ class' "btn btn-sm btn-outline-secondary"; type' "submit"; value "-" ] ]
else
Elem.span [ class' "btn btn-sm btn-outline-secondary me-1 disabled" ] [ raw "-" ]
Elem.span [ class' "mx-2" ] [ raw (item.Quantity.ToString()) ]
Elem.form
[ method "post"; action "/basket/updatequantity"; class' "ms-1" ]
[ input [ type' "hidden"; name "id"; value $"{item.CatalogItemId}" ]
input [ type' "hidden"; name "quantity"; value $"{item.Quantity + 1}" ]
input [ class' "btn btn-sm btn-outline-secondary"; type' "submit"; value "+" ] ] ]

article
[ class' "esh-basket-items" ]
[ div
Expand All @@ -28,7 +45,7 @@ module BasketComponent =
class' "esh-basket-image" ] ]
section [ class' "esh-basket-item esh-basket-item--middle col" ] [ raw item.ProductName ]
section [ class' "esh-basket-item esh-basket-item--middle col" ] [ raw (item.UnitPrice.ToString "C") ]
section [ class' "esh-basket-item esh-basket-item--middle col" ] [ raw (item.Quantity.ToString()) ]
section [ class' "esh-basket-item esh-basket-item--middle col" ] [ quantityControls ]
section [ class' "esh-basket-item esh-basket-item--middle col" ] [ raw ((decimal(item.Quantity) * item.UnitPrice).ToString "C" ) ]
section [ class' "esh-basket-item esh-basket-item--middle col" ]
[ Elem.form
Expand Down
28 changes: 28 additions & 0 deletions src/Microsoft.eShopWeb.Web/Basket/Basket.Domain.fs
Original file line number Diff line number Diff line change
Expand Up @@ -123,3 +123,31 @@ module BasketDomain =
printfn $"Error removing item {catalogItemId} from basket"; printfn $"{exp}"
return None
}

let updateBasketItemQuantity (db: ShopContext) catalogItemId newQuantity =
async {
// Validate quantity
if newQuantity < 1 then
return Error "Quantity must be at least 1"
else
let! existingBasket =
(db.Baskets.Include(fun b -> b.Items).OrderBy(fun b -> b.Id)) |> tryFirstAsync

let basket = existingBasket |> defaultValue emptyBasket

try
let itemToUpdate =
db.BasketItems.Where(fun bi -> bi.CatalogItemId = catalogItemId && bi.BasketId = basket.Id)
|> Seq.tryHead

match itemToUpdate with
| Some item ->
item.Quantity <- newQuantity
do! saveChangesAsync' db |> Async.Ignore
return Ok newQuantity
| None ->
return Error "Item not found in basket"
with exp ->
printfn $"Error updating quantity for item {catalogItemId}"; printfn $"{exp}"
return Error "Failed to update item quantity"
}
20 changes: 20 additions & 0 deletions src/Microsoft.eShopWeb.Web/Basket/Basket.Page.fs
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,26 @@ module BasketPage =
| None -> Response.redirectPermanently "/basket?error=notfound"
| Some _ -> Response.redirectPermanently "/basket?removed=success"))

let updateQuantity: HttpHandler =
Services.inject<ShopContext> (fun db ->

let mapAsync = fun (form: FormCollectionReader) ->
async {
let catalogItemId = form.TryGetGuid "id"
let quantity = form.TryGetInt32 "quantity"

match catalogItemId, quantity with
| Some id, Some qty ->
return! BasketDomain.updateBasketItemQuantity db id qty
| _ ->
return Error "Invalid request parameters"
} |> Async.StartAsTask

Request.mapFormAsync mapAsync (fun result ->
match result with
| Ok qty -> Response.redirectPermanently $"/basket?updated={qty}"
| Error msg -> Response.redirectPermanently $"/basket?error={msg}"))

// This uses a more low-level approach to reading the form
let postAlternate: HttpHandler =
Services.inject<ShopContext> (fun db -> fun ctx ->
Expand Down
1 change: 1 addition & 0 deletions src/Microsoft.eShopWeb.Web/Program.fs
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ module Program =
get "/basket" BasketPage.get
post "/basket" BasketPage.post
post "/basket/remove" BasketPage.remove
post "/basket/updatequantity" BasketPage.updateQuantity

get "/identity/account/login" LoginPage.handler

Expand Down
222 changes: 222 additions & 0 deletions tests/Basket/UpdateBasketItemQuantity.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
module UpdateBasketItemQuantity

open System
open System.Linq
open Xunit
open Microsoft.EntityFrameworkCore
open Microsoft.eShopWeb.Web
open Microsoft.eShopWeb.Web.Domain
open Microsoft.eShopWeb.Web.Persistence
open Microsoft.eShopWeb.Web.Basket.BasketDomain
open EntityFrameworkCore.FSharp.DbContextHelpers

// Helper function to create an in-memory database context
let createInMemoryContext () =
let options = DbContextOptionsBuilder<ShopContext>()
.UseInMemoryDatabase(databaseName = Guid.NewGuid().ToString())
.Options
new ShopContext(options)

// Helper function to seed test data
let seedTestData (context: ShopContext) =
let catalogItemId = Guid.NewGuid()

let basketItem = {
Id = 0
CatalogItemId = catalogItemId
ProductName = "Test Product"
UnitPrice = 10.0M
OldUnitPrice = 10.0M
Quantity = 5
PictureUri = "/test.png"
BasketId = Unchecked.defaultof<Guid>
}

context.BasketItems.Add(basketItem) |> ignore
context.SaveChanges() |> ignore

catalogItemId

[<Fact>]
let ``updateBasketItemQuantity should return Ok when updating to valid quantity`` () =
async {
// Arrange
use context = createInMemoryContext()
context.Database.EnsureCreated() |> ignore
let catalogItemId = seedTestData context
let newQuantity = 10

// Act
let! result = updateBasketItemQuantity context catalogItemId newQuantity

// Assert
match result with
| Ok qty ->
Assert.Equal(newQuantity, qty)

// Verify the quantity was actually updated in the database
let updatedItem = context.BasketItems.FirstOrDefault(fun bi -> bi.CatalogItemId = catalogItemId)
Assert.NotNull(updatedItem)
Assert.Equal(newQuantity, updatedItem.Quantity)
| Error msg ->
Assert.True(false, $"Expected Ok but got Error: {msg}")
}

[<Fact>]
let ``updateBasketItemQuantity should return Error when quantity is less than 1`` () =
async {
// Arrange
use context = createInMemoryContext()
context.Database.EnsureCreated() |> ignore
let catalogItemId = seedTestData context
let invalidQuantity = 0

// Act
let! result = updateBasketItemQuantity context catalogItemId invalidQuantity

// Assert
match result with
| Ok _ -> Assert.True(false, "Expected Error but got Ok")
| Error msg -> Assert.Equal("Quantity must be at least 1", msg)
}

[<Fact>]
let ``updateBasketItemQuantity should return Error when quantity is negative`` () =
async {
// Arrange
use context = createInMemoryContext()
context.Database.EnsureCreated() |> ignore
let catalogItemId = seedTestData context
let invalidQuantity = -5

// Act
let! result = updateBasketItemQuantity context catalogItemId invalidQuantity

// Assert
match result with
| Ok _ -> Assert.True(false, "Expected Error but got Ok")
| Error msg -> Assert.Equal("Quantity must be at least 1", msg)
}

[<Fact>]
let ``updateBasketItemQuantity should return Error when item does not exist`` () =
async {
// Arrange
use context = createInMemoryContext()
context.Database.EnsureCreated() |> ignore
let _ = seedTestData context
let nonExistentItemId = Guid.NewGuid()
let newQuantity = 5

// Act
let! result = updateBasketItemQuantity context nonExistentItemId newQuantity

// Assert
match result with
| Ok _ -> Assert.True(false, "Expected Error but got Ok")
| Error msg -> Assert.Equal("Item not found in basket", msg)
}

[<Fact>]
let ``updateBasketItemQuantity should update quantity to 1`` () =
async {
// Arrange
use context = createInMemoryContext()
context.Database.EnsureCreated() |> ignore
let catalogItemId = seedTestData context
let newQuantity = 1

// Act
let! result = updateBasketItemQuantity context catalogItemId newQuantity

// Assert
match result with
| Ok qty ->
Assert.Equal(newQuantity, qty)

// Verify the quantity was actually updated in the database
let updatedItem = context.BasketItems.FirstOrDefault(fun bi -> bi.CatalogItemId = catalogItemId)
Assert.NotNull(updatedItem)
Assert.Equal(1, updatedItem.Quantity)
| Error msg ->
Assert.True(false, $"Expected Ok but got Error: {msg}")
}

[<Fact>]
let ``updateBasketItemQuantity should handle large quantities`` () =
async {
// Arrange
use context = createInMemoryContext()
context.Database.EnsureCreated() |> ignore
let catalogItemId = seedTestData context
let largeQuantity = 1000

// Act
let! result = updateBasketItemQuantity context catalogItemId largeQuantity

// Assert
match result with
| Ok qty ->
Assert.Equal(largeQuantity, qty)

// Verify the quantity was actually updated in the database
let updatedItem = context.BasketItems.FirstOrDefault(fun bi -> bi.CatalogItemId = catalogItemId)
Assert.NotNull(updatedItem)
Assert.Equal(largeQuantity, updatedItem.Quantity)
| Error msg ->
Assert.True(false, $"Expected Ok but got Error: {msg}")
}

[<Fact>]
let ``updateBasketItemQuantity should not affect other basket items`` () =
async {
// Arrange
use context = createInMemoryContext()
context.Database.EnsureCreated() |> ignore

let catalogItemId1 = Guid.NewGuid()
let catalogItemId2 = Guid.NewGuid()

let basketItem1 = {
Id = 0
CatalogItemId = catalogItemId1
ProductName = "Test Product 1"
UnitPrice = 10.0M
OldUnitPrice = 10.0M
Quantity = 5
PictureUri = "/test1.png"
BasketId = Unchecked.defaultof<Guid>
}

let basketItem2 = {
Id = 0
CatalogItemId = catalogItemId2
ProductName = "Test Product 2"
UnitPrice = 20.0M
OldUnitPrice = 20.0M
Quantity = 3
PictureUri = "/test2.png"
BasketId = Unchecked.defaultof<Guid>
}

context.BasketItems.AddRange([basketItem1; basketItem2]) |> ignore
context.SaveChanges() |> ignore

let newQuantity = 10

// Act - update only first item
let! result = updateBasketItemQuantity context catalogItemId1 newQuantity

// Assert
match result with
| Ok _ ->
// Verify first item was updated
let updatedItem1 = context.BasketItems.FirstOrDefault(fun bi -> bi.CatalogItemId = catalogItemId1)
Assert.Equal(newQuantity, updatedItem1.Quantity)

// Verify second item was not affected
let unchangedItem2 = context.BasketItems.FirstOrDefault(fun bi -> bi.CatalogItemId = catalogItemId2)
Assert.Equal(3, unchangedItem2.Quantity)
| Error msg ->
Assert.True(false, $"Expected Ok but got Error: {msg}")
}
1 change: 1 addition & 0 deletions tests/FShopOnWeb.Tests.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

<ItemGroup>
<Compile Include="Basket/RemoveFromBasket.fs" />
<Compile Include="Basket/UpdateBasketItemQuantity.fs" />
<Compile Include="CentralPackageManagementTests.fs" />
<Compile Include="Program.fs" />
</ItemGroup>
Expand Down