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,6 +52,11 @@ public static class FruitBankConstClient
|
||||||
public const string CargoPartnerDbTableName = "fbCargoPartner";
|
public const string CargoPartnerDbTableName = "fbCargoPartner";
|
||||||
public const string CargoTruckDbTableName = "fbCargoTruck";
|
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 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[] DevAdminIds = new Guid[2] { Guid.Parse("dcf451d2-cc4c-4ac2-8c1f-da00041be1fd"), Guid.Parse("4cbaed43-2465-4d99-84f1-c8bc6b7025f7") };
|
||||||
|
|
|
||||||
|
|
@ -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": {
|
"AcHubConnection": {
|
||||||
"Url": "https://shop.fruitbank.hu/fbHub",
|
//"Url": "https://localhost:59579/fbHub",
|
||||||
"TransportMaxBufferSize": 30000000,
|
"Url": "https://shop.fruitbank.hu/fbHub",
|
||||||
"ApplicationMaxBufferSize": 30000000,
|
"TransportMaxBufferSize": 30000000,
|
||||||
"CloseTimeout": "00:00:10",
|
"ApplicationMaxBufferSize": 30000000,
|
||||||
"KeepAliveInterval": "00:01:00",
|
"CloseTimeout": "00:00:10",
|
||||||
"ServerTimeout": "00:03:00",
|
"KeepAliveInterval": "00:01:00",
|
||||||
"SkipNegotiation": true,
|
"ServerTimeout": "00:03:00",
|
||||||
"Transports": "WebSockets",
|
"SkipNegotiation": true,
|
||||||
"UseAutomaticReconnect": true,
|
"Transports": "WebSockets",
|
||||||
"UseStatefulReconnect": true
|
"UseAutomaticReconnect": true,
|
||||||
},
|
"UseStatefulReconnect": true
|
||||||
"AcBinaryHubProtocol": {
|
},
|
||||||
"ProtocolMode": "AsyncSegment",
|
"AcBinaryHubProtocol": {
|
||||||
"BufferSize": 4096,
|
"ProtocolMode": "Bytes",
|
||||||
"FlushPolicy": "Coalesced",
|
"BufferSize": 4096,
|
||||||
"FlushTimeout": "00:00:10"
|
"FlushPolicy": "Coalesced",
|
||||||
}
|
"FlushTimeout": "00:00:10"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,10 +8,10 @@
|
||||||
"AyCode": {
|
"AyCode": {
|
||||||
"ProjectId": "aad53443-2ee2-4650-8a99-97e907265e4e",
|
"ProjectId": "aad53443-2ee2-4650-8a99-97e907265e4e",
|
||||||
"Urls": {
|
"Urls": {
|
||||||
"BaseUrl": "https://localhost:59579",
|
//"BaseUrl": "https://localhost:59579",
|
||||||
"ApiBaseUrl": "https://localhost:59579"
|
//"ApiBaseUrl": "https://localhost:59579"
|
||||||
//"BaseUrl": "https://shop.fruitbank.hu",
|
"BaseUrl": "https://shop.fruitbank.hu",
|
||||||
//"ApiBaseUrl": "https://shop.fruitbank.hu"
|
"ApiBaseUrl": "https://shop.fruitbank.hu"
|
||||||
},
|
},
|
||||||
"Logger": {
|
"Logger": {
|
||||||
"AppType": "Server",
|
"AppType": "Server",
|
||||||
|
|
@ -26,8 +26,8 @@
|
||||||
|
|
||||||
},
|
},
|
||||||
"AcHubConnection": {
|
"AcHubConnection": {
|
||||||
"Url": "https://localhost:59579/fbHub",
|
//"Url": "https://localhost:59579/fbHub",
|
||||||
//"Url": "https://shop.fruitbank.hu/fbHub",
|
"Url": "https://shop.fruitbank.hu/fbHub",
|
||||||
"TransportMaxBufferSize": 30000000,
|
"TransportMaxBufferSize": 30000000,
|
||||||
"ApplicationMaxBufferSize": 30000000,
|
"ApplicationMaxBufferSize": 30000000,
|
||||||
"CloseTimeout": "00:00:10",
|
"CloseTimeout": "00:00:10",
|
||||||
|
|
@ -38,10 +38,10 @@
|
||||||
"UseAutomaticReconnect": true,
|
"UseAutomaticReconnect": true,
|
||||||
"UseStatefulReconnect": true
|
"UseStatefulReconnect": true
|
||||||
},
|
},
|
||||||
"AcBinaryHubProtocol": {
|
"AcBinaryHubProtocol": {
|
||||||
"ProtocolMode": "AsyncSegment",
|
"ProtocolMode": "Bytes",
|
||||||
"BufferSize": 4096,
|
"BufferSize": 4096,
|
||||||
"FlushPolicy": "Coalesced",
|
"FlushPolicy": "Coalesced",
|
||||||
"FlushTimeout": "00:00:10"
|
"FlushTimeout": "00:00:10"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,10 +8,10 @@
|
||||||
"AyCode": {
|
"AyCode": {
|
||||||
"ProjectId": "aad53443-2ee2-4650-8a99-97e907265e4e",
|
"ProjectId": "aad53443-2ee2-4650-8a99-97e907265e4e",
|
||||||
"Urls": {
|
"Urls": {
|
||||||
"BaseUrl": "https://localhost:59579",
|
//"BaseUrl": "https://localhost:59579",
|
||||||
"ApiBaseUrl": "https://localhost:59579"
|
//"ApiBaseUrl": "https://localhost:59579"
|
||||||
//"BaseUrl": "https://shop.fruitbank.hu",
|
"BaseUrl": "https://shop.fruitbank.hu",
|
||||||
//"ApiBaseUrl": "https://shop.fruitbank.hu"
|
"ApiBaseUrl": "https://shop.fruitbank.hu"
|
||||||
},
|
},
|
||||||
"Logger": {
|
"Logger": {
|
||||||
"AppType": "Server",
|
"AppType": "Server",
|
||||||
|
|
@ -26,8 +26,8 @@
|
||||||
|
|
||||||
},
|
},
|
||||||
"AcHubConnection": {
|
"AcHubConnection": {
|
||||||
"Url": "https://localhost:59579/fbHub",
|
//"Url": "https://localhost:59579/fbHub",
|
||||||
//"Url": "https://shop.fruitbank.hu/fbHub",
|
"Url": "https://shop.fruitbank.hu/fbHub",
|
||||||
"TransportMaxBufferSize": 30000000,
|
"TransportMaxBufferSize": 30000000,
|
||||||
"ApplicationMaxBufferSize": 30000000,
|
"ApplicationMaxBufferSize": 30000000,
|
||||||
"CloseTimeout": "00:00:10",
|
"CloseTimeout": "00:00:10",
|
||||||
|
|
@ -38,10 +38,10 @@
|
||||||
"UseAutomaticReconnect": true,
|
"UseAutomaticReconnect": true,
|
||||||
"UseStatefulReconnect": true
|
"UseStatefulReconnect": true
|
||||||
},
|
},
|
||||||
"AcBinaryHubProtocol": {
|
"AcBinaryHubProtocol": {
|
||||||
"ProtocolMode": "AsyncSegment",
|
"ProtocolMode": "Bytes",
|
||||||
"BufferSize": 4096,
|
"BufferSize": 4096,
|
||||||
"FlushPolicy": "Coalesced",
|
"FlushPolicy": "Coalesced",
|
||||||
"FlushTimeout": "00:00:10"
|
"FlushTimeout": "00:00:10"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue