OrderDraft feature
This commit is contained in:
parent
bad80dd660
commit
10f975ce72
|
|
@ -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; }
|
||||
}
|
||||
|
|
@ -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<OrderDraftItem> Items { get; set; } = [];
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
public enum OrderDraftStatus
|
||||
{
|
||||
Pending = 0,
|
||||
Approved = 1,
|
||||
Rejected = 2,
|
||||
Expired = 3
|
||||
}
|
||||
|
|
@ -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]
|
||||
//{
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
@ -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)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue