From 11ac8b5f8bd76055dd5d0bba1fd1887151a41869 Mon Sep 17 00:00:00 2001
From: Omid Marfavi <21163286+marfavi@users.noreply.github.com>
Date: Sun, 3 May 2026 15:36:15 +0200
Subject: [PATCH 1/7] Rewrite receipts endpoint to remove pagination and
polymorphism
---
.../Services/v2/IReceiptService.cs | 15 +-
.../Services/v2/ReceiptService.cs | 154 +++---
.../v2/Receipts/ReceiptBase.cs | 122 ++---
.../v2/Receipts/ReceiptsRequest.cs | 37 +-
.../Controllers/v2/GetReceiptsTest.cs | 449 ++++++++++++------
.../Controllers/v2/ReceiptController.cs | 63 +--
6 files changed, 487 insertions(+), 353 deletions(-)
diff --git a/coffeecard/CoffeeCard.Library/Services/v2/IReceiptService.cs b/coffeecard/CoffeeCard.Library/Services/v2/IReceiptService.cs
index 91ee001f..88fbc54b 100644
--- a/coffeecard/CoffeeCard.Library/Services/v2/IReceiptService.cs
+++ b/coffeecard/CoffeeCard.Library/Services/v2/IReceiptService.cs
@@ -1,10 +1,21 @@
-using System;
using System.Threading.Tasks;
using CoffeeCard.Models.DataTransferObjects.v2.Receipts;
namespace CoffeeCard.Library.Services.v2;
+///
+/// Service for retrieving receipts for a specific user.
+///
public interface IReceiptService
{
- Task GetReceipts(DateTime from, ReceiptType type, int userId, int batchSize);
+ ///
+ /// Returns a flat, unsegmented list of receipts for the given user, optionally filtered by
+ /// . The returned list is sorted by event date descending.
+ ///
+ ///
+ /// The receipt type to include. Use to return every type.
+ ///
+ /// The database primary key of the user whose receipts are fetched.
+ /// A containing the matching receipt items.
+ Task GetReceipts(ReceiptTypeFilter type, int userId);
}
diff --git a/coffeecard/CoffeeCard.Library/Services/v2/ReceiptService.cs b/coffeecard/CoffeeCard.Library/Services/v2/ReceiptService.cs
index a3f57315..bf5308d4 100644
--- a/coffeecard/CoffeeCard.Library/Services/v2/ReceiptService.cs
+++ b/coffeecard/CoffeeCard.Library/Services/v2/ReceiptService.cs
@@ -10,97 +10,113 @@
namespace CoffeeCard.Library.Services.v2;
+///
+/// Implementation of that queries the database for receipts
+/// and returns a flat, merged, sorted list.
+///
public class ReceiptService : IReceiptService
{
private readonly ILogger _logger;
private readonly CoffeeCardContext _context;
+ ///
+ /// Initializes a new instance of .
+ ///
+ /// Logger instance.
+ /// EF Core database context.
public ReceiptService(ILogger logger, CoffeeCardContext context)
{
_logger = logger;
_context = context;
}
- public async Task GetReceipts(
- DateTime from,
- ReceiptType type,
- int userId,
- int batchSize
- )
+ ///
+ public async Task GetReceipts(ReceiptTypeFilter type, int userId)
{
- var ticketReceipts = type.HasFlag(ReceiptType.UsedTicket)
- ? await _context
- .Tickets.Where(t => t.OwnerId == userId)
- .Where(ticket => ticket.DateUsed < from)
- .Where(ticket => ticket.DateUsed != null)
- .OrderByDescending(ticket => ticket.DateCreated)
- .Take(batchSize)
- .Select(t => new UsedTicketReceipt
+ var all = new List();
+
+ if (type == ReceiptTypeFilter.All || type == ReceiptTypeFilter.Purchase)
+ {
+ var purchases = await _context
+ .Purchases.AsNoTracking()
+ .Where(p => p.PurchasedById == userId && p.Type != PurchaseType.Voucher)
+ .Select(p => new ReceiptListItem
{
- ProductName = t.Purchase.ProductName,
- SwipeDate = t.DateUsed.Value,
+ Id = "Purchase:" + p.Id,
+ Type = ReceiptType.Purchase,
+ EventDate = p.DateCreated,
+ Title = "Purchased " + p.NumberOfTickets + "x " + p.ProductName,
+ Amount = p.NumberOfTickets,
+ PriceDKK = p.Price,
+ TicketName = p.ProductName,
+ DrinkName = null,
})
- .ToListAsync()
- : [];
+ .ToListAsync();
+
+ all.AddRange(purchases);
+ }
- var voucherReceipts = type.HasFlag(ReceiptType.Voucher)
- ? await _context
- .Purchases.Where(p => p.PurchasedById == userId)
- .Where(purchase => purchase.DateCreated < from)
- .Where(p => p.Type == PurchaseType.Voucher)
- .OrderByDescending(purchase => purchase.DateCreated)
- .Take(batchSize)
- .Select(p => new VoucherReceipt
+ if (type == ReceiptTypeFilter.All || type == ReceiptTypeFilter.Voucher)
+ {
+ var vouchers = await _context
+ .Purchases.AsNoTracking()
+ .Where(p => p.PurchasedById == userId && p.Type == PurchaseType.Voucher)
+ .Select(p => new ReceiptListItem
{
- Code = p.Voucher.Code,
- Quantity = p.NumberOfTickets,
- ProductName = p.ProductName,
- RedeemDate = p.DateCreated,
+ Id = "Voucher:" + p.Id,
+ Type = ReceiptType.Voucher,
+ EventDate = p.DateCreated,
+ Title = "Redeemed " + p.NumberOfTickets + "x " + p.ProductName + " tickets",
+ Amount = p.NumberOfTickets,
+ PriceDKK = null,
+ TicketName = p.ProductName,
+ DrinkName = null,
})
- .ToListAsync()
- : [];
+ .ToListAsync();
+
+ all.AddRange(vouchers);
+ }
- var purchaseReceipts = type.HasFlag(ReceiptType.Purchase)
- ? await _context
- .Purchases.Where(p => p.PurchasedById == userId)
- .Where(purchase => purchase.DateCreated < from)
- .Where(p => p.Type != PurchaseType.Voucher)
- .OrderByDescending(purchase => purchase.DateCreated)
- .Take(batchSize)
- .Select(p => new PurchaseReceipt
+ if (type == ReceiptTypeFilter.All || type == ReceiptTypeFilter.UsedTicket)
+ {
+ var usedTickets = await _context
+ .Tickets.AsNoTracking()
+ .Where(t => t.OwnerId == userId && t.DateUsed != null)
+ .Select(t => new ReceiptListItem
{
- OrderId = Guid.Parse(p.OrderId),
- ProductName = p.ProductName,
- Quantity = p.NumberOfTickets,
- Status = p.Status,
- OrderDate = p.DateCreated,
- Price = p.Price,
+ Id = "UsedTicket:" + t.Id,
+ Type = ReceiptType.UsedTicket,
+ EventDate = t.DateUsed!.Value,
+ Title =
+ t.UsedOnMenuItem != null
+ ? "Swiped a " + t.UsedOnMenuItem.Name
+ : "Swiped a " + t.Purchase.ProductName + " ticket",
+ Amount = null,
+ PriceDKK = null,
+ TicketName = t.Purchase.ProductName,
+ DrinkName = t.UsedOnMenuItem != null ? t.UsedOnMenuItem.Name : null,
})
- .ToListAsync()
- : [];
+ .ToListAsync();
- List combinedReceipts =
- [
- .. purchaseReceipts,
- .. ticketReceipts,
- .. voucherReceipts,
- ];
- var receiptList = combinedReceipts
- .OrderByDescending(r => r.IssuingDate)
- .Take(batchSize)
+ all.AddRange(usedTickets);
+ }
+
+ var sorted = all.OrderByDescending(r => r.EventDate)
+ .ThenByDescending(r => ParseIdNumber(r.Id))
.ToList();
- // As they are ordered by date, the last index will represent the continuation token
- var continuationToken =
- receiptList.Count == 0 ? DateTime.UtcNow : receiptList[^1].IssuingDate;
- var encodedToken = Convert.ToBase64String(
- System.Text.Encoding.UTF8.GetBytes(continuationToken.ToString("O"))
- );
- return new ReceiptResponse
- {
- Receipts = receiptList,
- // As they are ordered by date, the last index will represent the continuation token
- ContinuationToken = encodedToken,
- };
+ return new ReceiptsResponse { Receipts = sorted };
+ }
+
+ ///
+ /// Parses the numeric entity-ID component from a composite receipt identifier such as
+ /// "Purchase:123" and returns it as an .
+ ///
+ /// The composite ID string.
+ /// The numeric database primary key embedded in .
+ private static int ParseIdNumber(string id)
+ {
+ var parts = id.Split(':');
+ return int.Parse(parts[1]);
}
}
diff --git a/coffeecard/CoffeeCard.Models/DataTransferObjects/v2/Receipts/ReceiptBase.cs b/coffeecard/CoffeeCard.Models/DataTransferObjects/v2/Receipts/ReceiptBase.cs
index 6eae6800..a1abda27 100644
--- a/coffeecard/CoffeeCard.Models/DataTransferObjects/v2/Receipts/ReceiptBase.cs
+++ b/coffeecard/CoffeeCard.Models/DataTransferObjects/v2/Receipts/ReceiptBase.cs
@@ -1,118 +1,98 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
-using System.Text.Json.Serialization;
-using CoffeeCard.Models.DataTransferObjects.v2.Purchase;
namespace CoffeeCard.Models.DataTransferObjects.v2.Receipts;
-public class ReceiptResponse
+///
+/// Discriminator values that identify the concrete type of a .
+/// Each value is mutually exclusive.
+///
+public enum ReceiptType
{
- ///
- /// The list of receipts for the user.
- ///
- [Required]
- public required List Receipts { get; set; }
+ /// A ticket-bundle purchase made via a payment method (e.g. MobilePay).
+ Purchase,
- ///
- /// Used as the "from" parameter in the next request to get the next batch of receipts.
- /// Is set to the date and time of the oldest receipt in the current batch.
- ///
- [Required]
- public required string ContinuationToken { get; set; }
+ /// Tickets issued by redeeming a voucher.
+ Voucher,
+
+ /// A single ticket consumed (swiped) by the user.
+ UsedTicket,
}
///
-/// Shared properties for all receipt types.
+/// Flat response wrapper returned by GET /api/v2/receipts.
+/// Contains all receipts matching the requested filter, sorted newest-first.
///
-[JsonPolymorphic(TypeDiscriminatorPropertyName = "Type")]
-[JsonDerivedType(typeof(PurchaseReceipt), "Purchase")]
-[JsonDerivedType(typeof(VoucherReceipt), "Voucher")]
-[JsonDerivedType(typeof(UsedTicketReceipt), "UsedTicket")]
-public abstract class ReceiptBase
+public class ReceiptsResponse
{
- [Required]
- public required string ProductName { get; set; }
-
///
- /// The date/time the receipt was issued (type-specific).
- /// Use this for sorting across all receipt types.
+ /// The flat list of receipts for the authenticated user, sorted by
+ /// descending.
///
- [JsonIgnore]
- public abstract DateTime IssuingDate { get; }
+ [Required]
+ public required List Receipts { get; set; }
}
-public class PurchaseReceipt : ReceiptBase
+///
+/// A single receipt entry in the flat receipt list.
+/// All fields are present on every item; nullable fields are null when not applicable for the
+/// receipt .
+///
+public class ReceiptListItem
{
///
- /// The amount of tickets issued by the purchase.
+ /// Composite string identifier in the format "TypeName:EntityId",
+ /// e.g. "Purchase:123", "Voucher:456", or "UsedTicket:789".
+ /// The numeric part is the entity's database primary key.
///
[Required]
- public required int Quantity { get; set; }
+ public required string Id { get; set; }
///
- /// The status of the purchase.
+ /// The discriminator type of this receipt entry.
///
[Required]
- public required PurchaseStatus Status { get; set; }
+ public required ReceiptType Type { get; set; }
///
- /// The date and time when the order was placed.
+ /// The canonical event date used for sorting across all receipt types.
+ /// For purchases and vouchers this is the order/redeem date; for used tickets it is the swipe date.
+ /// Expressed in UTC.
///
[Required]
- public required DateTime OrderDate { get; set; }
+ public required DateTime EventDate { get; set; }
///
- /// The total price of the purchase in kr.
+ /// Server-assembled, human-readable summary of this receipt entry, e.g.
+ /// "Purchased 10x Filter", "Redeemed 5x Filter tickets", or "Swiped a Filter Coffee".
///
[Required]
- public required int Price { get; set; }
+ public required string Title { get; set; }
///
- /// The unique identifier of the order associated with the purchase.
+ /// Number of tickets involved in this receipt.
+ /// Set for and ;
+ /// null for .
///
- [Required]
- public required Guid OrderId { get; set; }
+ public int? Amount { get; set; }
- [JsonIgnore]
- public override DateTime IssuingDate => OrderDate;
-}
-
-///
-/// Represents of tickets issued by a voucher.
-///
-public class VoucherReceipt : ReceiptBase
-{
///
- /// The code of the voucher.
+ /// Total price paid in Danish kroner (DKK).
+ /// Only set for ; null for all other types.
///
- [Required]
- public required string Code { get; set; }
-
- [Required]
- public required DateTime RedeemDate { get; set; }
+ public int? PriceDKK { get; set; }
///
- /// The amount of tickets issued by the purchase.
+ /// The name of the product or ticket type, e.g. "Filter".
///
[Required]
- public required int Quantity { get; set; }
+ public required string TicketName { get; set; }
- [JsonIgnore]
- public override DateTime IssuingDate => RedeemDate;
-}
-
-///
-/// Represents a receipt for a ticket being used by a swipe
-///
-public class UsedTicketReceipt : ReceiptBase
-{
///
- /// The date and time when the swipe was made.
+ /// The name of the drink or menu item the ticket was used on.
+ /// Only set for items where a menu item was recorded;
+ /// null for all other types and for used tickets with no associated menu item.
///
- [Required]
- public required DateTime SwipeDate { get; set; }
-
- [JsonIgnore]
- public override DateTime IssuingDate => SwipeDate;
+ public string? DrinkName { get; set; }
}
diff --git a/coffeecard/CoffeeCard.Models/DataTransferObjects/v2/Receipts/ReceiptsRequest.cs b/coffeecard/CoffeeCard.Models/DataTransferObjects/v2/Receipts/ReceiptsRequest.cs
index d579fee9..b7dd86f1 100644
--- a/coffeecard/CoffeeCard.Models/DataTransferObjects/v2/Receipts/ReceiptsRequest.cs
+++ b/coffeecard/CoffeeCard.Models/DataTransferObjects/v2/Receipts/ReceiptsRequest.cs
@@ -1,30 +1,33 @@
-using System;
-using System.ComponentModel.DataAnnotations;
-
namespace CoffeeCard.Models.DataTransferObjects.v2.Receipts;
///
-/// A paginated request for receipts based on using a continuation token
+/// Filter values for the type query parameter on GET /api/v2/receipts.
+/// (the default) returns every receipt type in a single merged list.
///
-public class ReceiptsRequest
+public enum ReceiptTypeFilter
{
- [Required]
- public required ReceiptType Type { get; set; }
+ /// Return all receipt types (purchases, vouchers, and used tickets).
+ All = 0,
- [Required]
- public int BatchSize { get; set; } = 20;
+ /// Return only purchase receipts.
+ Purchase,
- public required string? ContinuationToken { get; set; }
+ /// Return only voucher receipts.
+ Voucher,
+
+ /// Return only used-ticket receipts.
+ UsedTicket,
}
///
-/// The types of receipts that can be requested. Multiple types can be combined.
+/// Query-string parameters for GET /api/v2/receipts.
///
-[Flags]
-public enum ReceiptType
+public class ReceiptsRequest
{
- Purchase = 1,
- Voucher = 2,
- UsedTicket = 4,
- All = Purchase | Voucher | UsedTicket,
+ ///
+ /// The receipt type to include in the response.
+ /// Defaults to , which returns every type merged into a
+ /// single list sorted by event date descending.
+ ///
+ public ReceiptTypeFilter Type { get; set; } = ReceiptTypeFilter.All;
}
diff --git a/coffeecard/CoffeeCard.Tests.Integration/Controllers/v2/GetReceiptsTest.cs b/coffeecard/CoffeeCard.Tests.Integration/Controllers/v2/GetReceiptsTest.cs
index e33b0be8..d0e71b60 100644
--- a/coffeecard/CoffeeCard.Tests.Integration/Controllers/v2/GetReceiptsTest.cs
+++ b/coffeecard/CoffeeCard.Tests.Integration/Controllers/v2/GetReceiptsTest.cs
@@ -17,33 +17,35 @@ namespace CoffeeCard.Tests.Integration.Controllers.v2
public class GetReceiptsTest(CustomWebApplicationFactory factory)
: BaseIntegrationTest(factory)
{
+ // ── Authentication ────────────────────────────────────────────────────
+
[Fact]
public async Task GetReceipts_returns_401_when_not_authenticated()
{
RemoveRequestHeaders();
var exception = await Assert.ThrowsAsync(async () =>
- await CoffeeCardClientV2.Receipt_GetReceiptsAsync(ReceiptType.All, 20, null)
+ await CoffeeCardClientV2.Receipt_GetReceiptsAsync(ReceiptTypeFilter.All)
);
Assert.Equal(401, exception.StatusCode);
}
+ // ── Empty state ───────────────────────────────────────────────────────
+
[Fact]
public async Task GetReceipts_returns_empty_list_when_user_has_no_activity()
{
await GetAuthenticatedUserAsync();
- var response = await CoffeeCardClientV2.Receipt_GetReceiptsAsync(
- ReceiptType.All,
- 20,
- null
- );
+ var response = await CoffeeCardClientV2.Receipt_GetReceiptsAsync(ReceiptTypeFilter.All);
Assert.NotNull(response);
Assert.Empty(response.Receipts);
}
+ // ── Purchase receipts ─────────────────────────────────────────────────
+
[Fact]
public async Task GetReceipts_returns_purchase_receipt_for_completed_purchase()
{
@@ -66,25 +68,83 @@ public async Task GetReceipts_returns_purchase_receipt_for_completed_purchase()
await Context.SaveChangesAsync();
var response = await CoffeeCardClientV2.Receipt_GetReceiptsAsync(
- ReceiptType.Purchase,
- 20,
- null
+ ReceiptTypeFilter.Purchase
+ );
+
+ Assert.Single(response.Receipts);
+ var receipt = response.Receipts.First();
+ Assert.StartsWith("Purchase:", receipt.Id);
+ Assert.Equal(ReceiptType.Purchase, receipt.Type);
+ Assert.Equal(purchase.ProductName, receipt.TicketName);
+ Assert.Equal(purchase.NumberOfTickets, receipt.Amount);
+ Assert.Equal(purchase.Price, receipt.PriceDKK);
+ Assert.Null(receipt.DrinkName);
+ }
+
+ // ── Voucher receipts ──────────────────────────────────────────────────
+
+ [Fact]
+ public async Task GetReceipts_returns_voucher_receipt_for_voucher_purchase()
+ {
+ var user = await GetAuthenticatedUserAsync();
+ var redeemDate = new DateTime(2026, 1, 3, 9, 0, 0, DateTimeKind.Utc);
+
+ var product = ProductBuilder.Simple().Build();
+ await Context.Products.AddAsync(product);
+
+ var voucher = VoucherBuilder
+ .Simple()
+ .WithCode("TESTVOUCHER")
+ .WithProduct(product)
+ .WithUser(user)
+ .WithDateCreated(redeemDate)
+ .WithDateUsed(redeemDate)
+ .WithDescription(f => null)
+ .WithRequester(f => null)
+ .Build();
+
+ var purchase = PurchaseBuilder
+ .Simple()
+ .WithPurchasedBy(user)
+ .WithPrice(0)
+ .WithNumberOfTickets(5)
+ .WithStatus(PurchaseStatus.Completed)
+ .WithType(PurchaseType.Voucher)
+ .WithOrderId(Guid.NewGuid().ToString())
+ .WithDateCreated(redeemDate)
+ .WithTickets(new List())
+ .WithVoucher(voucher)
+ .Build();
+
+ await Context.Purchases.AddAsync(purchase);
+ await Context.SaveChangesAsync();
+
+ var response = await CoffeeCardClientV2.Receipt_GetReceiptsAsync(
+ ReceiptTypeFilter.Voucher
);
Assert.Single(response.Receipts);
- var receipt = Assert.IsType(response.Receipts.First());
- Assert.Equal(purchase.ProductName, receipt.ProductName);
- Assert.Equal(purchase.Price, receipt.Price);
- Assert.Equal(purchase.NumberOfTickets, receipt.Quantity);
- Assert.Equal(Guid.Parse(purchase.OrderId), receipt.OrderId);
+ var receipt = response.Receipts.First();
+ Assert.StartsWith("Voucher:", receipt.Id);
+ Assert.Equal(ReceiptType.Voucher, receipt.Type);
+ Assert.Equal(purchase.ProductName, receipt.TicketName);
+ Assert.Equal(purchase.NumberOfTickets, receipt.Amount);
+ Assert.Null(receipt.PriceDKK);
+ Assert.Null(receipt.DrinkName);
}
+ // ── UsedTicket receipts ───────────────────────────────────────────────
+
[Fact]
public async Task GetReceipts_returns_used_ticket_receipt_for_used_ticket()
{
var user = await GetAuthenticatedUserAsync();
var swipeDate = new Faker().Date.Past().ToUniversalTime();
+ var menuItem = MenuItemBuilder.Simple().Build();
+ await Context.MenuItems.AddAsync(menuItem);
+ await Context.SaveChangesAsync();
+
var purchase = PurchaseBuilder
.Simple()
.WithPurchasedBy(user)
@@ -96,6 +156,7 @@ public async Task GetReceipts_returns_used_ticket_receipt_for_used_ticket()
.WithStatus(TicketStatus.Used)
.WithDateUsed(swipeDate)
.WithOwner(user)
+ .WithUsedOnMenuItem(menuItem)
.Build(1)
)
.Build();
@@ -104,76 +165,62 @@ public async Task GetReceipts_returns_used_ticket_receipt_for_used_ticket()
await Context.SaveChangesAsync();
var response = await CoffeeCardClientV2.Receipt_GetReceiptsAsync(
- ReceiptType.UsedTicket,
- 20,
- null
+ ReceiptTypeFilter.UsedTicket
);
Assert.Single(response.Receipts);
- var receipt = Assert.IsType(response.Receipts.First());
- Assert.Equal(purchase.ProductName, receipt.ProductName);
-
- // We compare the string representations of the dates to avoid issues with precision loss (nanoseconds) when converting between DateTime and DateTimeOffset
- Assert.Equal(swipeDate.ToString("F"), receipt.SwipeDate.UtcDateTime.ToString("F"));
+ var receipt = response.Receipts.First();
+ Assert.StartsWith("UsedTicket:", receipt.Id);
+ Assert.Equal(ReceiptType.UsedTicket, receipt.Type);
+ Assert.Equal(menuItem.Name, receipt.DrinkName);
+ Assert.Null(receipt.Amount);
+ Assert.Null(receipt.PriceDKK);
}
[Fact]
- public async Task GetReceipts_returns_voucher_receipt_for_voucher_purchase()
+ public async Task GetReceipts_used_ticket_without_menu_item_has_null_drink_name()
{
var user = await GetAuthenticatedUserAsync();
- var redeemDate = new DateTime(2026, 1, 3, 9, 0, 0, DateTimeKind.Utc);
-
- var product = ProductBuilder.Simple().Build();
- await Context.Products.AddAsync(product);
-
- var voucher = VoucherBuilder
- .Simple()
- .WithCode("TESTVOUCHER")
- .WithProduct(product)
- .WithUser(user)
- .WithDateCreated(redeemDate)
- .WithDateUsed(redeemDate)
- .WithDescription(f => null)
- .WithRequester(f => null)
- .Build();
+ var swipeDate = new Faker().Date.Past().ToUniversalTime();
var purchase = PurchaseBuilder
.Simple()
.WithPurchasedBy(user)
- .WithPrice(0)
- .WithNumberOfTickets(5)
.WithStatus(PurchaseStatus.Completed)
- .WithType(PurchaseType.Voucher)
- .WithOrderId(Guid.NewGuid().ToString())
- .WithDateCreated(redeemDate)
- .WithTickets(new List())
- .WithVoucher(voucher)
+ .WithType(PurchaseType.Free)
+ .WithTickets(
+ TicketBuilder
+ .Simple()
+ .WithStatus(TicketStatus.Used)
+ .WithDateUsed(swipeDate)
+ .WithOwner(user)
+ .WithUsedOnMenuItem(_ => null)
+ .Build(1)
+ )
.Build();
await Context.Purchases.AddAsync(purchase);
await Context.SaveChangesAsync();
var response = await CoffeeCardClientV2.Receipt_GetReceiptsAsync(
- ReceiptType.Voucher,
- 20,
- null
+ ReceiptTypeFilter.UsedTicket
);
Assert.Single(response.Receipts);
- var receipt = Assert.IsType(response.Receipts.First());
- Assert.Equal(purchase.ProductName, receipt.ProductName);
- Assert.Equal("TESTVOUCHER", receipt.Code);
- Assert.Equal(purchase.NumberOfTickets, receipt.Quantity);
- Assert.Equal(redeemDate, receipt.RedeemDate.UtcDateTime);
+ Assert.Null(response.Receipts.First().DrinkName);
}
+ // ── User isolation ────────────────────────────────────────────────────
+
[Fact]
public async Task GetReceipts_only_returns_receipts_for_authenticated_user()
{
var user = await GetAuthenticatedUserAsync();
- var otherPurchase = PurchaseBuilder.Simple().Build();
+ // Another user's purchase — should NOT appear
+ var otherPurchase = PurchaseBuilder.Simple().WithType(PurchaseType.MobilePayV2).Build();
+ // This user's purchase
var myPurchase = PurchaseBuilder
.Simple()
.WithPurchasedBy(user)
@@ -184,183 +231,283 @@ public async Task GetReceipts_only_returns_receipts_for_authenticated_user()
await Context.SaveChangesAsync();
var response = await CoffeeCardClientV2.Receipt_GetReceiptsAsync(
- ReceiptType.Purchase,
- 20,
- null
+ ReceiptTypeFilter.Purchase
);
Assert.Single(response.Receipts);
- var receipt = Assert.IsType(response.Receipts.First());
- Assert.Equal(myPurchase.ProductName, receipt.ProductName);
+ Assert.Equal(myPurchase.ProductName, response.Receipts.First().TicketName);
}
+ // ── All-types filter ──────────────────────────────────────────────────
+
[Fact]
- public async Task GetReceipts_respects_batch_size()
+ public async Task GetReceipts_with_All_type_returns_all_receipt_types()
{
var user = await GetAuthenticatedUserAsync();
+ var baseDate = new DateTime(2026, 1, 10, 12, 0, 0, DateTimeKind.Utc);
- var purchases = PurchaseBuilder
+ // Purchase
+ var purchase = PurchaseBuilder
.Simple()
.WithPurchasedBy(user)
.WithStatus(PurchaseStatus.Completed)
.WithType(PurchaseType.MobilePayV2)
- .Build(5);
+ .WithDateCreated(baseDate)
+ .WithTickets(new List())
+ .WithVoucher(f => null)
+ .Build();
- await Context.Purchases.AddRangeAsync(purchases);
+ // Voucher purchase
+ var voucherPurchase = PurchaseBuilder
+ .Simple()
+ .WithPurchasedBy(user)
+ .WithStatus(PurchaseStatus.Completed)
+ .WithType(PurchaseType.Voucher)
+ .WithDateCreated(baseDate.AddHours(-1))
+ .WithTickets(new List())
+ .Build();
+ // Used ticket (no menu item)
+ var usedPurchase = PurchaseBuilder
+ .Simple()
+ .WithPurchasedBy(user)
+ .WithStatus(PurchaseStatus.Completed)
+ .WithType(PurchaseType.Free)
+ .WithDateCreated(baseDate.AddHours(-2))
+ .WithTickets(
+ TicketBuilder
+ .Simple()
+ .WithOwner(user)
+ .WithStatus(TicketStatus.Used)
+ .WithDateUsed(baseDate.AddHours(-2))
+ .WithUsedOnMenuItem(_ => null)
+ .Build(1)
+ )
+ .WithVoucher(f => null)
+ .Build();
+
+ await Context.Purchases.AddRangeAsync(purchase, voucherPurchase, usedPurchase);
await Context.SaveChangesAsync();
- var response = await CoffeeCardClientV2.Receipt_GetReceiptsAsync(
- ReceiptType.Purchase,
- 3,
- null
- );
+ var response = await CoffeeCardClientV2.Receipt_GetReceiptsAsync(ReceiptTypeFilter.All);
Assert.Equal(3, response.Receipts.Count);
+
+ var types = response.Receipts.Select(r => r.Type).ToHashSet();
+ Assert.Contains(ReceiptType.Purchase, types);
+ Assert.Contains(ReceiptType.Voucher, types);
+ Assert.Contains(ReceiptType.UsedTicket, types);
}
+ // ── Sort order ────────────────────────────────────────────────────────
+
[Fact]
- public async Task GetReceipts_default_batch_size_is_20()
+ public async Task GetReceipts_items_are_sorted_newest_first()
{
var user = await GetAuthenticatedUserAsync();
- var purchases = PurchaseBuilder
- .Simple()
- .WithPurchasedBy(user)
- .WithStatus(PurchaseStatus.Completed)
- .WithType(PurchaseType.MobilePayV2)
- .Build(25);
-
- await Context.Purchases.AddRangeAsync(purchases);
+ var dates = new[]
+ {
+ new DateTime(2026, 1, 3, 0, 0, 0, DateTimeKind.Utc),
+ new DateTime(2026, 1, 1, 0, 0, 0, DateTimeKind.Utc),
+ new DateTime(2026, 1, 2, 0, 0, 0, DateTimeKind.Utc),
+ };
+
+ foreach (var date in dates)
+ {
+ var p = PurchaseBuilder
+ .Simple()
+ .WithPurchasedBy(user)
+ .WithType(PurchaseType.MobilePayV2)
+ .WithStatus(PurchaseStatus.Completed)
+ .WithDateCreated(date)
+ .WithTickets(new List())
+ .WithVoucher(f => null)
+ .Build();
+
+ await Context.Purchases.AddAsync(p);
+ }
await Context.SaveChangesAsync();
var response = await CoffeeCardClientV2.Receipt_GetReceiptsAsync(
- ReceiptType.Purchase,
- null,
- null
+ ReceiptTypeFilter.Purchase
);
- Assert.Equal(20, response.Receipts.Count);
+ Assert.Equal(3, response.Receipts.Count);
+
+ var eventDates = response.Receipts.Select(r => r.EventDate).ToList();
+ for (var i = 0; i < eventDates.Count - 1; i++)
+ {
+ Assert.True(
+ eventDates[i] >= eventDates[i + 1],
+ $"Expected item {i} ({eventDates[i]}) to be >= item {i + 1} ({eventDates[i + 1]})"
+ );
+ }
}
+ // ── Type-specific filters ─────────────────────────────────────────────
+
[Fact]
- public async Task GetReceipts_with_continuation_token_returns_next_batch()
+ public async Task GetReceipts_purchase_filter_excludes_vouchers_and_tickets()
{
var user = await GetAuthenticatedUserAsync();
+ var baseDate = new DateTime(2026, 2, 1, 12, 0, 0, DateTimeKind.Utc);
- var purchases = PurchaseBuilder
+ var purchase = PurchaseBuilder
.Simple()
.WithPurchasedBy(user)
- .WithStatus(PurchaseStatus.Completed)
.WithType(PurchaseType.MobilePayV2)
- .Build(4);
+ .WithStatus(PurchaseStatus.Completed)
+ .WithDateCreated(baseDate)
+ .WithTickets(new List())
+ .WithVoucher(f => null)
+ .Build();
+
+ var voucherPurchase = PurchaseBuilder
+ .Simple()
+ .WithPurchasedBy(user)
+ .WithType(PurchaseType.Voucher)
+ .WithStatus(PurchaseStatus.Completed)
+ .WithDateCreated(baseDate.AddHours(-1))
+ .WithTickets(new List())
+ .Build();
- await Context.Purchases.AddRangeAsync(purchases);
+ var usedPurchase = PurchaseBuilder
+ .Simple()
+ .WithPurchasedBy(user)
+ .WithType(PurchaseType.Free)
+ .WithStatus(PurchaseStatus.Completed)
+ .WithDateCreated(baseDate.AddHours(-2))
+ .WithTickets(
+ TicketBuilder
+ .Simple()
+ .WithOwner(user)
+ .WithStatus(TicketStatus.Used)
+ .WithDateUsed(baseDate.AddHours(-2))
+ .WithUsedOnMenuItem(_ => null)
+ .Build(1)
+ )
+ .WithVoucher(f => null)
+ .Build();
+ await Context.Purchases.AddRangeAsync(purchase, voucherPurchase, usedPurchase);
await Context.SaveChangesAsync();
- // Get the first batch of 2
- var firstPage = await CoffeeCardClientV2.Receipt_GetReceiptsAsync(
- ReceiptType.Purchase,
- 2,
- null
- );
- Assert.Equal(2, firstPage.Receipts.Count);
- Assert.NotNull(firstPage.ContinuationToken);
-
- // Use the continuation token to get the next batch
- var secondPage = await CoffeeCardClientV2.Receipt_GetReceiptsAsync(
- ReceiptType.Purchase,
- 2,
- firstPage.ContinuationToken
+ var response = await CoffeeCardClientV2.Receipt_GetReceiptsAsync(
+ ReceiptTypeFilter.Purchase
);
- Assert.Equal(2, secondPage.Receipts.Count);
-
- // The two pages should not overlap
- var firstPageDates = firstPage
- .Receipts.Cast()
- .Select(r => r.OrderDate)
- .ToHashSet();
- var secondPageDates = secondPage
- .Receipts.Cast()
- .Select(r => r.OrderDate)
- .ToHashSet();
- Assert.Empty(firstPageDates.Intersect(secondPageDates));
+
+ Assert.All(response.Receipts, r => Assert.Equal(ReceiptType.Purchase, r.Type));
+ Assert.Single(response.Receipts);
}
[Fact]
- public async Task GetReceipts_with_All_type_returns_all_receipt_types()
+ public async Task GetReceipts_voucher_filter_excludes_purchases_and_tickets()
{
var user = await GetAuthenticatedUserAsync();
- var baseDate = new Faker().Date.Past();
+ var baseDate = new DateTime(2026, 2, 2, 12, 0, 0, DateTimeKind.Utc);
- // Add a purchase receipt
var purchase = PurchaseBuilder
.Simple()
.WithPurchasedBy(user)
- .WithStatus(PurchaseStatus.Completed)
.WithType(PurchaseType.MobilePayV2)
+ .WithStatus(PurchaseStatus.Completed)
.WithDateCreated(baseDate)
+ .WithTickets(new List())
+ .WithVoucher(f => null)
.Build();
- // Add a used ticket receipt
-
- var ticket = TicketBuilder
+ var voucherPurchase = PurchaseBuilder
.Simple()
- .WithOwner(user)
- .WithStatus(TicketStatus.Used)
- .WithDateUsed(baseDate.AddHours(-2))
+ .WithPurchasedBy(user)
+ .WithType(PurchaseType.Voucher)
+ .WithStatus(PurchaseStatus.Completed)
.WithDateCreated(baseDate.AddHours(-1))
+ .WithTickets(new List())
.Build();
- // Add a voucher receipt
- var product = ProductBuilder.Simple().Build();
- await Context.Products.AddAsync(product);
-
- var voucher = VoucherBuilder
+ var usedPurchase = PurchaseBuilder
.Simple()
- .WithUser(user)
- .WithPurchase(
- PurchaseBuilder
+ .WithPurchasedBy(user)
+ .WithType(PurchaseType.Free)
+ .WithStatus(PurchaseStatus.Completed)
+ .WithDateCreated(baseDate.AddHours(-2))
+ .WithTickets(
+ TicketBuilder
.Simple()
- .WithType(PurchaseType.Voucher)
- .WithPurchasedBy(user)
- .Build()
+ .WithOwner(user)
+ .WithStatus(TicketStatus.Used)
+ .WithDateUsed(baseDate.AddHours(-2))
+ .WithUsedOnMenuItem(_ => null)
+ .Build(1)
)
+ .WithVoucher(f => null)
.Build();
- await Context.Purchases.AddAsync(purchase);
- await Context.Tickets.AddAsync(ticket);
- await Context.Vouchers.AddAsync(voucher);
+ await Context.Purchases.AddRangeAsync(purchase, voucherPurchase, usedPurchase);
await Context.SaveChangesAsync();
var response = await CoffeeCardClientV2.Receipt_GetReceiptsAsync(
- ReceiptType.All,
- 20,
- null
+ ReceiptTypeFilter.Voucher
);
- Assert.Equal(3, response.Receipts.Count);
- Assert.Single(response.Receipts.OfType());
- Assert.Single(response.Receipts.OfType());
- Assert.Single(response.Receipts.OfType());
+ Assert.All(response.Receipts, r => Assert.Equal(ReceiptType.Voucher, r.Type));
+ Assert.Single(response.Receipts);
}
[Fact]
- public async Task GetReceipts_with_invalid_continuation_token_returns_400()
+ public async Task GetReceipts_usedticket_filter_excludes_purchases_and_vouchers()
{
- await GetAuthenticatedUserAsync();
+ var user = await GetAuthenticatedUserAsync();
+ var baseDate = new DateTime(2026, 2, 3, 12, 0, 0, DateTimeKind.Utc);
- var exception = await Assert.ThrowsAsync(async () =>
- await CoffeeCardClientV2.Receipt_GetReceiptsAsync(
- ReceiptType.All,
- 20,
- "not-a-valid-token"
+ var purchase = PurchaseBuilder
+ .Simple()
+ .WithPurchasedBy(user)
+ .WithType(PurchaseType.MobilePayV2)
+ .WithStatus(PurchaseStatus.Completed)
+ .WithDateCreated(baseDate)
+ .WithTickets(new List())
+ .WithVoucher(f => null)
+ .Build();
+
+ var voucherPurchase = PurchaseBuilder
+ .Simple()
+ .WithPurchasedBy(user)
+ .WithType(PurchaseType.Voucher)
+ .WithStatus(PurchaseStatus.Completed)
+ .WithDateCreated(baseDate.AddHours(-1))
+ .WithTickets(new List())
+ .Build();
+
+ var usedPurchase = PurchaseBuilder
+ .Simple()
+ .WithPurchasedBy(user)
+ .WithType(PurchaseType.Free)
+ .WithStatus(PurchaseStatus.Completed)
+ .WithDateCreated(baseDate.AddHours(-2))
+ .WithTickets(
+ TicketBuilder
+ .Simple()
+ .WithOwner(user)
+ .WithStatus(TicketStatus.Used)
+ .WithDateUsed(baseDate.AddHours(-2))
+ .WithUsedOnMenuItem(_ => null)
+ .Build(1)
)
+ .WithVoucher(f => null)
+ .Build();
+
+ await Context.Purchases.AddRangeAsync(purchase, voucherPurchase, usedPurchase);
+ await Context.SaveChangesAsync();
+
+ var response = await CoffeeCardClientV2.Receipt_GetReceiptsAsync(
+ ReceiptTypeFilter.UsedTicket
);
- Assert.Equal(400, exception.StatusCode);
+ Assert.All(response.Receipts, r => Assert.Equal(ReceiptType.UsedTicket, r.Type));
+ Assert.Single(response.Receipts);
}
}
}
diff --git a/coffeecard/CoffeeCard.WebApi/Controllers/v2/ReceiptController.cs b/coffeecard/CoffeeCard.WebApi/Controllers/v2/ReceiptController.cs
index 25758bdc..5302721a 100644
--- a/coffeecard/CoffeeCard.WebApi/Controllers/v2/ReceiptController.cs
+++ b/coffeecard/CoffeeCard.WebApi/Controllers/v2/ReceiptController.cs
@@ -1,16 +1,17 @@
-using System;
-using System.Buffers.Text;
-using System.Globalization;
using System.Threading.Tasks;
-using CoffeeCard.Common.Errors;
using CoffeeCard.Library.Services.v2;
using CoffeeCard.Library.Utils;
using CoffeeCard.Models.DataTransferObjects.v2.Receipts;
using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
namespace CoffeeCard.WebApi.Controllers.v2;
+///
+/// Endpoints for retrieving the authenticated user's receipts.
+/// Receipts include completed purchases, redeemed vouchers, and swiped tickets.
+///
[ApiVersion("2")]
[Route("api/v{version:apiVersion}/receipts")]
[ApiController]
@@ -21,10 +22,10 @@ public class ReceiptController : ControllerBase
private readonly ClaimsUtilities _claimsUtilities;
///
- /// Contains endpoints for retrieving receipts for the authenticated user. This includes all purchases, swiped tickets, and used vouchers
+ /// Initializes a new instance of the class.
///
- ///
- ///
+ /// Receipt service.
+ /// Helper for resolving the authenticated user from claims.
public ReceiptController(IReceiptService receiptService, ClaimsUtilities claimsUtilities)
{
_receiptService = receiptService;
@@ -32,46 +33,22 @@ public ReceiptController(IReceiptService receiptService, ClaimsUtilities claimsU
}
///
- /// Retrieve all receipts for the authenticated user
- /// This includes all purchases, swiped tickets, and used vouchers
+ /// Retrieve a flat list of receipts for the authenticated user.
+ /// Pass type to filter by receipt kind; omit it (or pass All) to get every type
+ /// merged into a single list sorted by event date descending.
///
- /// All users receipts
- public async Task> GetReceipts(
+ /// Query parameters specifying the optional type filter.
+ /// The matching receipts, sorted newest-first.
+ /// Invalid or missing authentication credentials.
+ [HttpGet]
+ [ProducesResponseType(typeof(ReceiptsResponse), StatusCodes.Status200OK)]
+ [ProducesResponseType(typeof(void), StatusCodes.Status401Unauthorized)]
+ public async Task> GetReceipts(
[FromQuery] ReceiptsRequest request
)
{
var user = await _claimsUtilities.ValidateAndReturnUserFromClaimAsync(User.Claims);
-
- DateTime from;
- if (request.ContinuationToken == null)
- {
- from = DateTime.UtcNow;
- }
- else
- {
- try
- {
- var bytes = Convert.FromBase64String(request.ContinuationToken);
- var utf8String = System.Text.Encoding.UTF8.GetString(bytes);
- from = DateTime.ParseExact(
- utf8String,
- "O",
- CultureInfo.InvariantCulture,
- DateTimeStyles.RoundtripKind
- );
- }
- catch (FormatException)
- {
- throw new BadRequestException("Invalid continuation token");
- }
- }
- var receipts = await _receiptService.GetReceipts(
- from,
- request.Type,
- user.Id,
- request.BatchSize
- );
-
- return Ok(receipts);
+ var result = await _receiptService.GetReceipts(request.Type, user.Id);
+ return Ok(result);
}
}
From 59eb0e8073b373abbe2e852ab6c0f09888efa8a4 Mon Sep 17 00:00:00 2001
From: Omid Marfavi <21163286+marfavi@users.noreply.github.com>
Date: Sun, 3 May 2026 16:41:46 +0200
Subject: [PATCH 2/7] Split ReceiptBase into multiple files
---
.../{ReceiptBase.cs => ReceiptListItem.cs} | 31 -------------------
.../v2/Receipts/ReceiptType.cs | 17 ++++++++++
.../v2/Receipts/ReceiptsResponse.cs | 18 +++++++++++
3 files changed, 35 insertions(+), 31 deletions(-)
rename coffeecard/CoffeeCard.Models/DataTransferObjects/v2/Receipts/{ReceiptBase.cs => ReceiptListItem.cs} (71%)
create mode 100644 coffeecard/CoffeeCard.Models/DataTransferObjects/v2/Receipts/ReceiptType.cs
create mode 100644 coffeecard/CoffeeCard.Models/DataTransferObjects/v2/Receipts/ReceiptsResponse.cs
diff --git a/coffeecard/CoffeeCard.Models/DataTransferObjects/v2/Receipts/ReceiptBase.cs b/coffeecard/CoffeeCard.Models/DataTransferObjects/v2/Receipts/ReceiptListItem.cs
similarity index 71%
rename from coffeecard/CoffeeCard.Models/DataTransferObjects/v2/Receipts/ReceiptBase.cs
rename to coffeecard/CoffeeCard.Models/DataTransferObjects/v2/Receipts/ReceiptListItem.cs
index a1abda27..8ac34806 100644
--- a/coffeecard/CoffeeCard.Models/DataTransferObjects/v2/Receipts/ReceiptBase.cs
+++ b/coffeecard/CoffeeCard.Models/DataTransferObjects/v2/Receipts/ReceiptListItem.cs
@@ -1,39 +1,8 @@
using System;
-using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
namespace CoffeeCard.Models.DataTransferObjects.v2.Receipts;
-///
-/// Discriminator values that identify the concrete type of a .
-/// Each value is mutually exclusive.
-///
-public enum ReceiptType
-{
- /// A ticket-bundle purchase made via a payment method (e.g. MobilePay).
- Purchase,
-
- /// Tickets issued by redeeming a voucher.
- Voucher,
-
- /// A single ticket consumed (swiped) by the user.
- UsedTicket,
-}
-
-///
-/// Flat response wrapper returned by GET /api/v2/receipts.
-/// Contains all receipts matching the requested filter, sorted newest-first.
-///
-public class ReceiptsResponse
-{
- ///
- /// The flat list of receipts for the authenticated user, sorted by
- /// descending.
- ///
- [Required]
- public required List Receipts { get; set; }
-}
-
///
/// A single receipt entry in the flat receipt list.
/// All fields are present on every item; nullable fields are null when not applicable for the
diff --git a/coffeecard/CoffeeCard.Models/DataTransferObjects/v2/Receipts/ReceiptType.cs b/coffeecard/CoffeeCard.Models/DataTransferObjects/v2/Receipts/ReceiptType.cs
new file mode 100644
index 00000000..4f23a66b
--- /dev/null
+++ b/coffeecard/CoffeeCard.Models/DataTransferObjects/v2/Receipts/ReceiptType.cs
@@ -0,0 +1,17 @@
+namespace CoffeeCard.Models.DataTransferObjects.v2.Receipts;
+
+///
+/// Discriminator values that identify the concrete type of a .
+/// Each value is mutually exclusive.
+///
+public enum ReceiptType
+{
+ /// A ticket-bundle purchase made via a payment method (e.g. MobilePay).
+ Purchase,
+
+ /// Tickets issued by redeeming a voucher.
+ Voucher,
+
+ /// A single ticket consumed (swiped) by the user.
+ UsedTicket,
+}
diff --git a/coffeecard/CoffeeCard.Models/DataTransferObjects/v2/Receipts/ReceiptsResponse.cs b/coffeecard/CoffeeCard.Models/DataTransferObjects/v2/Receipts/ReceiptsResponse.cs
new file mode 100644
index 00000000..28134042
--- /dev/null
+++ b/coffeecard/CoffeeCard.Models/DataTransferObjects/v2/Receipts/ReceiptsResponse.cs
@@ -0,0 +1,18 @@
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+
+namespace CoffeeCard.Models.DataTransferObjects.v2.Receipts;
+
+///
+/// Flat response wrapper returned by GET /api/v2/receipts.
+/// Contains all receipts matching the requested filter, sorted newest-first.
+///
+public class ReceiptsResponse
+{
+ ///
+ /// The flat list of receipts for the authenticated user, sorted by
+ /// descending.
+ ///
+ [Required]
+ public required List Receipts { get; set; }
+}
From 4ba0a9041d8d6375645edf931cff1dfaa89bb8bc Mon Sep 17 00:00:00 2001
From: Omid Marfavi <21163286+marfavi@users.noreply.github.com>
Date: Sun, 3 May 2026 19:33:02 +0200
Subject: [PATCH 3/7] Exclude Free purchases from receipt retrieval
---
coffeecard/CoffeeCard.Library/Services/v2/ReceiptService.cs | 6 +++++-
1 file changed, 5 insertions(+), 1 deletion(-)
diff --git a/coffeecard/CoffeeCard.Library/Services/v2/ReceiptService.cs b/coffeecard/CoffeeCard.Library/Services/v2/ReceiptService.cs
index bf5308d4..f9c52b19 100644
--- a/coffeecard/CoffeeCard.Library/Services/v2/ReceiptService.cs
+++ b/coffeecard/CoffeeCard.Library/Services/v2/ReceiptService.cs
@@ -39,7 +39,11 @@ public async Task GetReceipts(ReceiptTypeFilter type, int user
{
var purchases = await _context
.Purchases.AsNoTracking()
- .Where(p => p.PurchasedById == userId && p.Type != PurchaseType.Voucher)
+ .Where(p =>
+ p.PurchasedById == userId
+ && p.Type != PurchaseType.Voucher
+ && p.Type != PurchaseType.Free
+ )
.Select(p => new ReceiptListItem
{
Id = "Purchase:" + p.Id,
From 1ad0aaa305f3590aa00a2fd63c86622c3001db83 Mon Sep 17 00:00:00 2001
From: Omid Marfavi <21163286+marfavi@users.noreply.github.com>
Date: Sun, 3 May 2026 20:12:53 +0200
Subject: [PATCH 4/7] Remove type filtering from endpoint
---
.../Services/v2/IReceiptService.cs | 11 +-
.../Services/v2/ReceiptService.cs | 121 +++++------
.../v2/Receipts/ReceiptsRequest.cs | 33 ---
.../Controllers/v2/GetReceiptsTest.cs | 194 +-----------------
.../Controllers/v2/ReceiptController.cs | 12 +-
5 files changed, 73 insertions(+), 298 deletions(-)
delete mode 100644 coffeecard/CoffeeCard.Models/DataTransferObjects/v2/Receipts/ReceiptsRequest.cs
diff --git a/coffeecard/CoffeeCard.Library/Services/v2/IReceiptService.cs b/coffeecard/CoffeeCard.Library/Services/v2/IReceiptService.cs
index 88fbc54b..802a2f82 100644
--- a/coffeecard/CoffeeCard.Library/Services/v2/IReceiptService.cs
+++ b/coffeecard/CoffeeCard.Library/Services/v2/IReceiptService.cs
@@ -9,13 +9,10 @@ namespace CoffeeCard.Library.Services.v2;
public interface IReceiptService
{
///
- /// Returns a flat, unsegmented list of receipts for the given user, optionally filtered by
- /// . The returned list is sorted by event date descending.
+ /// Returns a list of receipts for the given user,
+ /// sorted by most recent event first.
///
- ///
- /// The receipt type to include. Use to return every type.
- ///
/// The database primary key of the user whose receipts are fetched.
- /// A containing the matching receipt items.
- Task GetReceipts(ReceiptTypeFilter type, int userId);
+ /// A containing the receipt items.
+ Task GetReceipts(int userId);
}
diff --git a/coffeecard/CoffeeCard.Library/Services/v2/ReceiptService.cs b/coffeecard/CoffeeCard.Library/Services/v2/ReceiptService.cs
index f9c52b19..1a3c94fb 100644
--- a/coffeecard/CoffeeCard.Library/Services/v2/ReceiptService.cs
+++ b/coffeecard/CoffeeCard.Library/Services/v2/ReceiptService.cs
@@ -31,79 +31,70 @@ public ReceiptService(ILogger logger, CoffeeCardContext context)
}
///
- public async Task GetReceipts(ReceiptTypeFilter type, int userId)
+ public async Task GetReceipts(int userId)
{
var all = new List();
- if (type == ReceiptTypeFilter.All || type == ReceiptTypeFilter.Purchase)
- {
- var purchases = await _context
- .Purchases.AsNoTracking()
- .Where(p =>
- p.PurchasedById == userId
- && p.Type != PurchaseType.Voucher
- && p.Type != PurchaseType.Free
- )
- .Select(p => new ReceiptListItem
- {
- Id = "Purchase:" + p.Id,
- Type = ReceiptType.Purchase,
- EventDate = p.DateCreated,
- Title = "Purchased " + p.NumberOfTickets + "x " + p.ProductName,
- Amount = p.NumberOfTickets,
- PriceDKK = p.Price,
- TicketName = p.ProductName,
- DrinkName = null,
- })
- .ToListAsync();
+ var purchases = await _context
+ .Purchases.AsNoTracking()
+ .Where(p =>
+ p.PurchasedById == userId
+ && p.Type != PurchaseType.Voucher
+ && p.Type != PurchaseType.Free
+ )
+ .Select(p => new ReceiptListItem
+ {
+ Id = "Purchase:" + p.Id,
+ Type = ReceiptType.Purchase,
+ EventDate = p.DateCreated,
+ Title = "Purchased " + p.NumberOfTickets + "x " + p.ProductName,
+ Amount = p.NumberOfTickets,
+ PriceDKK = p.Price,
+ TicketName = p.ProductName,
+ DrinkName = null,
+ })
+ .ToListAsync();
- all.AddRange(purchases);
- }
+ all.AddRange(purchases);
- if (type == ReceiptTypeFilter.All || type == ReceiptTypeFilter.Voucher)
- {
- var vouchers = await _context
- .Purchases.AsNoTracking()
- .Where(p => p.PurchasedById == userId && p.Type == PurchaseType.Voucher)
- .Select(p => new ReceiptListItem
- {
- Id = "Voucher:" + p.Id,
- Type = ReceiptType.Voucher,
- EventDate = p.DateCreated,
- Title = "Redeemed " + p.NumberOfTickets + "x " + p.ProductName + " tickets",
- Amount = p.NumberOfTickets,
- PriceDKK = null,
- TicketName = p.ProductName,
- DrinkName = null,
- })
- .ToListAsync();
+ var vouchers = await _context
+ .Purchases.AsNoTracking()
+ .Where(p => p.PurchasedById == userId && p.Type == PurchaseType.Voucher)
+ .Select(p => new ReceiptListItem
+ {
+ Id = "Voucher:" + p.Id,
+ Type = ReceiptType.Voucher,
+ EventDate = p.DateCreated,
+ Title = "Redeemed " + p.NumberOfTickets + "x " + p.ProductName + " tickets",
+ Amount = p.NumberOfTickets,
+ PriceDKK = null,
+ TicketName = p.ProductName,
+ DrinkName = null,
+ })
+ .ToListAsync();
- all.AddRange(vouchers);
- }
+ all.AddRange(vouchers);
- if (type == ReceiptTypeFilter.All || type == ReceiptTypeFilter.UsedTicket)
- {
- var usedTickets = await _context
- .Tickets.AsNoTracking()
- .Where(t => t.OwnerId == userId && t.DateUsed != null)
- .Select(t => new ReceiptListItem
- {
- Id = "UsedTicket:" + t.Id,
- Type = ReceiptType.UsedTicket,
- EventDate = t.DateUsed!.Value,
- Title =
- t.UsedOnMenuItem != null
- ? "Swiped a " + t.UsedOnMenuItem.Name
- : "Swiped a " + t.Purchase.ProductName + " ticket",
- Amount = null,
- PriceDKK = null,
- TicketName = t.Purchase.ProductName,
- DrinkName = t.UsedOnMenuItem != null ? t.UsedOnMenuItem.Name : null,
- })
- .ToListAsync();
+ var usedTickets = await _context
+ .Tickets.AsNoTracking()
+ .Where(t => t.OwnerId == userId && t.DateUsed != null)
+ .Select(t => new ReceiptListItem
+ {
+ Id = "UsedTicket:" + t.Id,
+ Type = ReceiptType.UsedTicket,
+ EventDate = t.DateUsed!.Value,
+ Title =
+ t.UsedOnMenuItem != null
+ ? "Swiped a " + t.UsedOnMenuItem.Name
+ : "Swiped a " + t.Purchase.ProductName + " ticket",
+ Amount = null,
+ PriceDKK = null,
+ TicketName = t.Purchase.ProductName,
+ DrinkName = t.UsedOnMenuItem != null ? t.UsedOnMenuItem.Name : null,
+ })
+ .ToListAsync();
- all.AddRange(usedTickets);
- }
+ all.AddRange(usedTickets);
var sorted = all.OrderByDescending(r => r.EventDate)
.ThenByDescending(r => ParseIdNumber(r.Id))
diff --git a/coffeecard/CoffeeCard.Models/DataTransferObjects/v2/Receipts/ReceiptsRequest.cs b/coffeecard/CoffeeCard.Models/DataTransferObjects/v2/Receipts/ReceiptsRequest.cs
deleted file mode 100644
index b7dd86f1..00000000
--- a/coffeecard/CoffeeCard.Models/DataTransferObjects/v2/Receipts/ReceiptsRequest.cs
+++ /dev/null
@@ -1,33 +0,0 @@
-namespace CoffeeCard.Models.DataTransferObjects.v2.Receipts;
-
-///
-/// Filter values for the type query parameter on GET /api/v2/receipts.
-/// (the default) returns every receipt type in a single merged list.
-///
-public enum ReceiptTypeFilter
-{
- /// Return all receipt types (purchases, vouchers, and used tickets).
- All = 0,
-
- /// Return only purchase receipts.
- Purchase,
-
- /// Return only voucher receipts.
- Voucher,
-
- /// Return only used-ticket receipts.
- UsedTicket,
-}
-
-///
-/// Query-string parameters for GET /api/v2/receipts.
-///
-public class ReceiptsRequest
-{
- ///
- /// The receipt type to include in the response.
- /// Defaults to , which returns every type merged into a
- /// single list sorted by event date descending.
- ///
- public ReceiptTypeFilter Type { get; set; } = ReceiptTypeFilter.All;
-}
diff --git a/coffeecard/CoffeeCard.Tests.Integration/Controllers/v2/GetReceiptsTest.cs b/coffeecard/CoffeeCard.Tests.Integration/Controllers/v2/GetReceiptsTest.cs
index d0e71b60..fb896353 100644
--- a/coffeecard/CoffeeCard.Tests.Integration/Controllers/v2/GetReceiptsTest.cs
+++ b/coffeecard/CoffeeCard.Tests.Integration/Controllers/v2/GetReceiptsTest.cs
@@ -25,7 +25,7 @@ public async Task GetReceipts_returns_401_when_not_authenticated()
RemoveRequestHeaders();
var exception = await Assert.ThrowsAsync(async () =>
- await CoffeeCardClientV2.Receipt_GetReceiptsAsync(ReceiptTypeFilter.All)
+ await CoffeeCardClientV2.Receipt_GetReceiptsAsync()
);
Assert.Equal(401, exception.StatusCode);
@@ -38,7 +38,7 @@ public async Task GetReceipts_returns_empty_list_when_user_has_no_activity()
{
await GetAuthenticatedUserAsync();
- var response = await CoffeeCardClientV2.Receipt_GetReceiptsAsync(ReceiptTypeFilter.All);
+ var response = await CoffeeCardClientV2.Receipt_GetReceiptsAsync();
Assert.NotNull(response);
Assert.Empty(response.Receipts);
@@ -67,9 +67,7 @@ public async Task GetReceipts_returns_purchase_receipt_for_completed_purchase()
await Context.Purchases.AddAsync(purchase);
await Context.SaveChangesAsync();
- var response = await CoffeeCardClientV2.Receipt_GetReceiptsAsync(
- ReceiptTypeFilter.Purchase
- );
+ var response = await CoffeeCardClientV2.Receipt_GetReceiptsAsync();
Assert.Single(response.Receipts);
var receipt = response.Receipts.First();
@@ -119,9 +117,7 @@ public async Task GetReceipts_returns_voucher_receipt_for_voucher_purchase()
await Context.Purchases.AddAsync(purchase);
await Context.SaveChangesAsync();
- var response = await CoffeeCardClientV2.Receipt_GetReceiptsAsync(
- ReceiptTypeFilter.Voucher
- );
+ var response = await CoffeeCardClientV2.Receipt_GetReceiptsAsync();
Assert.Single(response.Receipts);
var receipt = response.Receipts.First();
@@ -164,9 +160,7 @@ public async Task GetReceipts_returns_used_ticket_receipt_for_used_ticket()
await Context.Purchases.AddAsync(purchase);
await Context.SaveChangesAsync();
- var response = await CoffeeCardClientV2.Receipt_GetReceiptsAsync(
- ReceiptTypeFilter.UsedTicket
- );
+ var response = await CoffeeCardClientV2.Receipt_GetReceiptsAsync();
Assert.Single(response.Receipts);
var receipt = response.Receipts.First();
@@ -202,9 +196,7 @@ public async Task GetReceipts_used_ticket_without_menu_item_has_null_drink_name(
await Context.Purchases.AddAsync(purchase);
await Context.SaveChangesAsync();
- var response = await CoffeeCardClientV2.Receipt_GetReceiptsAsync(
- ReceiptTypeFilter.UsedTicket
- );
+ var response = await CoffeeCardClientV2.Receipt_GetReceiptsAsync();
Assert.Single(response.Receipts);
Assert.Null(response.Receipts.First().DrinkName);
@@ -230,9 +222,7 @@ public async Task GetReceipts_only_returns_receipts_for_authenticated_user()
await Context.Purchases.AddRangeAsync(otherPurchase, myPurchase);
await Context.SaveChangesAsync();
- var response = await CoffeeCardClientV2.Receipt_GetReceiptsAsync(
- ReceiptTypeFilter.Purchase
- );
+ var response = await CoffeeCardClientV2.Receipt_GetReceiptsAsync();
Assert.Single(response.Receipts);
Assert.Equal(myPurchase.ProductName, response.Receipts.First().TicketName);
@@ -289,7 +279,7 @@ public async Task GetReceipts_with_All_type_returns_all_receipt_types()
await Context.Purchases.AddRangeAsync(purchase, voucherPurchase, usedPurchase);
await Context.SaveChangesAsync();
- var response = await CoffeeCardClientV2.Receipt_GetReceiptsAsync(ReceiptTypeFilter.All);
+ var response = await CoffeeCardClientV2.Receipt_GetReceiptsAsync();
Assert.Equal(3, response.Receipts.Count);
@@ -330,9 +320,7 @@ public async Task GetReceipts_items_are_sorted_newest_first()
await Context.SaveChangesAsync();
- var response = await CoffeeCardClientV2.Receipt_GetReceiptsAsync(
- ReceiptTypeFilter.Purchase
- );
+ var response = await CoffeeCardClientV2.Receipt_GetReceiptsAsync();
Assert.Equal(3, response.Receipts.Count);
@@ -345,169 +333,5 @@ public async Task GetReceipts_items_are_sorted_newest_first()
);
}
}
-
- // ── Type-specific filters ─────────────────────────────────────────────
-
- [Fact]
- public async Task GetReceipts_purchase_filter_excludes_vouchers_and_tickets()
- {
- var user = await GetAuthenticatedUserAsync();
- var baseDate = new DateTime(2026, 2, 1, 12, 0, 0, DateTimeKind.Utc);
-
- var purchase = PurchaseBuilder
- .Simple()
- .WithPurchasedBy(user)
- .WithType(PurchaseType.MobilePayV2)
- .WithStatus(PurchaseStatus.Completed)
- .WithDateCreated(baseDate)
- .WithTickets(new List())
- .WithVoucher(f => null)
- .Build();
-
- var voucherPurchase = PurchaseBuilder
- .Simple()
- .WithPurchasedBy(user)
- .WithType(PurchaseType.Voucher)
- .WithStatus(PurchaseStatus.Completed)
- .WithDateCreated(baseDate.AddHours(-1))
- .WithTickets(new List())
- .Build();
-
- var usedPurchase = PurchaseBuilder
- .Simple()
- .WithPurchasedBy(user)
- .WithType(PurchaseType.Free)
- .WithStatus(PurchaseStatus.Completed)
- .WithDateCreated(baseDate.AddHours(-2))
- .WithTickets(
- TicketBuilder
- .Simple()
- .WithOwner(user)
- .WithStatus(TicketStatus.Used)
- .WithDateUsed(baseDate.AddHours(-2))
- .WithUsedOnMenuItem(_ => null)
- .Build(1)
- )
- .WithVoucher(f => null)
- .Build();
-
- await Context.Purchases.AddRangeAsync(purchase, voucherPurchase, usedPurchase);
- await Context.SaveChangesAsync();
-
- var response = await CoffeeCardClientV2.Receipt_GetReceiptsAsync(
- ReceiptTypeFilter.Purchase
- );
-
- Assert.All(response.Receipts, r => Assert.Equal(ReceiptType.Purchase, r.Type));
- Assert.Single(response.Receipts);
- }
-
- [Fact]
- public async Task GetReceipts_voucher_filter_excludes_purchases_and_tickets()
- {
- var user = await GetAuthenticatedUserAsync();
- var baseDate = new DateTime(2026, 2, 2, 12, 0, 0, DateTimeKind.Utc);
-
- var purchase = PurchaseBuilder
- .Simple()
- .WithPurchasedBy(user)
- .WithType(PurchaseType.MobilePayV2)
- .WithStatus(PurchaseStatus.Completed)
- .WithDateCreated(baseDate)
- .WithTickets(new List())
- .WithVoucher(f => null)
- .Build();
-
- var voucherPurchase = PurchaseBuilder
- .Simple()
- .WithPurchasedBy(user)
- .WithType(PurchaseType.Voucher)
- .WithStatus(PurchaseStatus.Completed)
- .WithDateCreated(baseDate.AddHours(-1))
- .WithTickets(new List())
- .Build();
-
- var usedPurchase = PurchaseBuilder
- .Simple()
- .WithPurchasedBy(user)
- .WithType(PurchaseType.Free)
- .WithStatus(PurchaseStatus.Completed)
- .WithDateCreated(baseDate.AddHours(-2))
- .WithTickets(
- TicketBuilder
- .Simple()
- .WithOwner(user)
- .WithStatus(TicketStatus.Used)
- .WithDateUsed(baseDate.AddHours(-2))
- .WithUsedOnMenuItem(_ => null)
- .Build(1)
- )
- .WithVoucher(f => null)
- .Build();
-
- await Context.Purchases.AddRangeAsync(purchase, voucherPurchase, usedPurchase);
- await Context.SaveChangesAsync();
-
- var response = await CoffeeCardClientV2.Receipt_GetReceiptsAsync(
- ReceiptTypeFilter.Voucher
- );
-
- Assert.All(response.Receipts, r => Assert.Equal(ReceiptType.Voucher, r.Type));
- Assert.Single(response.Receipts);
- }
-
- [Fact]
- public async Task GetReceipts_usedticket_filter_excludes_purchases_and_vouchers()
- {
- var user = await GetAuthenticatedUserAsync();
- var baseDate = new DateTime(2026, 2, 3, 12, 0, 0, DateTimeKind.Utc);
-
- var purchase = PurchaseBuilder
- .Simple()
- .WithPurchasedBy(user)
- .WithType(PurchaseType.MobilePayV2)
- .WithStatus(PurchaseStatus.Completed)
- .WithDateCreated(baseDate)
- .WithTickets(new List())
- .WithVoucher(f => null)
- .Build();
-
- var voucherPurchase = PurchaseBuilder
- .Simple()
- .WithPurchasedBy(user)
- .WithType(PurchaseType.Voucher)
- .WithStatus(PurchaseStatus.Completed)
- .WithDateCreated(baseDate.AddHours(-1))
- .WithTickets(new List())
- .Build();
-
- var usedPurchase = PurchaseBuilder
- .Simple()
- .WithPurchasedBy(user)
- .WithType(PurchaseType.Free)
- .WithStatus(PurchaseStatus.Completed)
- .WithDateCreated(baseDate.AddHours(-2))
- .WithTickets(
- TicketBuilder
- .Simple()
- .WithOwner(user)
- .WithStatus(TicketStatus.Used)
- .WithDateUsed(baseDate.AddHours(-2))
- .WithUsedOnMenuItem(_ => null)
- .Build(1)
- )
- .WithVoucher(f => null)
- .Build();
-
- await Context.Purchases.AddRangeAsync(purchase, voucherPurchase, usedPurchase);
- await Context.SaveChangesAsync();
-
- var response = await CoffeeCardClientV2.Receipt_GetReceiptsAsync(
- ReceiptTypeFilter.UsedTicket
- );
-
- Assert.All(response.Receipts, r => Assert.Equal(ReceiptType.UsedTicket, r.Type));
- Assert.Single(response.Receipts);
- }
}
}
diff --git a/coffeecard/CoffeeCard.WebApi/Controllers/v2/ReceiptController.cs b/coffeecard/CoffeeCard.WebApi/Controllers/v2/ReceiptController.cs
index 5302721a..286f48a4 100644
--- a/coffeecard/CoffeeCard.WebApi/Controllers/v2/ReceiptController.cs
+++ b/coffeecard/CoffeeCard.WebApi/Controllers/v2/ReceiptController.cs
@@ -33,22 +33,18 @@ public ReceiptController(IReceiptService receiptService, ClaimsUtilities claimsU
}
///
- /// Retrieve a flat list of receipts for the authenticated user.
- /// Pass type to filter by receipt kind; omit it (or pass All) to get every type
- /// merged into a single list sorted by event date descending.
+ /// Retrieve a list of receipts for the authenticated user,
+ /// sorted by most recent event first.
///
- /// Query parameters specifying the optional type filter.
/// The matching receipts, sorted newest-first.
/// Invalid or missing authentication credentials.
[HttpGet]
[ProducesResponseType(typeof(ReceiptsResponse), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(void), StatusCodes.Status401Unauthorized)]
- public async Task> GetReceipts(
- [FromQuery] ReceiptsRequest request
- )
+ public async Task> GetReceipts()
{
var user = await _claimsUtilities.ValidateAndReturnUserFromClaimAsync(User.Claims);
- var result = await _receiptService.GetReceipts(request.Type, user.Id);
+ var result = await _receiptService.GetReceipts(user.Id);
return Ok(result);
}
}
From b63769bdcdf256f45a4c3369b3cbaba3f482caff Mon Sep 17 00:00:00 2001
From: Omid Marfavi <21163286+marfavi@users.noreply.github.com>
Date: Sun, 3 May 2026 20:36:24 +0200
Subject: [PATCH 5/7] Rename ReceiptController -> ReceiptsController
---
.../v2/{ReceiptController.cs => ReceiptsController.cs} | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
rename coffeecard/CoffeeCard.WebApi/Controllers/v2/{ReceiptController.cs => ReceiptsController.cs} (88%)
diff --git a/coffeecard/CoffeeCard.WebApi/Controllers/v2/ReceiptController.cs b/coffeecard/CoffeeCard.WebApi/Controllers/v2/ReceiptsController.cs
similarity index 88%
rename from coffeecard/CoffeeCard.WebApi/Controllers/v2/ReceiptController.cs
rename to coffeecard/CoffeeCard.WebApi/Controllers/v2/ReceiptsController.cs
index 286f48a4..f7af8730 100644
--- a/coffeecard/CoffeeCard.WebApi/Controllers/v2/ReceiptController.cs
+++ b/coffeecard/CoffeeCard.WebApi/Controllers/v2/ReceiptsController.cs
@@ -16,17 +16,17 @@ namespace CoffeeCard.WebApi.Controllers.v2;
[Route("api/v{version:apiVersion}/receipts")]
[ApiController]
[Authorize]
-public class ReceiptController : ControllerBase
+public class ReceiptsController : ControllerBase
{
private readonly IReceiptService _receiptService;
private readonly ClaimsUtilities _claimsUtilities;
///
- /// Initializes a new instance of the class.
+ /// Initializes a new instance of the class.
///
/// Receipt service.
/// Helper for resolving the authenticated user from claims.
- public ReceiptController(IReceiptService receiptService, ClaimsUtilities claimsUtilities)
+ public ReceiptsController(IReceiptService receiptService, ClaimsUtilities claimsUtilities)
{
_receiptService = receiptService;
_claimsUtilities = claimsUtilities;
From 003b6661752763e7f8575777dc51ec04a3e2885b Mon Sep 17 00:00:00 2001
From: Omid Marfavi <21163286+marfavi@users.noreply.github.com>
Date: Sun, 3 May 2026 20:41:12 +0200
Subject: [PATCH 6/7] Don't sort by id as tiebreaker
---
.../Services/v2/ReceiptService.cs | 16 +---------------
1 file changed, 1 insertion(+), 15 deletions(-)
diff --git a/coffeecard/CoffeeCard.Library/Services/v2/ReceiptService.cs b/coffeecard/CoffeeCard.Library/Services/v2/ReceiptService.cs
index 1a3c94fb..8d2b7fa5 100644
--- a/coffeecard/CoffeeCard.Library/Services/v2/ReceiptService.cs
+++ b/coffeecard/CoffeeCard.Library/Services/v2/ReceiptService.cs
@@ -96,22 +96,8 @@ public async Task GetReceipts(int userId)
all.AddRange(usedTickets);
- var sorted = all.OrderByDescending(r => r.EventDate)
- .ThenByDescending(r => ParseIdNumber(r.Id))
- .ToList();
+ var sorted = all.OrderByDescending(r => r.EventDate).ToList();
return new ReceiptsResponse { Receipts = sorted };
}
-
- ///
- /// Parses the numeric entity-ID component from a composite receipt identifier such as
- /// "Purchase:123" and returns it as an .
- ///
- /// The composite ID string.
- /// The numeric database primary key embedded in .
- private static int ParseIdNumber(string id)
- {
- var parts = id.Split(':');
- return int.Parse(parts[1]);
- }
}
From 03dbebf0512a4b8e6330d69272ebdce5f5bcaf1b Mon Sep 17 00:00:00 2001
From: Omid Marfavi <21163286+marfavi@users.noreply.github.com>
Date: Sun, 3 May 2026 20:45:20 +0200
Subject: [PATCH 7/7] Also rename Receipt -> Receipts in tests
---
.../Controllers/v2/GetReceiptsTest.cs | 18 +++++++++---------
1 file changed, 9 insertions(+), 9 deletions(-)
diff --git a/coffeecard/CoffeeCard.Tests.Integration/Controllers/v2/GetReceiptsTest.cs b/coffeecard/CoffeeCard.Tests.Integration/Controllers/v2/GetReceiptsTest.cs
index fb896353..a8fcc106 100644
--- a/coffeecard/CoffeeCard.Tests.Integration/Controllers/v2/GetReceiptsTest.cs
+++ b/coffeecard/CoffeeCard.Tests.Integration/Controllers/v2/GetReceiptsTest.cs
@@ -25,7 +25,7 @@ public async Task GetReceipts_returns_401_when_not_authenticated()
RemoveRequestHeaders();
var exception = await Assert.ThrowsAsync(async () =>
- await CoffeeCardClientV2.Receipt_GetReceiptsAsync()
+ await CoffeeCardClientV2.Receipts_GetReceiptsAsync()
);
Assert.Equal(401, exception.StatusCode);
@@ -38,7 +38,7 @@ public async Task GetReceipts_returns_empty_list_when_user_has_no_activity()
{
await GetAuthenticatedUserAsync();
- var response = await CoffeeCardClientV2.Receipt_GetReceiptsAsync();
+ var response = await CoffeeCardClientV2.Receipts_GetReceiptsAsync();
Assert.NotNull(response);
Assert.Empty(response.Receipts);
@@ -67,7 +67,7 @@ public async Task GetReceipts_returns_purchase_receipt_for_completed_purchase()
await Context.Purchases.AddAsync(purchase);
await Context.SaveChangesAsync();
- var response = await CoffeeCardClientV2.Receipt_GetReceiptsAsync();
+ var response = await CoffeeCardClientV2.Receipts_GetReceiptsAsync();
Assert.Single(response.Receipts);
var receipt = response.Receipts.First();
@@ -117,7 +117,7 @@ public async Task GetReceipts_returns_voucher_receipt_for_voucher_purchase()
await Context.Purchases.AddAsync(purchase);
await Context.SaveChangesAsync();
- var response = await CoffeeCardClientV2.Receipt_GetReceiptsAsync();
+ var response = await CoffeeCardClientV2.Receipts_GetReceiptsAsync();
Assert.Single(response.Receipts);
var receipt = response.Receipts.First();
@@ -160,7 +160,7 @@ public async Task GetReceipts_returns_used_ticket_receipt_for_used_ticket()
await Context.Purchases.AddAsync(purchase);
await Context.SaveChangesAsync();
- var response = await CoffeeCardClientV2.Receipt_GetReceiptsAsync();
+ var response = await CoffeeCardClientV2.Receipts_GetReceiptsAsync();
Assert.Single(response.Receipts);
var receipt = response.Receipts.First();
@@ -196,7 +196,7 @@ public async Task GetReceipts_used_ticket_without_menu_item_has_null_drink_name(
await Context.Purchases.AddAsync(purchase);
await Context.SaveChangesAsync();
- var response = await CoffeeCardClientV2.Receipt_GetReceiptsAsync();
+ var response = await CoffeeCardClientV2.Receipts_GetReceiptsAsync();
Assert.Single(response.Receipts);
Assert.Null(response.Receipts.First().DrinkName);
@@ -222,7 +222,7 @@ public async Task GetReceipts_only_returns_receipts_for_authenticated_user()
await Context.Purchases.AddRangeAsync(otherPurchase, myPurchase);
await Context.SaveChangesAsync();
- var response = await CoffeeCardClientV2.Receipt_GetReceiptsAsync();
+ var response = await CoffeeCardClientV2.Receipts_GetReceiptsAsync();
Assert.Single(response.Receipts);
Assert.Equal(myPurchase.ProductName, response.Receipts.First().TicketName);
@@ -279,7 +279,7 @@ public async Task GetReceipts_with_All_type_returns_all_receipt_types()
await Context.Purchases.AddRangeAsync(purchase, voucherPurchase, usedPurchase);
await Context.SaveChangesAsync();
- var response = await CoffeeCardClientV2.Receipt_GetReceiptsAsync();
+ var response = await CoffeeCardClientV2.Receipts_GetReceiptsAsync();
Assert.Equal(3, response.Receipts.Count);
@@ -320,7 +320,7 @@ public async Task GetReceipts_items_are_sorted_newest_first()
await Context.SaveChangesAsync();
- var response = await CoffeeCardClientV2.Receipt_GetReceiptsAsync();
+ var response = await CoffeeCardClientV2.Receipts_GetReceiptsAsync();
Assert.Equal(3, response.Receipts.Count);