diff --git a/FruitBank.Common/Entities/ExternalUserPartnerMapping.cs b/FruitBank.Common/Entities/ExternalUserPartnerMapping.cs new file mode 100644 index 00000000..8ddba1fc --- /dev/null +++ b/FruitBank.Common/Entities/ExternalUserPartnerMapping.cs @@ -0,0 +1,24 @@ +using AyCode.Core.Serializers.Attributes; +using AyCode.Core.Serializers.Toons; +using LinqToDB.Mapping; +using Mango.Nop.Core.Entities; + +namespace FruitBank.Common.Entities; + +[AcBinarySerializable(false, true, false, true, false, false)] +[Table(Name = FruitBankConstClient.ExternalUserPartnerMapDbTableName)] +[System.ComponentModel.DataAnnotations.Schema.Table(FruitBankConstClient.ExternalUserPartnerMapDbTableName)] +[ToonDescription("Maps an external sender identity to a NopCommerce customer (partner)", Purpose = "One record per (ExternalUsername, CustomerId) pair. A single partner may have multiple mappings (e.g. one for Viber, one for email). Lookup is by ExternalUsername; first match wins.")] +public sealed class ExternalUserPartnerMapping : MgEntityBase +{ + [ToonDescription(Purpose = "The external identifier as it arrives in the API call (Viber user ID, email address, etc.). Indexed for fast lookup.")] + public string ExternalUsername { get; set; } = string.Empty; + + [ToonDescription(Purpose = "FK to the NopCommerce Customer this external identity maps to.")] + public int CustomerId { get; set; } + + [ToonDescription(Purpose = "Admin who created this mapping.")] + public int CreatedByAdminId { get; set; } + + public DateTime CreatedOnUtc { get; set; } +} \ No newline at end of file diff --git a/FruitBank.Common/Entities/OrderDraft.cs b/FruitBank.Common/Entities/OrderDraft.cs new file mode 100644 index 00000000..48653fd9 --- /dev/null +++ b/FruitBank.Common/Entities/OrderDraft.cs @@ -0,0 +1,41 @@ +using AyCode.Core.Serializers.Attributes; +using AyCode.Core.Serializers.Toons; +using FruitBank.Common.Enums; +using LinqToDB.Mapping; +using Mango.Nop.Core.Entities; + +namespace FruitBank.Common.Entities; + +[AcBinarySerializable(false, true, false, true, false, false)] +[Table(Name = FruitBankConstClient.OrderDraftDbTableName)] +[System.ComponentModel.DataAnnotations.Schema.Table(FruitBankConstClient.OrderDraftDbTableName)] +[ToonDescription("AI-parsed order draft created from an incoming text message", Purpose = "Temporary order intent parsed from a free-text message (Viber, email, dictation). Contains the original raw message and AI-resolved line items. An admin must review and approve within 24 hours; drafts are auto-expired after 14 days. Approval converts the draft into a PreOrder or Order depending on ParsedDeliveryDate.")] +public sealed class OrderDraft : MgEntityBase +{ + [ToonDescription(Purpose = "The external identifier of the sender (e.g. Viber user ID, email address). Used to look up the mapped NopCommerce CustomerId via ExternalUserPartnerMap.")] + public string ExternalUsername { get; set; } = string.Empty; + + [ToonDescription(Purpose = "FK to the NopCommerce Customer resolved from ExternalUsername. Null if no mapping exists yet — admin must assign before approval.")] + public int? CustomerId { get; set; } + + [ToonDescription(Purpose = "The full original message text as received. Stored verbatim and shown to the admin during review.")] + public string RawMessageText { get; set; } = string.Empty; + + [ToonDescription(Purpose = "Delivery date parsed from the message by AI. Drives IsPreorder logic on each item (>4 days ahead = preorder).")] + public DateTime ParsedDeliveryDate { get; set; } + + [ToonDescription(Purpose = "Draft lifecycle: Pending / Approved / Rejected / Expired.", + BusinessRule = "Pending on creation. Admin sets Approved or Rejected. Background job sets Expired after 14 days if still Pending.")] + public OrderDraftStatus Status { get; set; } + + [ToonDescription(Purpose = "Admin who resolved (approved or rejected) this draft. Null until resolved.")] + public int? ResolvedByAdminId { get; set; } + + [ToonDescription(Purpose = "Optional admin note on rejection.")] + public string? AdminNote { get; set; } + + public DateTime CreatedOnUtc { get; set; } + public DateTime? ResolvedOnUtc { get; set; } + + public List Items { get; set; } = []; +} diff --git a/FruitBank.Common/Entities/OrderDraftItem.cs b/FruitBank.Common/Entities/OrderDraftItem.cs new file mode 100644 index 00000000..f3e422fe --- /dev/null +++ b/FruitBank.Common/Entities/OrderDraftItem.cs @@ -0,0 +1,38 @@ +using AyCode.Core.Serializers.Attributes; +using AyCode.Core.Serializers.Toons; +using LinqToDB.Mapping; +using Mango.Nop.Core.Entities; + +namespace FruitBank.Common.Entities; + +[AcBinarySerializable(false, true, false, true, false, false)] +[Table(Name = FruitBankConstClient.OrderDraftItemDbTableName)] +[System.ComponentModel.DataAnnotations.Schema.Table(FruitBankConstClient.OrderDraftItemDbTableName)] +[ToonDescription("Single product line of an AI-parsed order draft", Purpose = "One requested product line within an OrderDraft. The AI resolves the raw product text to an ordered list of candidate NopCommerce products; the admin selects one before approval.")] +public sealed class OrderDraftItem : MgEntityBase +{ + [ToonDescription(Purpose = "FK to the parent OrderDraft.")] + public int DraftId { get; set; } + + [ToonDescription(Purpose = "The raw product text extracted from the message, as the sender wrote it (e.g. 'piros kalif', '20 rekesz citrom').")] + public string RawProductText { get; set; } = string.Empty; + + [ToonDescription(Purpose = "The canonical product name as resolved by AI from RawProductText (e.g. 'Kaliforniai paprika piros'). For display purposes.")] + public string AiResolvedProductName { get; set; } = string.Empty; + + [ToonDescription(Purpose = "Ordered JSON array of candidate NopCommerce Product IDs, best match first. Populated by AI + historical ProductTextMapping weighting.")] + public string SuggestedProductIdsJson { get; set; } = "[]"; + + [ToonDescription(Purpose = "The Product ID selected by the admin during review. Null until admin resolves this item.")] + public int? SelectedProductId { get; set; } + + [ToonDescription(Purpose = "Quantity as parsed from the message text.", Constraints = "positive")] + public int RequestedQuantity { get; set; } + + [ToonDescription(Purpose = "Whether this item should become a preorder item (true) or a direct order item (false). Computed from ParsedDeliveryDate of the parent draft: >4 days ahead = true.", + BusinessRule = "Set at creation time from parent draft's ParsedDeliveryDate. Not editable by admin.")] + public bool IsPreorder { get; set; } + + [ToonDescription(Purpose = "Display order within the draft, matching the original message sequence.")] + public int SortOrder { get; set; } +} \ No newline at end of file diff --git a/FruitBank.Common/Entities/ProductTextMapping.cs b/FruitBank.Common/Entities/ProductTextMapping.cs new file mode 100644 index 00000000..532787c2 --- /dev/null +++ b/FruitBank.Common/Entities/ProductTextMapping.cs @@ -0,0 +1,27 @@ +using AyCode.Core.Serializers.Attributes; +using AyCode.Core.Serializers.Toons; +using LinqToDB.Mapping; +using Mango.Nop.Core.Entities; + +namespace FruitBank.Common.Entities; + +[AcBinarySerializable(false, true, false, true, false, false)] +[Table(Name = FruitBankConstClient.ProductTextMappingDbTableName)] +[System.ComponentModel.DataAnnotations.Schema.Table(FruitBankConstClient.ProductTextMappingDbTableName)] +[ToonDescription("Historical record of admin product selections from free-text inputs", Purpose = "Every time an admin approves an OrderDraftItem and selects a product, a record is written here. Used to weight future AI suggestions: if the same raw text recently resolved to a given product, that product ranks first in SuggestedProductIds.")] +public sealed class ProductTextMapping : MgEntityBase +{ + [ToonDescription(Purpose = "The raw text as it came from the message (e.g. 'piros kalif'). Stored lowercase-trimmed for consistent matching.")] + public string RawText { get; set; } = string.Empty; + + [ToonDescription(Purpose = "FK to the NopCommerce Product the admin selected for this raw text.")] + public int ResolvedProductId { get; set; } + + [ToonDescription(Purpose = "Whether the selection was in a preorder context (true) or direct order context (false). Allows separate weighting per context.")] + public bool IsPreorder { get; set; } + + [ToonDescription(Purpose = "The admin who made this selection.")] + public int SelectedByAdminId { get; set; } + + public DateTime CreatedOnUtc { get; set; } +} \ No newline at end of file diff --git a/FruitBank.Common/Enums/OrderDraftStatus.cs b/FruitBank.Common/Enums/OrderDraftStatus.cs new file mode 100644 index 00000000..bafd7ac1 --- /dev/null +++ b/FruitBank.Common/Enums/OrderDraftStatus.cs @@ -0,0 +1,7 @@ +public enum OrderDraftStatus +{ + Pending = 0, + Approved = 1, + Rejected = 2, + Expired = 3 +} \ No newline at end of file diff --git a/FruitBank.Common/FruitBankConstClient.cs b/FruitBank.Common/FruitBankConstClient.cs index 983f42cc..3d3c59f1 100644 --- a/FruitBank.Common/FruitBankConstClient.cs +++ b/FruitBank.Common/FruitBankConstClient.cs @@ -52,8 +52,13 @@ public static class FruitBankConstClient public const string CargoPartnerDbTableName = "fbCargoPartner"; public const string CargoTruckDbTableName = "fbCargoTruck"; + public const string OrderDraftDbTableName = "fbOrderDraft"; + public const string OrderDraftItemDbTableName = "fbOrderDraftItem"; + public const string ExternalUserPartnerMapDbTableName = "fbExternalUserPartnerMapping"; + public const string ProductTextMappingDbTableName = "fbProductTextMapping"; + public const string DomainDescription = "This is a nopCommerce plugin developed for FruitBank, a fruit and vegetable wholesaler. The plugin manages supplier inbound delivery (receiving), warehouse weighing (net/gross/pallet/tare weights), and inventory stocktaking. The business logic is centered around FruitBank's requirement for precise physical measurement and quantity tracking."; - + //public static Guid[] DevAdminIds = new Guid[2] { Guid.Parse("dcf451d2-cc4c-4ac2-8c1f-da00041be1fd"), Guid.Parse("4cbaed43-2465-4d99-84f1-c8bc6b7025f7") }; //public static Guid[] SysAdmins = new Guid[3] //{ diff --git a/FruitBank.Common/Loggers/README.md b/FruitBank.Common/Loggers/README.md deleted file mode 100644 index b921ec05..00000000 --- a/FruitBank.Common/Loggers/README.md +++ /dev/null @@ -1,7 +0,0 @@ -# Loggers - -SignalR client-to-server log writer. - -## Key Files - -- **`SignaRClientLogItemWriter.cs`** — Routes client logs to `{BaseUrl}/loggerHub` via SignalR. Configurable by AppType and LogLevel. diff --git a/FruitBank.Common/Loggers/SignaRClientLogItemWriter.cs b/FruitBank.Common/Loggers/SignaRClientLogItemWriter.cs deleted file mode 100644 index 79e9859a..00000000 --- a/FruitBank.Common/Loggers/SignaRClientLogItemWriter.cs +++ /dev/null @@ -1,17 +0,0 @@ -using AyCode.Core.Enums; -using AyCode.Core.Loggers; -using AyCode.Services.Loggers; - -namespace FruitBank.Common.Loggers -{ - public class SignaRClientLogItemWriter : AcSignaRClientLogItemWriter - { - public SignaRClientLogItemWriter() : this(AppType.Web, LogLevel.Detail, null) - { - } - public SignaRClientLogItemWriter(AppType appType, LogLevel logLevel, string? categoryName = null) : base($"{FruitBankConstClient.BaseUrl}/{FruitBankConstClient.LoggerHubName}", appType, logLevel, categoryName) - { - } - } - -} diff --git a/FruitBankHybrid.Shared/appsettings.json b/FruitBankHybrid.Shared/appsettings.json index c36e1ed9..a14ace2d 100644 --- a/FruitBankHybrid.Shared/appsettings.json +++ b/FruitBankHybrid.Shared/appsettings.json @@ -25,22 +25,23 @@ } }, - "AcHubConnection": { - "Url": "https://shop.fruitbank.hu/fbHub", - "TransportMaxBufferSize": 30000000, - "ApplicationMaxBufferSize": 30000000, - "CloseTimeout": "00:00:10", - "KeepAliveInterval": "00:01:00", - "ServerTimeout": "00:03:00", - "SkipNegotiation": true, - "Transports": "WebSockets", - "UseAutomaticReconnect": true, - "UseStatefulReconnect": true - }, - "AcBinaryHubProtocol": { - "ProtocolMode": "AsyncSegment", - "BufferSize": 4096, - "FlushPolicy": "Coalesced", - "FlushTimeout": "00:00:10" - } + "AcHubConnection": { + //"Url": "https://localhost:59579/fbHub", + "Url": "https://shop.fruitbank.hu/fbHub", + "TransportMaxBufferSize": 30000000, + "ApplicationMaxBufferSize": 30000000, + "CloseTimeout": "00:00:10", + "KeepAliveInterval": "00:01:00", + "ServerTimeout": "00:03:00", + "SkipNegotiation": true, + "Transports": "WebSockets", + "UseAutomaticReconnect": true, + "UseStatefulReconnect": true + }, + "AcBinaryHubProtocol": { + "ProtocolMode": "Bytes", + "BufferSize": 4096, + "FlushPolicy": "Coalesced", + "FlushTimeout": "00:00:10" + } } diff --git a/FruitBankHybrid.Web.Client/wwwroot/appsettings.json b/FruitBankHybrid.Web.Client/wwwroot/appsettings.json index 7e8e1509..a14ace2d 100644 --- a/FruitBankHybrid.Web.Client/wwwroot/appsettings.json +++ b/FruitBankHybrid.Web.Client/wwwroot/appsettings.json @@ -8,10 +8,10 @@ "AyCode": { "ProjectId": "aad53443-2ee2-4650-8a99-97e907265e4e", "Urls": { - "BaseUrl": "https://localhost:59579", - "ApiBaseUrl": "https://localhost:59579" - //"BaseUrl": "https://shop.fruitbank.hu", - //"ApiBaseUrl": "https://shop.fruitbank.hu" + //"BaseUrl": "https://localhost:59579", + //"ApiBaseUrl": "https://localhost:59579" + "BaseUrl": "https://shop.fruitbank.hu", + "ApiBaseUrl": "https://shop.fruitbank.hu" }, "Logger": { "AppType": "Server", @@ -26,8 +26,8 @@ }, "AcHubConnection": { - "Url": "https://localhost:59579/fbHub", - //"Url": "https://shop.fruitbank.hu/fbHub", + //"Url": "https://localhost:59579/fbHub", + "Url": "https://shop.fruitbank.hu/fbHub", "TransportMaxBufferSize": 30000000, "ApplicationMaxBufferSize": 30000000, "CloseTimeout": "00:00:10", @@ -38,10 +38,10 @@ "UseAutomaticReconnect": true, "UseStatefulReconnect": true }, - "AcBinaryHubProtocol": { - "ProtocolMode": "AsyncSegment", - "BufferSize": 4096, - "FlushPolicy": "Coalesced", - "FlushTimeout": "00:00:10" - } + "AcBinaryHubProtocol": { + "ProtocolMode": "Bytes", + "BufferSize": 4096, + "FlushPolicy": "Coalesced", + "FlushTimeout": "00:00:10" + } } diff --git a/FruitBankHybrid.Web/appsettings.json b/FruitBankHybrid.Web/appsettings.json index 7e8e1509..a14ace2d 100644 --- a/FruitBankHybrid.Web/appsettings.json +++ b/FruitBankHybrid.Web/appsettings.json @@ -8,10 +8,10 @@ "AyCode": { "ProjectId": "aad53443-2ee2-4650-8a99-97e907265e4e", "Urls": { - "BaseUrl": "https://localhost:59579", - "ApiBaseUrl": "https://localhost:59579" - //"BaseUrl": "https://shop.fruitbank.hu", - //"ApiBaseUrl": "https://shop.fruitbank.hu" + //"BaseUrl": "https://localhost:59579", + //"ApiBaseUrl": "https://localhost:59579" + "BaseUrl": "https://shop.fruitbank.hu", + "ApiBaseUrl": "https://shop.fruitbank.hu" }, "Logger": { "AppType": "Server", @@ -26,8 +26,8 @@ }, "AcHubConnection": { - "Url": "https://localhost:59579/fbHub", - //"Url": "https://shop.fruitbank.hu/fbHub", + //"Url": "https://localhost:59579/fbHub", + "Url": "https://shop.fruitbank.hu/fbHub", "TransportMaxBufferSize": 30000000, "ApplicationMaxBufferSize": 30000000, "CloseTimeout": "00:00:10", @@ -38,10 +38,10 @@ "UseAutomaticReconnect": true, "UseStatefulReconnect": true }, - "AcBinaryHubProtocol": { - "ProtocolMode": "AsyncSegment", - "BufferSize": 4096, - "FlushPolicy": "Coalesced", - "FlushTimeout": "00:00:10" - } + "AcBinaryHubProtocol": { + "ProtocolMode": "Bytes", + "BufferSize": 4096, + "FlushPolicy": "Coalesced", + "FlushTimeout": "00:00:10" + } }