diff --git a/.tests/tests/__init__.py b/.tests/tests/__init__.py index a7f894e..a97f696 100644 --- a/.tests/tests/__init__.py +++ b/.tests/tests/__init__.py @@ -1,6 +1,7 @@ from .access_tests import AccessTests from .application_tests import ApplicationTests from .category_tests import CategoryTests +from .item_tests import ItemTests from .order_tests import OrderTests from .pricing_tests import PricingTests from .product_tests import ProductTests diff --git a/.tests/tests/item_tests.py b/.tests/tests/item_tests.py new file mode 100644 index 0000000..ad698b0 --- /dev/null +++ b/.tests/tests/item_tests.py @@ -0,0 +1,36 @@ +from utils.test_base import TestBase +from utils.auth import BearerAuth +from .audit_tests_helpers import AuditTestHelpers + + +class ItemTests(TestBase): + def setUp(self): + super().setUp() + self.set_authentication(BearerAuth("09876543210987654321")) + self.audit = AuditTestHelpers(self, 1, 3) + + def test_get_items_sold(self): + response = self.get("items-sold") + self.expect(response.status_code).to.be.equal_to(200) + + categories = response.json() + self.expect(categories).to.be.a(list)._and._not.empty() + + for category in categories: + self.expect(category).to.have.an_item("name").of.type(str) + items = self.expect(category).to.have.an_item("items").value + + self.expect(items).to.be.a(list)._and._not.empty() + + for item in items: + self.expect(item).to.have.an_item("id").of.type(int) + self.expect(item).to.have.an_item("name").of.type(str) + self.expect(item).to.have.an_item("memberPrice").that.is_.a_number() + self.expect(item).to.have.an_item( + "nonMemberPrice" + ).that.is_.none_or.a_number() + self.expect(item).to.have.an_item("isAvailable").of.type(bool) + self.expect(item).to.have.an_item("availableStock").that.is_.none_or.an( + int + ) + self.expect(item).to.have.an_item("isBundle") diff --git a/.tests/utils/expectations.py b/.tests/utils/expectations.py index 45ed2f3..f031202 100644 --- a/.tests/utils/expectations.py +++ b/.tests/utils/expectations.py @@ -80,15 +80,18 @@ def a(self, type): def a_number(self): is_a_number = isinstance(self.value, int) or isinstance(self.value, float) - is_a_number_or_none = is_a_number or self.value is None if self.negation: if self.nullable: - self.test_case.assertFalse(is_a_number_or_none, f"{self.value} is None") + self.test_case.assertFalse(is_a_number, f"{self.value} is a number") + self.test_case.assertFalse(self.value is None, f"{self.value} is None") else: self.test_case.assertFalse(is_a_number, f"{self.value} is a number") else: if self.nullable: - self.test_case.assertFalse(self.value is None, f"{self.value} is None") + self.test_case.assertTrue( + is_a_number or self.value is None, + f"{self.value} isn't a number and isn't None", + ) else: self.test_case.assertTrue(is_a_number, f"{self.value} isn't a number") diff --git a/Core/Checkout/ItemSellingPrice.cs b/Core/Checkout/ItemSellingPrice.cs deleted file mode 100644 index aa14f65..0000000 --- a/Core/Checkout/ItemSellingPrice.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace GalliumPlus.Core.Checkout; - -public class ItemSellingPrice(int pricingId, decimal price) -{ - public int PricingId => pricingId; - - public decimal Price => price; -} \ No newline at end of file diff --git a/Core/Checkout/ItemSold.cs b/Core/Checkout/ItemSold.cs deleted file mode 100644 index 8aa5e30..0000000 --- a/Core/Checkout/ItemSold.cs +++ /dev/null @@ -1,41 +0,0 @@ -using System.Text.Json.Serialization; -using GalliumPlus.Core.Stocks; - -namespace GalliumPlus.Core.Checkout; - -public class ItemSold( - string code, - string label, - int stock, - decimal primaryPrice, - decimal? secondaryPrice, - IEnumerable prices) -{ - public string Code => code; - - public string Label => label; - - public int Stock => stock; - - public decimal PrimaryPrice => primaryPrice; - - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public decimal? SecondaryPrice => secondaryPrice; - - public IEnumerable Prices => prices; - - public static ItemSold FromLegacyProduct(Product product) - { - return new ItemSold( - $"P{product.Id:0000}", - product.Name, - product.Stock, - product.MemberPrice, - product.NonMemberPrice, - [ - new ItemSellingPrice(90001, product.MemberPrice), - new ItemSellingPrice(90002, product.NonMemberPrice) - ] - ); - } -} \ No newline at end of file diff --git a/Core/Checkout/ItemsSoldCategory.cs b/Core/Checkout/ItemsSoldCategory.cs deleted file mode 100644 index a992b5e..0000000 --- a/Core/Checkout/ItemsSoldCategory.cs +++ /dev/null @@ -1,30 +0,0 @@ -using GalliumPlus.Core.Stocks; - -namespace GalliumPlus.Core.Checkout; - -public class ItemsSoldCategory(string label, List items) -{ - public string Label => label; - - public List Items => items; - - public static IEnumerable FromLegacyProducts(IEnumerable products) - { - Dictionary groupedByCategory = new(); - - foreach (Product product in products) - { - if (!product.Available) continue; - - if (!groupedByCategory.TryGetValue(product.Category.Id, out ItemsSoldCategory? category)) - { - category = new ItemsSoldCategory(product.Category.Name, []); - groupedByCategory.Add(product.Category.Id, category); - } - - category.Items.Add(ItemSold.FromLegacyProduct(product)); - } - - return groupedByCategory.OrderBy(kvp => kvp.Key).Select(kvp => kvp.Value); - } -} \ No newline at end of file diff --git a/Core/Core.csproj b/Core/Core.csproj index 7d5742f..82d7fac 100644 --- a/Core/Core.csproj +++ b/Core/Core.csproj @@ -11,7 +11,7 @@ - + diff --git a/Core/Stocks/Item.cs b/Core/Stocks/Item.cs new file mode 100644 index 0000000..a9045ae --- /dev/null +++ b/Core/Stocks/Item.cs @@ -0,0 +1,49 @@ +using KiwiQuery.Mapped; + +namespace GalliumPlus.Core.Stocks; + +/// +/// +/// +public class Item +{ + [PrimaryKey] + private int id; + private string name; + private bool isBundle; + private Availability isAvailable; + private int? currentStock; + private Category category; + private Category? group; + private string? picture; + + public int Id => this.id; + public string Name => this.name; + public bool IsBundle => this.isBundle; + public Availability IsAvailable => this.isAvailable; + public int? CurrentStock => this.currentStock; + public Category Category => this.category; + public Category? Group => this.group; + public string? Picture => this.picture; + + public Item( + int id, + string name, + bool isBundle, + Availability isAvailable, + int? currentStock, + Category category, + Category? group, + string? picture + ) + { + this.id = id; + this.name = name; + this.isBundle = isBundle; + this.isAvailable = isAvailable; + this.currentStock = currentStock; + this.category = category; + this.group = group; + this.picture = picture; + } +} \ No newline at end of file diff --git a/External/Data/MariaDb/Migrations/v1_04_00/AlterColumn_Client_allowed.cs b/External/Data/MariaDb/Migrations/v1_04_00/AlterColumn_Client_allowed.cs new file mode 100644 index 0000000..443e020 --- /dev/null +++ b/External/Data/MariaDb/Migrations/v1_04_00/AlterColumn_Client_allowed.cs @@ -0,0 +1,19 @@ +using FluentMigrator; + +namespace GalliumPlus.Data.MariaDb.Migrations.v1_04_00; + +// ReSharper disable once InconsistentNaming +// ReSharper disable once UnusedType.Global +[Migration(1_04_00_001)] +public class AlterColumn_Client_allowed : Migration +{ + public override void Up() + { + this.Alter.Table("Client").AlterColumn("allowed").AsInt32().NotNullable(); + } + + public override void Down() + { + this.Alter.Table("Client").AlterColumn("allowed").AsInt32().Nullable(); + } +} \ No newline at end of file diff --git a/External/Data/MariaDb/Migrations/v1_04_00/AlterColumn_Client_granted.cs b/External/Data/MariaDb/Migrations/v1_04_00/AlterColumn_Client_granted.cs new file mode 100644 index 0000000..497bd63 --- /dev/null +++ b/External/Data/MariaDb/Migrations/v1_04_00/AlterColumn_Client_granted.cs @@ -0,0 +1,19 @@ +using FluentMigrator; + +namespace GalliumPlus.Data.MariaDb.Migrations.v1_04_00; + +// ReSharper disable once InconsistentNaming +// ReSharper disable once UnusedType.Global +[Migration(1_04_00_002)] +public class AlterColumn_Client_granted : Migration +{ + public override void Up() + { + this.Alter.Table("Client").AlterColumn("granted").AsInt32().NotNullable(); + } + + public override void Down() + { + this.Alter.Table("Client").AlterColumn("granted").AsInt32().Nullable(); + } +} \ No newline at end of file diff --git a/External/Data/MariaDb/Migrations/v1_04_00/AlterColumn_Item_currentStock.cs b/External/Data/MariaDb/Migrations/v1_04_00/AlterColumn_Item_currentStock.cs new file mode 100644 index 0000000..c8d51e0 --- /dev/null +++ b/External/Data/MariaDb/Migrations/v1_04_00/AlterColumn_Item_currentStock.cs @@ -0,0 +1,19 @@ +using FluentMigrator; + +namespace GalliumPlus.Data.MariaDb.Migrations.v1_04_00; + +// ReSharper disable once InconsistentNaming +// ReSharper disable once UnusedType.Global +[Migration(1_04_00_003)] +public class AlterColumn_Item_currentStock : Migration +{ + public override void Up() + { + this.Alter.Table("Item").AlterColumn("currentStock").AsInt32().Nullable(); + } + + public override void Down() + { + this.Alter.Table("Item").AlterColumn("currentStock").AsInt32().NotNullable(); + } +} \ No newline at end of file diff --git a/KiwiQuery b/KiwiQuery index 84e712a..4c63669 160000 --- a/KiwiQuery +++ b/KiwiQuery @@ -1 +1 @@ -Subproject commit 84e712a6b25930dfe1e77ddd68fbeeef8ac845bf +Subproject commit 4c63669aa0335222c3f9afe2da972376ce247674 diff --git a/WebService/Controllers/ItemController.cs b/WebService/Controllers/ItemController.cs new file mode 100644 index 0000000..a663fce --- /dev/null +++ b/WebService/Controllers/ItemController.cs @@ -0,0 +1,17 @@ +using GalliumPlus.WebService.Services; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace GalliumPlus.WebService.Controllers; + +[Route("v1/items")] +[Authorize] +[ApiController] +public class ItemController(ItemService itemService) : GalliumController +{ + [HttpGet("/v1/items-sold")] + public IActionResult GetItemsSold() + { + return this.Json(itemService.GetItemsSold()); + } +} \ No newline at end of file diff --git a/WebService/Dto/Checkout/ItemSold.cs b/WebService/Dto/Checkout/ItemSold.cs new file mode 100644 index 0000000..d8baa8a --- /dev/null +++ b/WebService/Dto/Checkout/ItemSold.cs @@ -0,0 +1,59 @@ +using GalliumPlus.Core.Stocks; + +namespace GalliumPlus.WebService.Dto.Checkout; + +/// +/// Un article disponible sur la caisse. +/// +public class ItemSold +{ + /// + /// L'identifiant de l'article. + /// + public int Id { get; } + + /// + /// La désignation de l'article. + /// + public string Name { get; } + + /// + /// Le prix adhérent en euros. + /// + public decimal MemberPrice { get; } + + /// + /// Le prix non-adhérent en euros. Une valeur null indique une + /// exclusivité pour les adhérents. + /// + public decimal? NonMemberPrice { get; } + + /// + /// Indique si l'article peut être vendu ou non. Une valeur false + /// signifie que l'article est en rupture de stock et qu'il ne peut pas + /// être acheté. + /// + public bool IsAvailable { get; } + + /// + /// La quantité restante disponible. Cette valeur peut être null + /// pour indiquer un stock indéfini. + /// + public int? AvailableStock { get; } + + /// + /// Indique si l'article est une formule ou non. + /// + public bool IsBundle { get; } + + public ItemSold(Product product) + { + this.Id = product.Id; + this.Name = product.Name; + this.MemberPrice = product.MemberPrice; + this.NonMemberPrice = product.NonMemberPrice; + this.IsAvailable = product.Availability == Availability.Always; + this.AvailableStock = product.Stock; + this.IsBundle = false; + } +} \ No newline at end of file diff --git a/WebService/Dto/Checkout/ItemSoldCategory.cs b/WebService/Dto/Checkout/ItemSoldCategory.cs new file mode 100644 index 0000000..17ae27f --- /dev/null +++ b/WebService/Dto/Checkout/ItemSoldCategory.cs @@ -0,0 +1,25 @@ +using GalliumPlus.Core.Stocks; + +namespace GalliumPlus.WebService.Dto.Checkout; + +/// +/// Une catégorie d'articles disponibles sur la caisse. +/// +public class ItemSoldCategory +{ + /// + /// Le nom de la catégorie. + /// + public string Name { get; } + + /// + /// Les articles appartenant à la catégorie. + /// + public IList Items { get; } + + public ItemSoldCategory(string name, IEnumerable products) + { + this.Name = name; + this.Items = products.Select(p => new ItemSold(p)).ToList(); + } +} \ No newline at end of file diff --git a/WebService/Program.cs b/WebService/Program.cs index d25ad2b..627dcf5 100644 --- a/WebService/Program.cs +++ b/WebService/Program.cs @@ -210,7 +210,7 @@ app.UseAuthorization(); app.MapControllers(); -ServerInfo.Current.SetVersion(1, 4, 1, "beta"); +ServerInfo.Current.SetVersion(1, 5, 0, "beta"); Console.WriteLine(ServerInfo.Current); #if !FAKE_DB diff --git a/WebService/Services/CheckoutService.cs b/WebService/Services/CheckoutService.cs index fd82e0a..96369be 100644 --- a/WebService/Services/CheckoutService.cs +++ b/WebService/Services/CheckoutService.cs @@ -1,14 +1,16 @@ -using GalliumPlus.Core.Checkout; using GalliumPlus.Core.Data; +using GalliumPlus.WebService.Dto.Checkout; namespace GalliumPlus.WebService.Services; [ScopedService] -public class CheckoutService(IProductDao productDao) +public class ItemService(IProductDao productDao) { - public IEnumerable GetItemsSold() + public IEnumerable GetItemsSold() { var products = productDao.Read(); - return ItemsSoldCategory.FromLegacyProducts(products); + return products + .GroupBy(product => product.Category.Name) + .Select(groupe => new ItemSoldCategory(groupe.Key, groupe)); } } \ No newline at end of file