From 7a2b384d080bff5ca5575267406cdd5e070c9680 Mon Sep 17 00:00:00 2001 From: Louis DEVIE Date: Wed, 14 May 2025 22:52:39 +0200 Subject: [PATCH 1/4] =?UTF-8?q?fix:=20modification=20de=20la=20nullabilit?= =?UTF-8?q?=C3=A9=20de=20`Client.granted`,=20`Client.allowed`=20et=20`Item?= =?UTF-8?q?.currentStock`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../v1_04_00/AlterColumn_Client_allowed.cs | 19 +++++++++++++++++++ .../v1_04_00/AlterColumn_Client_granted.cs | 19 +++++++++++++++++++ .../v1_04_00/AlterColumn_Item_currentStock.cs | 19 +++++++++++++++++++ 3 files changed, 57 insertions(+) create mode 100644 External/Data/MariaDb/Migrations/v1_04_00/AlterColumn_Client_allowed.cs create mode 100644 External/Data/MariaDb/Migrations/v1_04_00/AlterColumn_Client_granted.cs create mode 100644 External/Data/MariaDb/Migrations/v1_04_00/AlterColumn_Item_currentStock.cs 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 From a162148fdd88b8f788e56822af7621ddd289971e Mon Sep 17 00:00:00 2001 From: Louis DEVIE Date: Thu, 15 May 2025 12:31:27 +0200 Subject: [PATCH 2/4] feat: modification de l'endpoint de la caisse --- .tests/tests/__init__.py | 1 + .tests/tests/checkout_tests.py | 60 ++++++++++----------- .tests/utils/expectations.py | 9 ++-- Core/Checkout/ItemSellingPrice.cs | 8 --- Core/Checkout/ItemSold.cs | 41 -------------- Core/Checkout/ItemsSoldCategory.cs | 30 ----------- Core/Stocks/Item.cs | 49 +++++++++++++++++ WebService/Controllers/ItemController.cs | 17 ++++++ WebService/Dto/Checkout/ItemSold.cs | 59 ++++++++++++++++++++ WebService/Dto/Checkout/ItemSoldCategory.cs | 17 ++++++ WebService/Services/CheckoutService.cs | 8 +-- 11 files changed, 182 insertions(+), 117 deletions(-) delete mode 100644 Core/Checkout/ItemSellingPrice.cs delete mode 100644 Core/Checkout/ItemSold.cs delete mode 100644 Core/Checkout/ItemsSoldCategory.cs create mode 100644 Core/Stocks/Item.cs create mode 100644 WebService/Controllers/ItemController.cs create mode 100644 WebService/Dto/Checkout/ItemSold.cs create mode 100644 WebService/Dto/Checkout/ItemSoldCategory.cs diff --git a/.tests/tests/__init__.py b/.tests/tests/__init__.py index a7f894e..71b54aa 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 .checkout_tests import CheckoutTests from .order_tests import OrderTests from .pricing_tests import PricingTests from .product_tests import ProductTests diff --git a/.tests/tests/checkout_tests.py b/.tests/tests/checkout_tests.py index 974b4f5..490c1f5 100644 --- a/.tests/tests/checkout_tests.py +++ b/.tests/tests/checkout_tests.py @@ -17,34 +17,32 @@ def test_get_items_sold(self): categories = response.json() self.expect(categories).to.be.a(list)._and._not.empty() - category = categories[0] - self.expect(category).to.have.an_item("label").of.type(str) - items = self.expect(category).to.have.an_item("items").value - - self.expect(items).to.be.a(list)._and._not.empty() - - item = items[0] - self.expect(item).to.have.an_item("code").of.type(str) - self.expect(item).to.have.an_item("label").of.type(str) - self.expect(item).to.have.an_item("stock").of.type(int) - self.expect(item).to.have.an_item("primaryPrice").that.is_.a_number() - if "secondaryPrice" in item: - self.expect(item["secondaryPrice"]).to.be.a_number() - prices = self.expect(item).to.have.an_item("prices").value - - self.expect(prices).to.be.a(list)._and._not.empty() - - price = prices[0] - self.expect(price).to.have.an_item("pricingId").of.type(int) - self.expect(price).to.have.an_item("price").that.is_.a_number() - - def test_create_order(self): - order = { - "operationCode": "O", - "customer": "@anonymous_customer", - "description": "commande test", - "items": [{"code": "P0002", "quantity": 3}], - } - - response = self.post("operations/sale", order) - self.expect(response.status_code).to.be.equal_to(200) + 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") + + # def test_create_order(self): + # order = { + # "operationCode": "O", + # "customer": "@anonymous_customer", + # "description": "commande test", + # "items": [{"code": "P0002", "quantity": 3}], + # } + + # response = self.post("operations/sale", order) + # self.expect(response.status_code).to.be.equal_to(200) 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/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/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..98ff4cc --- /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(Item item) + { + this.Id = item.Id; + this.Name = item.Name; + this.MemberPrice = 0; + this.NonMemberPrice = 0; + this.IsAvailable = item.IsAvailable == Availability.Always; + this.AvailableStock = item.CurrentStock; + this.IsBundle = item.IsBundle; + } +} \ 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..1a6b7c9 --- /dev/null +++ b/WebService/Dto/Checkout/ItemSoldCategory.cs @@ -0,0 +1,17 @@ +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; } +} \ No newline at end of file diff --git a/WebService/Services/CheckoutService.cs b/WebService/Services/CheckoutService.cs index fd82e0a..67e23fc 100644 --- a/WebService/Services/CheckoutService.cs +++ b/WebService/Services/CheckoutService.cs @@ -1,14 +1,14 @@ -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 [new ItemSoldCategory()]; } } \ No newline at end of file From 98fa0fe1320b4a2b75305d4f533fd52346b07410 Mon Sep 17 00:00:00 2001 From: Louis DEVIE Date: Wed, 3 Sep 2025 13:59:11 +0200 Subject: [PATCH 3/4] api de la caisse --- .tests/tests/__init__.py | 2 +- .tests/tests/item_tests.py | 36 +++++++++++++++++++++ Core/Core.csproj | 2 +- KiwiQuery | 2 +- WebService/Dto/Checkout/ItemSold.cs | 18 +++++------ WebService/Dto/Checkout/ItemSoldCategory.cs | 8 +++++ WebService/Services/CheckoutService.cs | 4 ++- 7 files changed, 59 insertions(+), 13 deletions(-) create mode 100644 .tests/tests/item_tests.py diff --git a/.tests/tests/__init__.py b/.tests/tests/__init__.py index 71b54aa..a97f696 100644 --- a/.tests/tests/__init__.py +++ b/.tests/tests/__init__.py @@ -1,7 +1,7 @@ from .access_tests import AccessTests from .application_tests import ApplicationTests from .category_tests import CategoryTests -from .checkout_tests import CheckoutTests +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/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/KiwiQuery b/KiwiQuery index 84e712a..4c63669 160000 --- a/KiwiQuery +++ b/KiwiQuery @@ -1 +1 @@ -Subproject commit 84e712a6b25930dfe1e77ddd68fbeeef8ac845bf +Subproject commit 4c63669aa0335222c3f9afe2da972376ce247674 diff --git a/WebService/Dto/Checkout/ItemSold.cs b/WebService/Dto/Checkout/ItemSold.cs index 98ff4cc..d8baa8a 100644 --- a/WebService/Dto/Checkout/ItemSold.cs +++ b/WebService/Dto/Checkout/ItemSold.cs @@ -40,20 +40,20 @@ public class ItemSold /// 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(Item item) + public ItemSold(Product product) { - this.Id = item.Id; - this.Name = item.Name; - this.MemberPrice = 0; - this.NonMemberPrice = 0; - this.IsAvailable = item.IsAvailable == Availability.Always; - this.AvailableStock = item.CurrentStock; - this.IsBundle = item.IsBundle; + 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 index 1a6b7c9..17ae27f 100644 --- a/WebService/Dto/Checkout/ItemSoldCategory.cs +++ b/WebService/Dto/Checkout/ItemSoldCategory.cs @@ -1,3 +1,5 @@ +using GalliumPlus.Core.Stocks; + namespace GalliumPlus.WebService.Dto.Checkout; /// @@ -14,4 +16,10 @@ public class ItemSoldCategory /// 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/Services/CheckoutService.cs b/WebService/Services/CheckoutService.cs index 67e23fc..96369be 100644 --- a/WebService/Services/CheckoutService.cs +++ b/WebService/Services/CheckoutService.cs @@ -9,6 +9,8 @@ public class ItemService(IProductDao productDao) public IEnumerable GetItemsSold() { var products = productDao.Read(); - return [new ItemSoldCategory()]; + return products + .GroupBy(product => product.Category.Name) + .Select(groupe => new ItemSoldCategory(groupe.Key, groupe)); } } \ No newline at end of file From 01a0bd2a4a6fc4ae07153939e91cc594928a4b6b Mon Sep 17 00:00:00 2001 From: Louis DEVIE Date: Wed, 3 Sep 2025 14:04:26 +0200 Subject: [PATCH 4/4] version --- WebService/Program.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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