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
39 changes: 39 additions & 0 deletions e2e/gelato-order.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { expect, test } from "@playwright/test";

test("complete gelato order without waffle cone", async ({ page }) => {
await page.goto("/");

await page.getByRole("button", { name: "🎨 Start Creating Your Ice Cream" }).click();

await page.getByText("Bowl Cup", { exact: true }).click();

await page.getByText("Vanilla Dream", { exact: true }).click();
await page.getByText("Strawberry Bliss", { exact: true }).click();
await page.getByText("Pistachio", { exact: true }).click();

await page.getByText("Rainbow Sprinkles", { exact: true }).click();
await page.getByText("Hot Fudge", { exact: true }).click();

await page.getByRole("button", { name: "🛒 Add to Cart" }).click();
await page.getByRole("button", { name: "View Cart" }).click();

await page.getByRole("button", { name: "💳 Proceed to Checkout" }).click();

await page.getByPlaceholder("Enter your first name").fill("Randy");
await page.getByPlaceholder("Enter your last name").fill("Pagels");
await page.getByPlaceholder("your@email.com").fill("randy.pagels@example.com");
await page.getByPlaceholder("(555) 123-4567").fill("555-123-4567");

await page.getByRole("button", { name: "Continue to Delivery →" }).click();
await page.getByText("Store Pickup", { exact: true }).click();
await page.getByRole("button", { name: "Continue to Payment →" }).click();

await page.getByPlaceholder("John Doe").fill("Randy Pagels");
await page.getByPlaceholder("1234 5678 9012 3456").fill("4242 4242 4242 4242");
await page.getByPlaceholder("MM/YY").fill("12/30");
await page.getByPlaceholder("123").fill("123");

await page.getByRole("button", { name: /Complete Order/ }).click();

await expect(page.getByRole("heading", { name: "Order Confirmed!" })).toBeVisible();
});
7 changes: 6 additions & 1 deletion ginos-gelato/client/src/utils/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,9 @@ export const validateOrder = (order: IceCream): string | null => {
return 'You can select up to three flavors.';
}
return null;
};
};

// Create a function that applies a 10% discount to a given price and returns the discounted price.
export const applyDiscount = (price: number): number => {
return price * 0.9;
};
63 changes: 63 additions & 0 deletions ginos-gelato/server/Controllers/OrderQueueController.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
using Microsoft.AspNetCore.Mvc;
using GinosGelato.Models;
using GinosGelato.Services;

namespace GinosGelato.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class OrderQueueController : ControllerBase
{
private readonly OrderQueueService _queueService;

public OrderQueueController(OrderQueueService queueService)
{
_queueService = queueService;
}

/// <summary>
/// Returns the current queue status including pending order count
/// and estimated wait time for a new order.
/// </summary>
[HttpGet("status")]
public ActionResult<QueueStatusResponse> GetQueueStatus()
{
var response = new QueueStatusResponse
{
PendingOrderCount = _queueService.GetPendingOrderCount(),
EstimatedWaitMinutes = _queueService.GetEstimatedWaitMinutes(),
AveragePrepTimeMinutes = _queueService.GetAveragePrepTimeMinutes()
};
return Ok(response);
}

/// <summary>
/// Returns the average preparation time across all completed orders.
/// Returns 204 No Content when no orders have been completed yet.
/// </summary>
[HttpGet("average-prep-time")]
public ActionResult<AveragePrepTimeResponse> GetAveragePrepTime()
{
var avg = _queueService.GetAveragePrepTimeMinutes();
if (avg == null)
return NoContent();

return Ok(new AveragePrepTimeResponse { AveragePrepTimeMinutes = avg.Value });
}

/// <summary>
/// Marks an order as completed so its preparation time is recorded
/// and factored into future wait-time estimates.
/// </summary>
[HttpPost("{id}/complete")]
public ActionResult CompleteOrder(int id)
{
var completed = _queueService.CompleteOrder(id);
if (!completed)
return NotFound($"Order {id} was not found in the queue.");

return NoContent();
}
}
}

11 changes: 9 additions & 2 deletions ginos-gelato/server/Controllers/OrdersController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,15 @@ public OrdersController(OrderService orderService)
[HttpPost]
public async Task<ActionResult<Order>> CreateOrder(Order order)
{
var createdOrder = await _orderService.CreateOrderAsync(order.IceCreams);
return CreatedAtAction(nameof(GetOrder), new { id = createdOrder.Id }, createdOrder);
try
{
var createdOrder = await _orderService.CreateOrderAsync(order.IceCreams);
return CreatedAtAction(nameof(GetOrder), new { id = createdOrder.Id }, createdOrder);
}
catch (Exception)
{
return StatusCode(500, "An error occurred while processing your order.");
}
}

[HttpGet("{id}")]
Expand Down
22 changes: 22 additions & 0 deletions ginos-gelato/server/Models/OrderQueueEntry.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
namespace GinosGelato.Models
{
public enum OrderQueueStatus
{
Queued,
InProgress,
Completed
}

public class OrderQueueEntry
{
public int OrderId { get; set; }
public DateTime QueuedAt { get; set; }
public DateTime? CompletedAt { get; set; }
public OrderQueueStatus Status { get; set; } = OrderQueueStatus.Queued;

public double? PrepTimeMinutes =>
CompletedAt.HasValue
? (CompletedAt.Value - QueuedAt).TotalMinutes
: null;
}
}
14 changes: 14 additions & 0 deletions ginos-gelato/server/Models/OrderQueueResponses.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
namespace GinosGelato.Models
{
public class QueueStatusResponse
{
public int PendingOrderCount { get; set; }
public double EstimatedWaitMinutes { get; set; }
public double? AveragePrepTimeMinutes { get; set; }
}

public class AveragePrepTimeResponse
{
public double AveragePrepTimeMinutes { get; set; }
}
}
1 change: 1 addition & 0 deletions ginos-gelato/server/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
options.UseInMemoryDatabase("GinosGelatoDb")); // Using in-memory database for simplicity

// Add custom services
builder.Services.AddSingleton<OrderQueueService>();
builder.Services.AddScoped<OrderService>();

// Add Swagger/OpenAPI
Expand Down
84 changes: 84 additions & 0 deletions ginos-gelato/server/Services/OrderQueueService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
using System.Collections.Concurrent;
using GinosGelato.Models;

namespace GinosGelato.Services
{
public class OrderQueueService
{
private readonly ConcurrentDictionary<int, OrderQueueEntry> _queue = new();

/// <summary>
/// Adds a new order to the preparation queue with status Queued.
/// </summary>
public void EnqueueOrder(int orderId)
{
var entry = new OrderQueueEntry
{
OrderId = orderId,
QueuedAt = DateTime.UtcNow,
Status = OrderQueueStatus.Queued
};
_queue.TryAdd(orderId, entry);
}

/// <summary>
/// Marks an order as completed and records the completion time.
/// Returns false if the order was not found in the queue.
/// </summary>
public bool CompleteOrder(int orderId)
{
if (!_queue.TryGetValue(orderId, out var entry))
return false;

entry.CompletedAt = DateTime.UtcNow;
entry.Status = OrderQueueStatus.Completed;
return true;
}

/// <summary>
/// Returns the number of orders that are currently pending (Queued or InProgress).
/// </summary>
public int GetPendingOrderCount()
{
return _queue.Values.Count(e => e.Status != OrderQueueStatus.Completed);
}

/// <summary>
/// Returns the average preparation time in minutes across all completed orders,
/// or null if there are no completed orders yet.
/// </summary>
public double? GetAveragePrepTimeMinutes()
{
var completedTimes = _queue.Values
.Where(e => e.PrepTimeMinutes.HasValue)
.Select(e => e.PrepTimeMinutes!.Value)
.ToList();

if (completedTimes.Count == 0)
return null;

return completedTimes.Average();
}

/// <summary>
/// Returns an estimated wait time in minutes for a new order placed right now.
/// The estimate is: pending orders × average prep time per order.
/// Falls back to a default of 5 minutes per order when no history is available.
/// </summary>
public double GetEstimatedWaitMinutes()
{
const double defaultPrepMinutes = 5.0;
var avgPrepTime = GetAveragePrepTimeMinutes() ?? defaultPrepMinutes;
var pendingCount = GetPendingOrderCount();
return pendingCount * avgPrepTime;
}

/// <summary>
/// Returns all queue entries (useful for diagnostics and testing).
/// </summary>
public IReadOnlyList<OrderQueueEntry> GetAllEntries()
{
return _queue.Values.ToList().AsReadOnly();
}
}
}
6 changes: 5 additions & 1 deletion ginos-gelato/server/Services/OrderService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,12 @@ namespace GinosGelato.Services
public class OrderService
{
private readonly ApplicationDbContext _context;
private readonly OrderQueueService _queueService;

public OrderService(ApplicationDbContext context)
public OrderService(ApplicationDbContext context, OrderQueueService queueService)
{
_context = context;
_queueService = queueService;
}

public async Task<Order> CreateOrderAsync(List<IceCream> iceCreams)
Expand All @@ -28,6 +30,8 @@ public async Task<Order> CreateOrderAsync(List<IceCream> iceCreams)
_context.Orders.Add(order);
await _context.SaveChangesAsync();

_queueService.EnqueueOrder(order.Id);

return order;
}

Expand Down
Loading