From 6c776a97ca219fe6e8e1e6fbd5846a1fb42c5ad5 Mon Sep 17 00:00:00 2001 From: Loretta Date: Thu, 2 Apr 2026 22:19:30 +0200 Subject: [PATCH] Enforce doc-first protocol, add SGen, modular plugin docs - Enforced strict documentation-first AI agent protocol in `.github/copilot-instructions.md` (multi-repo, no auto doc edits, explicit consent, [LOADED_DOCS] prefix, [DOCUMENTATION CHECK] on code changes) - Updated solution structure: added `docs` folder to solution items for LLM/AI context - Integrated AyCode SGen (source-generated binary serialization) with forced runtime registration; documented SGen usage and exclusions - Overhauled plugin `README.md` and added modular docs: `SCHEMA.md`, `DOMAIN_MODEL.md`, `MEASUREMENT.md`, `DATA_LAYER.md`, `AI_SERVICES.md`, `SIGNALR_ENDPOINTS.md` - Updated `CLAUDE.md` to require reading copilot instructions first - Switched appsettings connection string to production DB - Minor doc clarifications, corrects, and project file updates --- .../.github/copilot-instructions.md | 0 .../Infrastructure/PluginNopStartup.cs | 3 + .../Nop.Plugin.Misc.FruitBankPlugin.csproj | 9 + Nop.Plugin.Misc.AIPlugin/README.md | 96 ++- Nop.Plugin.Misc.AIPlugin/docs/AI_SERVICES.md | 155 +++++ Nop.Plugin.Misc.AIPlugin/docs/DATA_LAYER.md | 174 +++++ Nop.Plugin.Misc.AIPlugin/docs/DOMAIN_MODEL.md | 116 ++++ Nop.Plugin.Misc.AIPlugin/docs/MEASUREMENT.md | 175 +++++ Nop.Plugin.Misc.AIPlugin/docs/SCHEMA.md | 637 ++++++++++++++++++ .../docs/SIGNALR_ENDPOINTS.md | 166 +++++ 10 files changed, 1479 insertions(+), 52 deletions(-) create mode 100644 Nop.Plugin.Misc.AIPlugin/.github/copilot-instructions.md create mode 100644 Nop.Plugin.Misc.AIPlugin/docs/AI_SERVICES.md create mode 100644 Nop.Plugin.Misc.AIPlugin/docs/DATA_LAYER.md create mode 100644 Nop.Plugin.Misc.AIPlugin/docs/DOMAIN_MODEL.md create mode 100644 Nop.Plugin.Misc.AIPlugin/docs/MEASUREMENT.md create mode 100644 Nop.Plugin.Misc.AIPlugin/docs/SCHEMA.md create mode 100644 Nop.Plugin.Misc.AIPlugin/docs/SIGNALR_ENDPOINTS.md diff --git a/Nop.Plugin.Misc.AIPlugin/.github/copilot-instructions.md b/Nop.Plugin.Misc.AIPlugin/.github/copilot-instructions.md new file mode 100644 index 0000000..e69de29 diff --git a/Nop.Plugin.Misc.AIPlugin/Infrastructure/PluginNopStartup.cs b/Nop.Plugin.Misc.AIPlugin/Infrastructure/PluginNopStartup.cs index 18bcd93..3a93a69 100644 --- a/Nop.Plugin.Misc.AIPlugin/Infrastructure/PluginNopStartup.cs +++ b/Nop.Plugin.Misc.AIPlugin/Infrastructure/PluginNopStartup.cs @@ -46,6 +46,9 @@ public class PluginNopStartup : INopStartup /// Configuration of the application public void ConfigureServices(IServiceCollection services, IConfiguration configuration) { + // Enforce registration of source generated serializers which might not run due to AssemblyLoadContext restrictions + Mango.Nop.Core.AcBinaryForcedInit.ForceRegister(); + services.AddScoped(); services.AddTransient(); diff --git a/Nop.Plugin.Misc.AIPlugin/Nop.Plugin.Misc.FruitBankPlugin.csproj b/Nop.Plugin.Misc.AIPlugin/Nop.Plugin.Misc.FruitBankPlugin.csproj index 9088276..447e967 100644 --- a/Nop.Plugin.Misc.AIPlugin/Nop.Plugin.Misc.FruitBankPlugin.csproj +++ b/Nop.Plugin.Misc.AIPlugin/Nop.Plugin.Misc.FruitBankPlugin.csproj @@ -667,6 +667,15 @@ + + + + + + + + + diff --git a/Nop.Plugin.Misc.AIPlugin/README.md b/Nop.Plugin.Misc.AIPlugin/README.md index ca173d9..63c05b0 100644 --- a/Nop.Plugin.Misc.AIPlugin/README.md +++ b/Nop.Plugin.Misc.AIPlugin/README.md @@ -1,67 +1,59 @@ # Nop.Plugin.Misc.AIPlugin (FruitBank nopCommerce Plugin) -> For FruitBankHybridApp domain rules see: `../../../../../FruitBankHybridApp/.github/copilot-instructions.md` -> For Mango.Nop library docs see: `../../Libraries/README.md` +@project { + type = "nopcommerce-plugin" + own-dep-projects = [ + "Mango.Nop.Core, Mango.Nop.Services (in NopCommerce.Common/4.70/Libraries)" + ] +} -The **server-side nopCommerce plugin** for FruitBank. Runs inside nopCommerce 4.80.9 (net9.0). Handles admin UI, SignalR hubs, data access, measurements, orders, shipping, and AI services. +> For Mango.Nop library docs see `NopCommerce.Common/4.70/Libraries/README.md` +> For FruitBankHybridApp domain rules see the FruitBankHybridApp solution's `.github/copilot-instructions.md` +> For core measurement system rules and common domain traps, see: `../../../../FruitBankHybridApp/FruitBank.Common/docs/GLOSSARY.md` + +Server-side nopCommerce plugin for FruitBank, a fruit and vegetable wholesaler. Manages supplier inbound delivery (receiving), warehouse weighing (net/gross/pallet/tare weights), outbound order measurement, inventory stocktaking, AI services, and Billingo invoicing. Runs inside nopCommerce 4.80.9 (**net9.0**). Project file: `Nop.Plugin.Misc.FruitBankPlugin.csproj` +## Documentation + +| Document | Topic | +|---|---| +| `docs/SCHEMA.md` | Authoritative domain model in TOON format — all entities, DTOs, enums, relationships | +| `docs/DOMAIN_MODEL.md` | Behavioral docs: weight formula, MeasuringStatus lifecycle, GenericAttribute keys, entity hierarchy overview | +| `docs/MEASUREMENT.md` | Three measurement workflows (inbound shipping, outbound order, stocktaking), MeasuringStatus lifecycle, pricing, checkout filter | +| `docs/AI_SERVICES.md` | OpenAI, Cerebras, Replicate providers, FruitBankSettings, file storage, PDF conversion, audio transcription | +| `docs/DATA_LAYER.md` | FruitBankDbContext, StockTakingDbContext, DbTable repositories, entity mapping, FruitBankEventConsumer | +| `docs/SIGNALR_ENDPOINTS.md` | SignalR endpoints, FruitBankDataController, InnVoice/Billingo integration, FruitBankAttributeService | + ## Folder Structure -| Folder | Purpose | Key Types | -|---|---|---| -| `Areas/Admin/Controllers/` | Admin-side MVC controllers — order, product, shipping, SignalR endpoints, file management | `CustomOrderController`, `CustomOrderSignalREndpoint`, `ShippingController`, `StockSignalREndpointServer`, `CustomProductController` | -| `Areas/Admin/Components/` | Admin ViewComponents — DevExtreme grids | `ShippingGridComponent`, `ShippingDocumentGridComponent`, `PartnersGridComponent`, `FileUploadGridComponent` | -| `Areas/Admin/Models/` | Admin view models — shipping, order, product, app download | `ShippingModel`, `ShippingSearchModel`, `OrderSearchModelExtended`, `GridBaseViewModel` | -| `Areas/Admin/Views/` | Admin Razor views | | -| `Components/` | Public-side ViewComponents | | -| `Controllers/` | Public-side controllers | `CheckoutController`, `FruitBankDataController` | -| `Domains/` | Entity configuration + data layer | `CustomTable`, `FruitBankDbContext` | -| `Domains/DataLayer/` | DbTable classes — EF Core repository per entity | `ShippingDbTable`, `OrderDtoDbTable`, `ShippingItemPalletDbTable`, etc. | -| `Domains/DataLayer/Interfaces/` | Repository interfaces | `IShippingDbSet`, `IOrderDtoDbSet`, `IShippingItemPalletDbSet`, etc. | -| `Domains/EventConsumers/` | nopCommerce entity event handlers | `FruitBankEventConsumer` | -| `Factories/` | Model factories — build admin models from entities | `ShippingModelFactory`, `MgOrderModelFactory`, `MgProductModelFactory` | -| `Filters/` | MVC action filters | `PendingMeasurementCheckoutFilter` | -| `Helpers/` | Utility helpers | `TextHelper` | -| `Infrastructure/` | Plugin startup, routing, view engine | `PluginNopStartup`, `RouteProvider`, `ViewLocationExpander` | -| `Mapping/` | EF Core entity mapping builders | `PluginBuilder`, `NameCompatibility` | -| `Migrations/` | Database schema migrations | `SchemaMigration` | -| `Models/` | Shared models — AI chat, MgBase extensions | `AIChatMessage`, `MgOrderModelExtended`, `MgProductModelExtended` | -| `Models/Orders/` | Order-specific admin models | `OrderModelExtended`, `OrderAttributesModel`, `OrderRevisionModel` | -| `Models/Products/` | Product-specific admin models | `ProductModelExtended`, `ProductAttributesModel` | -| `Services/` | Business logic services | See below | - -## Key Services - -| Service | Purpose | +| Folder | Purpose | |---|---| -| `MeasurementService` / `IMeasurementService` | Shipping measurement logic (weighing pallets) | -| `OrderMeasurementService` | Order measurement logic (outbound weighing) | -| `FruitBankHub` | SignalR hub — real-time communication with FruitBankHybridApp clients | -| `FruitBankAttributeService` | GenericAttribute CRUD for FruitBank-specific attributes | -| `FileStorageService` / `IFileStorageProvider` | File upload/download, local storage | -| `LockService` / `ILockService` | Distributed locking | -| `CustomPriceCalculationService` | Custom price calculation override | -| `AICalculationService` | AI-powered calculations | -| `OpenAIApiService` / `OpenAIService` | OpenAI GPT integration | -| `CerebrasAPIService` | Cerebras AI API integration | -| `InnvoiceApiService` / `InnVoiceOrderService` | Billingo/Innvoice integration | -| `PdfToImageService` | PDF rendering to images | - -## SignalR Endpoints - -The plugin exposes SignalR endpoints consumed by FruitBankHybridApp: - -| Endpoint | File | Role | -|---|---|---| -| `CustomOrderSignalREndpoint` | `Areas/Admin/Controllers/` | Order CRUD + measurement via SignalR | -| `StockSignalREndpointServer` | `Areas/Admin/Controllers/` | StockTaking CRUD via SignalR | -| `FruitBankHub` | `Services/` | Main SignalR hub | +| `Areas/Admin/Controllers/` | Admin MVC controllers — order, product, shipping, SignalR endpoints, file management | +| `Areas/Admin/Components/` | Admin ViewComponents — DevExtreme grids | +| `Areas/Admin/Models/` | Admin view models | +| `Areas/Admin/Views/` | Admin Razor views | +| `Components/` | Public-side ViewComponents | +| `Controllers/` | Public-side controllers — `FruitBankDataController`, `CheckoutController` | +| `Domains/DataLayer/` | DbContexts, DbTable repositories, interfaces | +| `Domains/EventConsumers/` | nopCommerce entity event handlers | +| `Factories/` | Model factories (order, product, shipping) | +| `Filters/` | `PendingMeasurementCheckoutFilter` | +| `Infrastructure/` | `PluginNopStartup` (DI), `RouteProvider`, `ViewLocationExpander` | +| `Mapping/` | EF Core entity mapping, `PluginBuilder` | +| `Migrations/` | FluentMigrator database schema | +| `Models/` | Shared models — AI chat, MgBase extensions, order/product models | +| `Services/` | Business logic — AI, measurement, file storage, invoicing, locking | ## Dependencies - `Mango.Nop.Core`, `Mango.Nop.Services` (ProjectReferences via `../../Libraries/`) - `Nop.Services`, `Nop.Web` (nopCommerce ProjectReferences) -- DevExpress ASP.NET Core 25.1.3 +- AyCode.Core solution assemblies (DLL HintPaths in .csproj) +- DevExpress ASP.NET Core 25.1.3, DevExtreme 25.1.3 - SignalR, EF Core 9.0, PdfPig, PDFtoImage + +## Initialization & Quirks + +- **AcBinary Source Generator Registration**: NopCommerce plugins are loaded dynamically via `AssemblyLoadContext`, which prevents the .NET runtime from executing `[ModuleInitializer]` attributes. To ensure the high-performance binary serializers are registered (instead of falling back to slow reflection), `PluginNopStartup.ConfigureServices` must call `Mango.Nop.Core.AcBinaryForcedInit.ForceRegister()`. diff --git a/Nop.Plugin.Misc.AIPlugin/docs/AI_SERVICES.md b/Nop.Plugin.Misc.AIPlugin/docs/AI_SERVICES.md new file mode 100644 index 0000000..1f56b91 --- /dev/null +++ b/Nop.Plugin.Misc.AIPlugin/docs/AI_SERVICES.md @@ -0,0 +1,155 @@ +# AI Services, File Storage, and PDF + +> Part of `Nop.Plugin.Misc.FruitBankPlugin`. See `README.md` for project overview. + +## FruitBankSettings + +Plugin configuration stored in nopCommerce settings table (`ISettings`). Retrieved via `ISettingService`. + +| Property | Type | Purpose | +|---|---|---| +| `ApiKey` | string | Cerebras API key | +| `ModelName` | string | Cerebras model name | +| `ApiBaseUrl` | string | Cerebras API base URL | +| `OpenAIApiKey` | string | OpenAI API key | +| `OpenAIModelName` | string | OpenAI model name | +| `OpenAIApiBaseUrl` | string | OpenAI API base URL | +| `MaxTokens` | int | Max response tokens | +| `Temperature` | double | Sampling temperature | +| `RequestTimeoutSeconds` | int | HTTP timeout | +| `IsEnabled` | bool | Plugin enabled flag | + +Configured via admin UI: `FruitBankPluginAdminController` → Configure view. + +## IAIAPIService Interface + +Common contract for AI providers. + +| Method | Signature | Purpose | +|---|---|---| +| `GetSimpleResponseAsync` | `(List, CancellationToken?) -> Task` | Non-streaming chat completion | +| `GetStreamedResponseAsync` | `(List, CancellationToken?) -> IAsyncEnumerable` | Streaming chat completion | +| `GetApiKey` | `() -> string` | Returns configured API key | +| `GetModelName` | `() -> string` | Returns configured model name | + +**Chat message model:** + +``` +AIChatMessage { Role: "user"|"assistant"|"system", Content: string } +``` + +## OpenAIApiService + +Primary AI provider. Implements `IAIAPIService` plus additional capabilities. + +| Method | Purpose | +|---|---| +| `GetSimpleResponseAsync()` | Chat completion via OpenAI API | +| `GetStreamedResponseAsync()` | Streaming chat via SSE | +| `TranscribeAudioAsync(byte[] audioData, string language)` | Whisper API audio-to-text transcription | +| `GenerateImageAsync(string prompt)` | DALL-E image generation | +| `ExtractTextFromImageAsync(byte[] imageBytes)` | Vision API — extracts text from image | +| `AnalyzePdfWithAssistant(byte[] pdfBytes, string prompt)` | Vector store + Assistant API for PDF analysis | +| `UploadFileAsync(byte[], string purpose)` | File upload to OpenAI | +| `CreateVectorStoreAsync(string fileId)` | Creates vector store from uploaded file | +| `CreateThreadAndRunAsync(string vectorStoreId, string prompt)` | Runs assistant with vector store | + +## CerebrasAPIService + +Alternative fast-inference AI provider. Implements `IAIAPIService`. + +| Method | Purpose | +|---|---| +| `GetSimpleResponseAsync()` | Chat completion via Cerebras API | +| `GetStreamedResponseAsync()` | Streaming chat via Cerebras API | + +Tracks token usage via logging. + +## ReplicateService + +Image generation and processing via Replicate.com API. + +| Method | Purpose | +|---|---| +| `GenerateImageAsync(string prompt)` | Image generation (Flux/Imagen model) | +| `GenerateLogoAsync(string prompt)` | Specialized logo generation | +| `RemoveBackgroundAsync(byte[] imageBytes)` | Background removal | +| `AnalyzeImageAsync(byte[] imageBytes, string prompt)` | Image analysis/description | + +Uses `HttpClient` configured in `PluginNopStartup` with Replicate API token header. + +## OpenAIService + +Simple wrapper for quick single-prompt queries. + +| Method | Signature | Purpose | +|---|---|---| +| `AskAsync` | `(string prompt) -> Task` | Single prompt → response using GPT-4o-mini | + +## AICalculationService + +AI-powered business intelligence for the admin dashboard. + +| Method | Purpose | +|---|---| +| `GetWelcomeMessageAsync()` | Generates personalized admin dashboard briefing | + +**Aggregates:** +- Recent order data (count, totals, status distribution) +- Stock levels and discrepancies +- Weather information +- Flags inventory anomalies and processing issues + +Called from `CustomDashboardController`. + +## FileStorageService + +File upload/download service with deduplication and compression. + +| Method | Purpose | +|---|---| +| `SaveFileAsync(byte[], string fileName, ...)` | Save with hash calculation and optional compression | +| `GetFileAsync(int fileId)` | Retrieve file with automatic decompression if compressed | +| `SearchByFileNameAsync(string fileName)` | Search by original filename | +| `SearchByHashAsync(string hash)` | Find by SHA256 hash | +| `SearchByRawTextAsync(string text)` | Full-text search in extracted OCR content | +| `CalculateFileHashAsync(byte[])` | SHA256 hash calculation | + +**Features:** +- **Deduplication**: Checks SHA256 hash before saving. Returns existing file if duplicate found +- **Compression**: Auto-compresses with GZip for non-compressed formats (excludes .jpg, .png, .gif, .zip, .gz, .pdf) +- **RawText**: Stores OCR-extracted text for full-text search +- **Metadata**: Tracks FileName, FileExtension, FileHash, FileSubPath, IsCompressed, Created/Modified + +## IFileStorageProvider / LocalStorageProvider + +Pluggable storage backend. Currently only `LocalStorageProvider` implemented. + +| Method | Purpose | +|---|---| +| `SaveFileAsync(byte[], string relativePath)` | Save to `wwwroot/uploads/{relativePath}` | +| `GetFileAsync(string relativePath)` | Read file bytes | +| `DeleteFileAsync(string relativePath)` | Delete file, cleanup empty parent directories | +| `FileExistsAsync(string relativePath)` | Check file existence | + +Files organized by relative sub-paths. Auto-creates directories as needed. + +## PdfToImageService + +PDF rendering using PDFtoImage library (Pdfium native backend). + +| Method | Purpose | +|---|---| +| `ConvertPdfToJpgAsync(byte[] pdfBytes)` | Converts all PDF pages to JPG images | + +- 300 DPI rendering, white background +- Returns `List` — one JPG per page +- Pdfium native library bootstrapped on first use + +## Audio Transcription Flow + +1. `FruitBankAudioController` receives audio recording +2. Calls `OpenAIApiService.TranscribeAudioAsync(audioData, language)` +3. Sends to Whisper API endpoint (`/v1/audio/transcriptions`) +4. Returns transcribed text +5. Used for voice-based order entry in FruitBankHybridApp diff --git a/Nop.Plugin.Misc.AIPlugin/docs/DATA_LAYER.md b/Nop.Plugin.Misc.AIPlugin/docs/DATA_LAYER.md new file mode 100644 index 0000000..48938a5 --- /dev/null +++ b/Nop.Plugin.Misc.AIPlugin/docs/DATA_LAYER.md @@ -0,0 +1,174 @@ +# Data Layer + +> Part of `Nop.Plugin.Misc.FruitBankPlugin`. See `README.md` for project overview. +> For entity model see `docs/DOMAIN_MODEL.md`. +> For measurement workflows see `docs/MEASUREMENT.md`. + +## DbContext Classes + +### FruitBankDbContext + +Inherits `MgDbContextBase` (from `Mango.Nop.Data`). Main data context for orders, shipping, partners, and files. + +**Implements interfaces:** IOrderDtoDbSet, IOrderItemDtoDbSet, IPartnerDbSet, IShippingDbSet, IShippingDocumentDbSet, IShippingItemDbSet, IShippingItemPalletDbSet, IOrderItemPalletDbSet, IShippingDocumentToFilesDbSet, IFilesDbSet + +**DbSet properties:** + +| DbSet | Entity | Table | +|---|---|---| +| `OrderDtos` | OrderDto | Order | +| `OrderItemDtos` | OrderItemDto | OrderItem | +| `OrderItemPallets` | OrderItemPallet | fbOrderItemPallet | +| `Partners` | Partner | fbPartner | +| `Shippings` | Shipping | fbShipping | +| `ShippingDocuments` | ShippingDocument | fbShippingDocument | +| `ShippingItems` | ShippingItem | fbShippingItem | +| `ShippingItemPallets` | ShippingItemPallet | fbShippingItemPallet | +| `Files` | Files | fbFiles | +| `ShippingDocumentToFiles` | ShippingDocumentToFiles | fbShippingDocumentToFiles | +| `ProductDtos` | ProductDto | Product | +| `GenericAttributeDtos` | GenericAttributeDto | GenericAttribute | +| `StockQuantityHistoryDtos` | StockQuantityHistoryDto | StockQuantityHistory | +| `Customers` | Customer | Customer | +| `CustomerRoles` | CustomerRole | CustomerRole | + +**Key methods:** + +| Method | Purpose | +|---|---| +| `AddShippingItemAsync()` | Creates item, copies `IsMeasurable` from ProductDto | +| `UpdateShippingItemAsync()` | Complex: updates measuring values, handles stock/weight changes, manages product transitions | +| `SetupShippingItemMeasuringValues()` | Recalculates totals from child pallets | +| `AddShippingItemPalletAsync()` / `UpdateShippingItemPalletAsync()` | Pallet CRUD with cascade to parent ShippingItem | +| `StartMeasuringAsync()` | Sets MeasurementOwnerId GenericAttribute, updates order status | +| `SetOrderStatusToCompleteAsync()` | Validates pallets, updates stock, sets RevisorId, publishes event | +| `DeleteOrderItemConstraintsAsync()` | Cleanup when order items deleted | +| `UpdateStockQuantityAndWeightAsync()` | Updates Product.StockQuantity + NetWeight, creates history entry | +| `DeleteShippingSafeAsync()` | Transactional cascade delete of shipping + children | +| `DeleteShippingDocumentSafeAsync()` | Transactional cascade delete of document + children | +| `GetCustomersBySystemRoleName()` | LINQ query for role-based customer lookup | + +All state-changing operations wrapped in `TransactionSafeAsync()` for ACID compliance. + +### StockTakingDbContext + +Inherits `MgDbContextBase`. Dedicated context for inventory sessions. + +**DbSet properties:** + +| DbSet | Entity | +|---|---| +| `ProductDtos` | ProductDto | +| `OrderItemDtos` | OrderItemDto | +| `StockTakings` | StockTaking | +| `StockTakingItems` | StockTakingItem | +| `StockTakingItemPallets` | StockTakingItemPallet | +| `StockQuantityHistories` | StockQuantityHistory | +| `GenericAttributes` | GenericAttribute | + +**Key methods:** + +| Method | Purpose | +|---|---| +| `CloseStockTaking()` | Validates all measured, updates stock/weight per item, marks closed | +| `AddStockTakingItemPallet()` / `UpdateStockTakingItemPallet()` | Pallet CRUD with measured values refresh | +| `RefreshStockTakingItemMeasuredValuesFromPallets()` | Sums quantities/weights from pallets | + +## DbTable Repositories + +Each entity has a `*DbTable` class inheriting `MgDbTableBase` (from `Mango.Nop.Data`). Located in `Domains/DataLayer/`. + +### ShippingDbTable + +| Method | Purpose | +|---|---| +| `GetAll()` | All shippings ordered by ShippingDate | +| `GetAll(bool loadRelations)` | Eager loads full graph: Documents → Items → Pallets + Pallet type + ProductDto + GenericAttributes | +| `GetAllNotMeasured()` | Filters for incomplete/recent shipments | +| `GetByIdAsync(bool loadRelations)` | Single with optional eager loading | +| `OnUpdate()` | Hook: sets `MeasuredDate` when `IsAllMeasured` becomes true | + +### OrderDtoDbTable + +| Method | Purpose | +|---|---| +| `GetAll(bool loadRelations)` | Eager loads: GenericAttributes, Customer, OrderNotes, OrderItemDtos (with ProductDto, GenericAttributes, OrderItemPallets) | +| `GetByIdAsync(bool loadRelations)` | Single order with optional relations | +| `GetAllByOrderStatus()` | Status-filtered query | +| `GetAllForMeasuring(DateTime fromDate)` | Unpaid, not cancelled, with DateOfReceipt >= fromDate | +| `GetAllByProductId()` / `GetAllByProductIds()` | Product-filtered queries | +| `GetAllByIds()` | Batch retrieval | + +### Other DbTable classes + +| Class | Entity | Key behaviors | +|---|---|---| +| `ProductDtoDbTable` | ProductDto | GetAll with GenericAttributes | +| `PartnerDbTable` | Partner | Standard CRUD | +| `ShippingDocumentDbTable` | ShippingDocument | GetAll with items, files, partner | +| `ShippingItemDbTable` | ShippingItem | GetAll with pallets | +| `ShippingItemPalletDbTable` | ShippingItemPallet | Standard CRUD | +| `OrderItemDtoDbTable` | OrderItemDto | GetAll with pallets, product | +| `OrderItemPalletDbTable` | OrderItemPallet | Standard CRUD | +| `FilesDbTable` | Files | Standard CRUD | +| `ShippingDocumentToFilesDbTable` | ShippingDocumentToFiles | Standard CRUD | +| `StockTakingDbTable` | StockTaking | GetAll with items, pallets | +| `StockTakingItemDbTable` | StockTakingItem | GetAll with pallets | +| `StockTakingItemPalletDbTable` | StockTakingItemPallet | Standard CRUD | +| `StockQuantityHistoryDtoDbTable` | StockQuantityHistoryDto | Filtered by date range | + +## Repository Interfaces + +Located in `Domains/DataLayer/Interfaces/`. Each interface (e.g., `IShippingDbSet`, `IOrderDtoDbSet`) declares the DbSet property that the DbContext must implement. This allows DbTable classes to accept any context that provides the required DbSet. + +## Entity Mapping + +- `Mapping/Builders/PluginBuilder.cs` — EF Core Fluent API entity configuration for plugin-owned tables (fb* prefix) +- `Mapping/NameCompatibility.cs` — Table name mapping for nopCommerce compatibility +- `Migrations/SchemaMigration.cs` — FluentMigrator schema setup for `CustomTable` base entity + +## FruitBankEventConsumer + +Located in `Domains/EventConsumers/`. Extends `MgEventConsumerBase` (from `Mango.Nop.Services`). Handles entity lifecycle events for cascading updates. + +**Product events:** + +| Event | Action | +|---|---| +| `EntityInsertedEvent` | Saves custom attributes (IsMeasurable, NetWeight, Tare, AverageWeight, AverageWeightTreshold, IncomingQuantity) | +| `EntityUpdatedEvent` | Updates custom attributes, syncs IsMeasurable/Tare changes to existing ShippingItems | + +**Shipping cascade events:** + +| Event | Action | +|---|---| +| `EntityDeletedEvent` | Refreshes parent ShippingItem measuring values | +| `EntityInsertedEvent/UpdatedEvent` | Rechecks ShippingDocument.IsAllMeasured | +| `EntityInsertedEvent/UpdatedEvent` | Rechecks Shipping.IsAllMeasured | +| `EntityDeletedEvent` | Cascade deletes child documents | +| `EntityDeletedEvent` | Cascade deletes child items | +| `EntityDeletedEvent` | Cascade deletes child pallets | + +**Order events:** + +| Event | Action | +|---|---| +| `EntityDeletedEvent` | Cleanup via `MeasurementService.DeleteOrderItemConstraintsAsync()` | +| `EntityInsertedEvent/UpdatedEvent` | Post-process via `MeasurementService.OrderItemInsertedOrUpdatedPostProcess()` | + +## Eager Loading Pattern + +DbTable `GetAll(bool loadRelations = true)` uses `LoadWith()` chains to eagerly load entity graphs. Example for OrderDtoDbTable: + +``` +OrderDto + → GenericAttributes + → Customer + → OrderNotes + → OrderItemDtos + → ProductDto → GenericAttributes + → GenericAttributes + → OrderItemPallets +``` + +This avoids N+1 queries when transferring full entity graphs to FruitBankHybridApp via SignalR. diff --git a/Nop.Plugin.Misc.AIPlugin/docs/DOMAIN_MODEL.md b/Nop.Plugin.Misc.AIPlugin/docs/DOMAIN_MODEL.md new file mode 100644 index 0000000..46ef545 --- /dev/null +++ b/Nop.Plugin.Misc.AIPlugin/docs/DOMAIN_MODEL.md @@ -0,0 +1,116 @@ +# Domain Model — Behavioral Documentation + +> Part of `Nop.Plugin.Misc.FruitBankPlugin`. See `README.md` for project overview. +> For entity definitions, properties, and relationships see `docs/SCHEMA.md` (authoritative TOON schema). +> For measurement workflows see `docs/MEASUREMENT.md`. +> For data layer details see `docs/DATA_LAYER.md`. +> For core measurement system rules and common domain traps, see: `../../../../../FruitBankHybridApp/FruitBank.Common/docs/GLOSSARY.md` + +This document explains **how the domain model behaves** in the plugin. For **what the entities are** (properties, types, relationships), see `docs/SCHEMA.md`. + +## Weight Formula + +All three pallet types (ShippingItemPallet, OrderItemPallet, StockTakingItemPallet) use the same formula: + +``` +NetWeight = GrossWeight - PalletWeight - (TrayQuantity * TareWeight) +``` + +| Field | Meaning | +|---|---| +| `GrossWeight` | Scale reading (total weight). 0.0 if product is not measurable | +| `PalletWeight` | Weight of the physical pallet. 0.0 if goods arrive without pallet | +| `TareWeight` | Weight of one tray/crate (per unit) | +| `TrayQuantity` | Number of trays/crates. Always recorded, even for non-measurable products | +| `NetWeight` | Computed, read-only, not-mapped (setter exists for shared interface but throws Exception) | + +## MeasuringStatus Lifecycle + +``` +NotStarted (0) → Started (10) → Finnished (20) → Audited (30) + [no record] [record created] [IsMeasured=true] [RevisorId > 0] +``` + +- **NotStarted**: No pallet record exists yet +- **Started**: Pallet record created (Id > 0) but `IsMeasured = false` +- **Finnished**: `IsMeasured = true` — weights recorded +- **Audited**: `RevisorId > 0` — quality auditor confirmed (OrderItemPallet only) + +## IsMeasurable Flag + +`ProductDto.IsMeasurable` is the master toggle (stored as GenericAttribute). When `false`: +- Weight validation is bypassed +- One pallet record is still created with `GrossWeight = 0.0` +- Only `TrayQuantity` is recorded +- Product is not included in average weight checks + +## GenericAttribute Keys + +Custom product/order properties are stored via nopCommerce's GenericAttribute table. The DTO classes expose these as computed, not-mapped properties. + +### Product attributes (saved by FruitBankEventConsumer) + +| Key | Type | Purpose | +|---|---|---| +| `IsMeasurable` | bool | Master flag for weight validation | +| `NetWeight` | double | Product net weight | +| `Tare` | double | Tare weight per tray | +| `AverageWeight` | double | Expected average weight per tray | +| `AverageWeightTreshold` | double | Max allowed deviation % from AverageWeight | +| `IncomingQuantity` | int | Incoming stock not yet received | + +### Order attributes + +| Key | Type | Purpose | +|---|---|---| +| `DateOfReceipt` | DateTime | Scheduled delivery date | +| `MeasurementOwnerId` | int | User who started measuring | +| `RevisorId` | int | Quality auditor user ID | + +## Entity Hierarchy Overview + +### Inbound Delivery (Shipping) + +``` +Shipping (fbShipping) ← truck arrival + └─ ShippingDocument (fbShippingDocument) ← supplier delivery note [1:N] + ├─ ShippingItem (fbShippingItem) ← product line [1:N] + │ └─ ShippingItemPallet ← measurement event [1:N] + ├─ ShippingDocumentToFiles ← uploaded PDFs [1:N] + │ └─ Files (fbFiles) [N:1] + └─ Partner (fbPartner) ← supplier [N:1] +``` + +### Outbound Order + +``` +OrderDto (Order) ← nopCommerce order + ├─ OrderItemDto (OrderItem) ← line item [1:N] + │ ├─ OrderItemPallet (fbOrderItemPallet) ← measurement record [1:N] + │ └─ ProductDto (Product) [N:1] + ├─ Customer [N:1] + └─ GenericAttributeDto ← custom attributes [1:N] +``` + +### Inventory (StockTaking) + +``` +StockTaking (fbStockTaking) ← inventory session + └─ StockTakingItem (fbStockTakingItem) ← product line [1:N] + ├─ StockTakingItemPallet ← measurement record [1:N] + └─ ProductDto (Product) [N:1] +``` + +## FullProcessModel + +Container for bulk data sync between server and FruitBankHybridApp via SignalR: + +| Property | Type | Purpose | +|---|---|---| +| `Orders` | `List` | All active orders with full graph | +| `Shippings` | `List` | All active shipments with full graph | +| `StockTakings` | `List` | All active inventory sessions | + +## Note on "Pallet" Naming + +The term "Pallet" in `OrderItemPallet`, `ShippingItemPallet`, and `StockTakingItemPallet` is a legacy naming convention. These are **general measurement records** that are always created for every item, regardless of whether goods arrive on a physical pallet. For non-measurable products, weight fields are 0.0 and only `TrayQuantity` is tracked. diff --git a/Nop.Plugin.Misc.AIPlugin/docs/MEASUREMENT.md b/Nop.Plugin.Misc.AIPlugin/docs/MEASUREMENT.md new file mode 100644 index 0000000..8a27ce3 --- /dev/null +++ b/Nop.Plugin.Misc.AIPlugin/docs/MEASUREMENT.md @@ -0,0 +1,175 @@ +# Measurement Workflows + +> Part of `Nop.Plugin.Misc.FruitBankPlugin`. See `README.md` for project overview. +> For entity model see `docs/DOMAIN_MODEL.md`. +> For SignalR endpoints see `docs/SIGNALR_ENDPOINTS.md`. +> For core measurement system rules and common domain traps, see: `../../../../../FruitBankHybridApp/FruitBank.Common/docs/GLOSSARY.md` + +Three parallel measurement workflows share the same weight formula and `MeasuringStatus` lifecycle. + +## MeasuringStatus Lifecycle + +``` +NotStarted (0) → Started (10) → Finnished (20) → Audited (30) + [no record] [record created] [IsMeasured=true] [RevisorId > 0] +``` + +- **NotStarted**: No pallet record exists yet +- **Started**: Pallet record created (Id > 0) but `IsMeasured = false` +- **Finnished**: `IsMeasured = true` — weights recorded +- **Audited**: `RevisorId > 0` — quality auditor confirmed (OrderItemPallet only) + +## IsMeasurable Flag + +`ProductDto.IsMeasurable` is the master toggle (stored as GenericAttribute). When `false`: +- Weight validation is bypassed +- One pallet record is still created with `GrossWeight = 0.0` +- Only `TrayQuantity` is recorded (for tare-weight calculations) +- Product is not included in average weight checks + +## Workflow 1: Inbound Shipping + +Supplier truck arrives at the warehouse. Goods are weighed and compared to the delivery document. + +``` +Shipping (truck) + → ShippingDocument (supplier delivery note) + → ShippingItem (product line) + → ShippingItemPallet (individual measurement) +``` + +### Flow + +1. **Create Shipping** — truck registration (LicencePlate, ShippingDate, CargoCompany) +2. **Add ShippingDocument** — link to Partner (supplier), upload PDF delivery note +3. **Add ShippingItem** — product line from document. `IsMeasurable` copied from `ProductDto.IsMeasurable` +4. **Weigh pallets** — `FruitBankDataController.AddOrUpdateMeasuredShippingItemPallet()` + - Each pallet: GrossWeight, PalletWeight, TareWeight, TrayQuantity + - NetWeight computed: `GrossWeight - PalletWeight - (TrayQuantity * TareWeight)` +5. **Cascade update** — after each pallet measurement: + - `ShippingItem`: MeasuredGrossWeight, MeasuredNetWeight, MeasuredQuantity recalculated from pallets + - `ShippingDocument.IsAllMeasured` = all items measured + - `Shipping.IsAllMeasured` = all documents measured + - Product `AverageWeight` updated from measured data + +### Event Cascade (FruitBankEventConsumer) + +``` +ShippingItemPallet deleted → ShippingItem measuring values refreshed +ShippingItem inserted/updated → ShippingDocument.IsAllMeasured rechecked +ShippingDocument inserted/updated → Shipping.IsAllMeasured rechecked +Shipping deleted → cascade delete ShippingDocuments +ShippingDocument deleted → cascade delete ShippingItems +ShippingItem deleted → cascade delete ShippingItemPallets +``` + +### ShippingItem: Declared vs Measured + +| Declared (from document) | Measured (warehouse) | Discrepancy tracked | +|---|---|---| +| `QuantityOnDocument` | `MeasuredQuantity` | Yes | +| `GrossWeightOnDocument` | `MeasuredGrossWeight` | Yes | +| `NetWeightOnDocument` | `MeasuredNetWeight` | Yes | + +## Workflow 2: Outbound Order + +Customer order is prepared. Items are weighed before dispatch. + +``` +OrderDto (order) + → OrderItemDto (line item) + → OrderItemPallet (measurement record) +``` + +### Flow + +1. **Start Measuring** — `CustomOrderSignalREndpoint.StartMeasuring(orderId, userId)` + - Sets `MeasurementOwnerId` via GenericAttribute + - Order status set to Processing, Payment to Pending + - Broadcasts `SendOrderChanged` +2. **Weigh pallets** — `CustomOrderSignalREndpoint.AddOrUpdateMeasuredOrderItemPallet()` + - Same weight formula as shipping + - Triggers price recalculation after each pallet + - Broadcasts `SendOrderItemPalletChanged` +3. **Complete order** — `CustomOrderSignalREndpoint.SetOrderStatusToComplete(orderId, revisorId)` + - Validates: all order items must have pallets + - Recalculates final prices via `CustomPriceCalculationService` + - Updates product stock quantities and weights + - Sets `RevisorId` on each OrderItemPallet → MeasuringStatus becomes Audited + - Order status → Complete + - Publishes `OrderPaidEvent` for nopCommerce integration + +### Average Weight Validation + +After measuring, `OrderItemDto.AverageWeightIsValid` checks: + +``` +deviation = |ProductDto.AverageWeight - OrderItemDto.AverageWeight| +percentage = (deviation / ProductDto.AverageWeight) * 100 +valid = percentage < ProductDto.AverageWeightTreshold +``` + +`OrderDto.IsAllOrderItemAvgWeightValid` = all items pass this check. + +### PendingMeasurementCheckoutFilter + +ASP.NET Core action filter on the Checkout `ConfirmOrder` action. + +| Step | Action | +|---|---| +| Intercept | `ConfirmOrder` POST in CheckoutController | +| Check | `OrderMeasurementService.IsPendingMeasurementAsync()` | +| If pending | Set OrderStatus=Processing, PaymentStatus=Pending, redirect to PendingMeasurementWarning | +| If not pending | Allow normal checkout flow | + +## Workflow 3: StockTaking + +Inventory session compares logical stock with physical count. + +``` +StockTaking (session) + → StockTakingItem (product line) + → StockTakingItemPallet (measurement record) +``` + +### Flow + +1. **Create session** — `StockSignalREndpointServer.AddStockTaking()` + - Auto-populates `StockTakingItem` for each product + - Captures `OriginalStockQuantity` (current stock) and `InProcessOrdersQuantity` (reserved in pending orders) + - `TotalOriginalQuantity = OriginalStockQuantity + InProcessOrdersQuantity` +2. **Weigh items** — `StockSignalREndpointServer.AddOrUpdateMeasuredStockTakingItemPallet()` + - Same weight formula + - Refreshes `MeasuredStockQuantity` and `MeasuredNetWeight` from pallets +3. **Close session** — `StockSignalREndpointServer.CloseStockTaking()` + - Validates: all items must be measured + - For each item: `QuantityDiff = MeasuredStockQuantity - TotalOriginalQuantity` + - Updates `Product.StockQuantity` with the diff + - Updates `Product.NetWeight` with measured weight + - Creates stock quantity history entries + - Marks session as closed (`IsClosed = true`) + +## CustomPriceCalculationService + +Extends nopCommerce's `PriceCalculationService`. Overrides order item pricing based on measurement data. + +| Scenario | Price calculation | +|---|---| +| Measurable product | `FinalPrice = NetWeight * UnitPriceExclTax` (weight-based) | +| Non-measurable product | `FinalPrice = Quantity * UnitPriceExclTax` (standard quantity-based) | + +**Methods:** + +| Method | Purpose | +|---|---| +| `CalculateOrderItemFinalPrice(OrderItemDto)` | Single item price calculation | +| `CheckAndUpdateOrderItemFinalPricesAsync(int orderId)` | Recalculates all items in an order | +| `CheckAndUpdateOrderTotalPrice(int orderId)` | Updates order total from item sum | + +## Stock Update Flow + +When an order is completed or a stocktaking session closes, `FruitBankDbContext.UpdateStockQuantityAndWeightAsync()`: + +1. Updates `Product.StockQuantity` (add/subtract delta) +2. Updates `Product.NetWeight` via GenericAttribute +3. Creates `StockQuantityHistory` entry with extension record for audit trail diff --git a/Nop.Plugin.Misc.AIPlugin/docs/SCHEMA.md b/Nop.Plugin.Misc.AIPlugin/docs/SCHEMA.md new file mode 100644 index 0000000..b18dc77 --- /dev/null +++ b/Nop.Plugin.Misc.AIPlugin/docs/SCHEMA.md @@ -0,0 +1,637 @@ +# Domain Model Schema (Toon Format) + +> Part of `Nop.Plugin.Misc.FruitBankPlugin`. See `README.md` for project overview. +> Full domain model in Toon (Token-Oriented Object Notation) format — see `AyCode.Core/Serializers/Toons/README.md` (in AyCode.Core solution). +> This is the authoritative schema for entities, DTOs, and enums in the FruitBank domain. +> For behavioral documentation (workflows, lifecycles, event cascades) see `docs/DOMAIN_MODEL.md`. + +@meta { + version = "1.0" + format = "toon" + source-code-language = "C#" + context = "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." + types = ["OrderStatus", "ShippingStatus", "PaymentStatus", "GenericAttributeDto", "MeasuringStatus", "VatNumberStatus", "TaxDisplayType", "OrderNote", "DocumentType", "Files", "Pallet", "ProductDto", "Customer", "FullProcessModel", "OrderDto", "OrderItemDto", "OrderItemPallet", "Partner", "Shipping", "ShippingDocument", "ShippingDocumentToFiles", "ShippingItem", "ShippingItemPallet", "StockTaking", "StockTakingItem", "StockTakingItemPallet"] +} + +@types { + OrderStatus: enum + underlying-type: "int" + default-value: 10 + values: + Pending = 10 + Processing = 20 + Complete = 30 + Cancelled = 40 + + ShippingStatus: enum + underlying-type: "int" + default-value: 10 + values: + ShippingNotRequired = 10 + NotYetShipped = 20 + PartiallyShipped = 25 + Shipped = 30 + Delivered = 40 + + PaymentStatus: enum + underlying-type: "int" + default-value: 10 + values: + Pending = 10 + Authorized = 20 + Paid = 30 + PartiallyRefunded = 35 + Refunded = 40 + Voided = 50 + + GenericAttributeDto: "Data transfer object for GenericAttribute" + table-name: "GenericAttribute" + related-type: "dto-of GenericAttribute" + CreatedOrUpdatedDateUTC: DateTime? + EntityId: int + constraints: "polymorphic-fk(KeyGroup)" + Key: string + KeyGroup: string + StoreId: int + Value: string + purpose: "Raw string representation of the Key's value" + Id: int + purpose: "Primary key / unique identification" + primary-key: true + + MeasuringStatus: enum + underlying-type: "int" + default-value: 0 + values: + NotStarted = 0 + Started = 10 + Finnished = 20 + Audited = 30 + + VatNumberStatus: enum + underlying-type: "int" + default-value: 0 + values: + Unknown = 0 + Empty = 10 + Valid = 20 + Invalid = 30 + + TaxDisplayType: enum + underlying-type: "int" + default-value: 0 + values: + IncludingTax = 0 + ExcludingTax = 10 + + OrderNote: "NopCommerce order note entity" + table-name: "OrderNote" + CreatedOnUtc: DateTime + DisplayToCustomer: bool + DownloadId: int + Note: string + OrderId: int + description: "Foreign key to parent Order" + Id: int + purpose: "Primary key / unique identification" + primary-key: true + + DocumentType: enum + underlying-type: "int" + default-value: 0 + values: + NotSet = 0 + Unknown = 5 + ShippingDocument = 10 + OrderConfirmation = 15 + Invoice = 20 + + Files: "Uploaded file with extracted text content" + table-name: "fbFiles" + purpose: "A centralized repository for all uploaded binary content and metadata, featuring a 'RawText' field that stores OCR-extracted information for full-text search and automated data validation across the system" + Created: DateTime + FileExtension: string + FileHash: string + FileName: string + FileSubPath: string + IsCompressed: bool + purpose: "Status flag" + Modified: DateTime + RawText: string + Id: int + purpose: "Primary key / unique identification" + primary-key: true + + Pallet: "Pallet type definition with size and weight" + table-name: "fbPallet" + Created: DateTime + Modified: DateTime + Name: string + Size: string + Weight: double? + Id: int + purpose: "Primary key / unique identification" + primary-key: true + + ProductDto: "Product data with measurements and generic attributes" + table-name: "Product" + related-type: "dto-of Product" + AvailableQuantity: int + business-logic: "get => StockQuantity + IncomingQuantity" + constraints: "readonly, not-mapped" + AverageWeight: double + business-logic: "get => GenericAttributes.GetValueOrDefault('AverageWeight')" + constraints: "readonly, not-mapped" + AverageWeightTreshold: double + business-logic: "get => GenericAttributes.GetValueOrDefault('AverageWeightTreshold')" + constraints: "readonly, not-mapped" + GenericAttributes: List + navigation: "one-to-many" + IncomingQuantity: int + business-logic: "get => GenericAttributes.GetValueOrDefault('IncomingQuantity')" + constraints: "not-mapped" + IsMeasurable: bool + purpose: "Master flag: if false, the system bypasses weight validation but still creates one Measurement Record (PalletItem) with TrayQuantity." + business-logic: "get => GenericAttributes.GetValueOrDefault('IsMeasurable')" + constraints: "not-mapped" + NetWeight: double + business-logic: "get => GenericAttributes.GetValueOrDefault('NetWeight')" + constraints: "not-mapped" + Tare: double + business-logic: "get => GenericAttributes.GetValueOrDefault('Tare')" + constraints: "not-mapped" + Deleted: bool + FullDescription: string + Height: decimal + Length: decimal + LimitedToStores: bool + Name: string + ParentGroupedProductId: int + Price: decimal + ProductCost: decimal + ProductTypeId: int + ShortDescription: string + StockQuantity: int + SubjectToAcl: bool + WarehouseId: int + Weight: decimal + Width: decimal + Id: int + purpose: "Primary key / unique identification" + primary-key: true + + Customer: "NopCommerce customer entity" + table-name: "Customer" + Active: bool + AdminComment: string + AffiliateId: int + BillingAddressId: int? + CannotLoginUntilDateUtc: DateTime? + City: string + Company: string + CountryId: int + County: string + CreatedOnUtc: DateTime + CurrencyId: int? + CustomCustomerAttributesXML: string + CustomerGuid: Guid + DateOfBirth: DateTime? + Deleted: bool + Email: string + constraints: "email-format" + EmailToRevalidate: string + constraints: "email-format" + FailedLoginAttempts: int + Fax: string + FirstName: string + Gender: string + HasShoppingCartItems: bool + IsSystemAccount: bool + purpose: "Status flag" + IsTaxExempt: bool + purpose: "Status flag" + LanguageId: int? + constraints: "range: 0-150" + LastActivityDateUtc: DateTime + LastIpAddress: string + LastLoginDateUtc: DateTime? + LastName: string + MustChangePassword: bool + Phone: string + RegisteredInStoreId: int + RequireReLogin: bool + ShippingAddressId: int? + StateProvinceId: int + StreetAddress: string + StreetAddress2: string + SystemName: string + TaxDisplayType: TaxDisplayType? + TaxDisplayTypeId: int? + TimeZoneId: string + Username: string + VatNumber: string + VatNumberStatus: VatNumberStatus + VatNumberStatusId: int + VendorId: int + ZipPostalCode: string + Id: int + purpose: "Primary key / unique identification" + primary-key: true + + FullProcessModel: "Object of type FullProcessModel" + table-name: "FullProcessModel" + purpose: "Container model for Shipping, Order" + Orders: List + navigation: "one-to-many" + Shippings: List + navigation: "one-to-many" + StockTakings: List + navigation: "one-to-many" + + OrderDto: "Data transfer object for Order" + table-name: "Order" + related-type: "dto-of Order" + DateOfReceipt: DateTime? + business-logic: "get => GenericAttributes.GetValueOrNull('DateOfReceipt')" + constraints: "readonly, not-mapped" + DateOfReceiptOrCreated: DateTime + constraints: "readonly, not-mapped" + GenericAttributes: List + navigation: "one-to-many" + IsAllOrderItemAudited: bool + purpose: "Status flag" + business-logic: "get => OrderItemDtos.Count > 0 && OrderItemDtos.All(oi => oi.IsAudited)" + constraints: "readonly, not-mapped" + IsAllOrderItemAvgWeightValid: bool + purpose: "Status flag" + business-logic: "get => OrderItemDtos.All(oi => oi.AverageWeightIsValid)" + constraints: "readonly, not-mapped" + IsComplete: bool + purpose: "Status flag" + business-logic: "get => OrderStatus == OrderStatus.Complete" + constraints: "readonly, not-mapped" + IsMeasurable: bool + purpose: "Status flag" + business-logic: "get => OrderItemDtos.Any(oi => oi.IsMeasurable)" + constraints: "readonly, not-mapped" + IsMeasured: bool + purpose: "Status flag" + business-logic: "get => Id > 0 && OrderItemDtos.Count > 0 && OrderItemDtos.All(x => x.IsMeasured)" + constraints: "readonly, not-mapped" + MeasurementOwnerId: int + business-logic: "get => GenericAttributes.GetValueOrDefault('MeasurementOwnerId', 0)" + constraints: "readonly, not-mapped" + MeasuringStatus: MeasuringStatus + constraints: "readonly, not-mapped" + RevisorId: int + business-logic: "get => GenericAttributes.GetValueOrDefault('RevisorId', 0)" + constraints: "readonly, not-mapped" + TimeOfReceiptText: string + constraints: "readonly, not-mapped" + CreatedOnUtc: DateTime + CustomOrderNumber: string + CustomValuesXml: string + Customer: Customer + foreign-key: "CustomerId" + navigation: "many-to-one" + CustomerId: int + Deleted: bool + OrderDiscount: decimal + OrderGuid: Guid + OrderItemDtos: List + other-key: "OrderId" + navigation: "one-to-many" + inverse-property: "OrderDto" + OrderNotes: List + other-key: "OrderId" + navigation: "one-to-many" + OrderStatus: OrderStatus + purpose: "Enum wrapper" + business-logic: "get, set => OrderStatusId" + OrderStatusId: int + OrderTotal: decimal + PaidDateUtc: DateTime? + PaymentStatus: PaymentStatus + purpose: "Enum wrapper" + business-logic: "get, set => PaymentStatusId" + PaymentStatusId: int + ShippingMethod: string + ShippingStatus: ShippingStatus + purpose: "Enum wrapper" + business-logic: "get, set => ShippingStatusId" + ShippingStatusId: int + StoreId: int + Id: int + purpose: "Primary key / unique identification" + primary-key: true + + OrderItemDto: "Order item with measurements, pallets, and validation" + table-name: "OrderItem" + related-type: "dto-of OrderItem" + AverageWeight: double + business-logic: "get => IsMeasurable && OrderItemPallets.Count > 0 ? double.Round(OrderItemPallets.Sum(oip => oip.AverageWeight) / OrderItemPallets.Count, 1) : 0d" + constraints: "readonly, not-mapped" + AverageWeightDifference: double + business-logic: "get => IsMeasurable ? double.Round(ProductDto!.AverageWeight - AverageWeight, 1) : 0" + constraints: "readonly, not-mapped" + AverageWeightIsValid: bool + business-logic: "get => !IsMeasurable || (ProductDto!.AverageWeight > 0 && ((AverageWeightDifference / ProductDto!.AverageWeight) * 100) < ProductDto!.AverageWeightTreshold)" + constraints: "readonly, not-mapped" + GenericAttributes: List + navigation: "one-to-many" + GrossWeight: double + business-logic: "get => double.Round(OrderItemPallets.Sum(x => x.NetWeight), 1)" + constraints: "not-mapped" + IsAudited: bool + purpose: "Status flag" + business-logic: "get => OrderItemPallets.Count > 0 && OrderItemPallets.All(oip => oip.IsAudited)" + constraints: "readonly, not-mapped" + IsMeasurable: bool + purpose: "Status flag" + business-logic: "get => ProductDto!.IsMeasurable" + constraints: "not-mapped" + IsMeasured: bool + purpose: "Status flag" + business-logic: "get => IsMeasuredAndValid()" + constraints: "not-mapped" + MeasuringStatus: MeasuringStatus + business-logic: "get => complex conditional logic based on IsAudited, IsMeasured, and OrderItemPallets status" + constraints: "readonly, not-mapped" + NetWeight: double + business-logic: "get => double.Round(OrderItemPallets.Sum(x => x.NetWeight), 1)" + constraints: "not-mapped" + OrderDto: OrderDto + foreign-key: "OrderId" + navigation: "many-to-one" + inverse-property: "OrderItemDtos" + OrderItemPallets: List + other-key: "OrderItemId" + navigation: "one-to-many" + inverse-property: "OrderItemDto" + TrayQuantity: int + business-logic: "get => OrderItemPallets.Sum(x => x.TrayQuantity)" + constraints: "not-mapped" + AttributesXml: string + ItemWeight: decimal? + OrderId: int + OrderItemGuid: Guid + PriceExclTax: decimal + PriceInclTax: decimal + ProductDto: ProductDto + foreign-key: "ProductId" + navigation: "many-to-one" + ProductId: int + ProductName: string + business-logic: "get => ProductDto?.Name ?? 'ProductDto is null!!!'" + constraints: "readonly" + Quantity: int + UnitPriceExclTax: decimal + UnitPriceInclTax: decimal + Id: int + purpose: "Primary key / unique identification" + primary-key: true + + OrderItemPallet: "Pallet measurements for order items with audit tracking" + table-name: "fbOrderItemPallet" + purpose: "A measurement record for outgoing goods. NOTE: Despite the 'Pallet' name, this is a general measurement record that is ALWAYS created for every item." + AverageWeight: double + business-logic: "get => double.Round(NetWeight / TrayQuantity, 1)" + constraints: "readonly, not-mapped" + IsAudited: bool + purpose: "Status flag" + business-logic: "get => RevisorId > 0" + constraints: "readonly, not-mapped" + MeasuringStatus: MeasuringStatus + business-logic: "get => IsAudited ? MeasuringStatus.Audited : base.MeasuringStatus" + constraints: "readonly, not-mapped" + OrderItemDto: OrderItemDto + foreign-key: "OrderItemId" + navigation: "many-to-one" + inverse-property: "OrderItemPallets" + OrderItemId: int + RevisorId: int + purpose: "User/Customer ID of the quality auditor" + Created: DateTime + CreatorId: int? + GrossWeight: double + IsMeasured: bool + Modified: DateTime + ModifierId: int? + NetWeight: double + business-logic: "get => GrossWeight - PalletWeight - (TrayQuantity * TareWeight)" + constraints: "readonly, not-mapped" + PalletWeight: double + TareWeight: double + TrayQuantity: int + Id: int + primary-key: true + + Partner: "Business partner with address and tax information" + table-name: "fbPartner" + purpose: "External supplier providing goods" + CertificationNumber: string + City: string + Country: string + County: string + Created: DateTime + Modified: DateTime + Name: string + PostalCode: string + ShippingDocuments: List + navigation: "one-to-many" + State: string + Street: string + TaxId: string + Id: int + primary-key: true + + Shipping: "Inbound delivery event at warehouse" + table-name: "fbShipping" + CargoCompany: string + Comment: string + Created: DateTime + IsAllMeasured: bool + LicencePlate: string + MeasuredDate: DateTime? + Modified: DateTime + ShippingDate: DateTime + ShippingDocuments: List + navigation: "one-to-many" + Id: int + primary-key: true + + ShippingDocument: "Supplier delivery note or invoice" + table-name: "fbShippingDocument" + Comment: string + Country: string + Created: DateTime + DocumentIdNumber: string + IsAllMeasured: bool + Modified: DateTime + Partner: Partner + foreign-key: "PartnerId" + navigation: "many-to-one" + PartnerId: int + PdfFileName: string + Shipping: Shipping + foreign-key: "ShippingId" + navigation: "many-to-one" + ShippingDate: DateTime + ShippingDocumentToFiles: List + navigation: "one-to-many" + ShippingId: int? + ShippingItems: List + navigation: "one-to-many" + TotalPallets: int + Id: int + primary-key: true + + ShippingDocumentToFiles: "Links documents to files with type" + table-name: "fbShippingDocumentToFiles" + Created: DateTime + DocumentType: DocumentType + DocumentTypeId: int + FilesId: int + Modified: DateTime + ShippingDocument: ShippingDocument + foreign-key: "ShippingDocumentId" + navigation: "many-to-one" + ShippingDocumentFile: Files + foreign-key: "FilesId" + navigation: "many-to-one" + ShippingDocumentId: int + Id: int + primary-key: true + + ShippingItem: "Product line on shipping document" + table-name: "fbShippingItem" + Created: DateTime + GrossWeightOnDocument: double + HungarianName: string + IsMeasurable: bool + IsMeasured: bool + MeasuredGrossWeight: double + MeasuredNetWeight: double + MeasuredQuantity: int + MeasuringCount: int + MeasuringStatus: MeasuringStatus + constraints: "readonly, not-mapped" + Modified: DateTime + Name: string + NameOnDocument: string + NetWeightOnDocument: double + Pallet: Pallet + foreign-key: "PalletId" + navigation: "many-to-one" + PalletId: int? + PalletsOnDocument: int + ProductDto: ProductDto + foreign-key: "ProductId" + navigation: "many-to-one" + ProductId: int? + QuantityOnDocument: int + ShippingDocument: ShippingDocument + foreign-key: "ShippingDocumentId" + navigation: "many-to-one" + ShippingDocumentId: int + ShippingItemPallets: List + navigation: "one-to-many" + UnitPriceOnDocument: double + Id: int + primary-key: true + + ShippingItemPallet: "Measurement record for incoming goods" + table-name: "fbShippingItemPallet" + purpose: "Always created even without physical pallet" + ShippingItem: ShippingItem + foreign-key: "ShippingItemId" + navigation: "many-to-one" + ShippingItemId: int + Created: DateTime + CreatorId: int? + GrossWeight: double + IsMeasured: bool + MeasuringStatus: MeasuringStatus + constraints: "readonly, not-mapped" + Modified: DateTime + ModifierId: int? + NetWeight: double + business-logic: "get => GrossWeight - PalletWeight - (TrayQuantity * TareWeight)" + constraints: "readonly, not-mapped" + PalletWeight: double + TareWeight: double + TrayQuantity: int + Id: int + primary-key: true + + StockTaking: "Inventory session record" + table-name: "fbStockTaking" + Created: DateTime + Creator: int + IsClosed: bool + Modified: DateTime + StartDateTime: DateTime + StockTakingItems: List + navigation: "one-to-many" + Id: int + primary-key: true + + StockTakingItem: "Line item for product reconciliation" + table-name: "fbStockTakingItem" + purpose: "Reconciles snapshot quantity with physical count" + InProcessOrdersQuantity: int + IsInvalid: bool + constraints: "readonly, not-mapped" + IsMeasurable: bool + IsRequiredForMeasuring: bool + constraints: "readonly, not-mapped" + MeasuredNetWeight: double + NetWeightDiff: double + constraints: "readonly, not-mapped" + OriginalNetWeight: double + QuantityDiff: int + constraints: "readonly, not-mapped" + StockTakingItemPallets: List + navigation: "one-to-many" + TotalOriginalQuantity: int + constraints: "readonly, not-mapped" + Created: DateTime + IsMeasured: bool + MeasuredStockQuantity: int + Modified: DateTime + OriginalStockQuantity: int + Product: ProductDto + foreign-key: "ProductId" + navigation: "many-to-one" + ProductId: int + StockTaking: StockTaking + foreign-key: "StockTakingId" + navigation: "many-to-one" + StockTakingId: int + Id: int + primary-key: true + + StockTakingItemPallet: "Weight record for inventory item" + table-name: "fbStockTakingItemPallet" + purpose: "Mandatory for every inventory item, even non-measurable" + StockTakingItem: StockTakingItem + foreign-key: "StockTakingItemId" + navigation: "many-to-one" + StockTakingItemId: int + Created: DateTime + CreatorId: int? + GrossWeight: double + IsMeasured: bool + MeasuringStatus: MeasuringStatus + constraints: "readonly, not-mapped" + Modified: DateTime + ModifierId: int? + NetWeight: double + business-logic: "get => GrossWeight - PalletWeight - (TrayQuantity * TareWeight)" + constraints: "readonly, not-mapped" + PalletWeight: double + TareWeight: double + TrayQuantity: int + Id: int + primary-key: true +} diff --git a/Nop.Plugin.Misc.AIPlugin/docs/SIGNALR_ENDPOINTS.md b/Nop.Plugin.Misc.AIPlugin/docs/SIGNALR_ENDPOINTS.md new file mode 100644 index 0000000..6217ca0 --- /dev/null +++ b/Nop.Plugin.Misc.AIPlugin/docs/SIGNALR_ENDPOINTS.md @@ -0,0 +1,166 @@ +# SignalR Endpoints and External Integrations + +> Part of `Nop.Plugin.Misc.FruitBankPlugin`. See `README.md` for project overview. +> For measurement workflows see `docs/MEASUREMENT.md`. +> For data layer see `docs/DATA_LAYER.md`. + +The plugin communicates with FruitBankHybridApp (MAUI client) via SignalR. Methods decorated with `[SignalR]` attributes auto-broadcast results to connected clients. + +## SignalR Configuration + +Configured in `PluginNopStartup`: + +| Setting | Value | +|---|---| +| KeepAliveInterval | 30 seconds | +| ClientTimeoutInterval | 30 seconds | +| MaximumReceiveMessageSize | 30 MB | +| EnableDetailedErrors | true | + +Hubs registered: `DevAdminSignalRHub`, `LoggerSignalRHub` (from AyCode.Core.Server). + +## FruitBankHub + +`Services/FruitBankHub.cs` — Minimal hub inheriting `Hub`. + +| Method | Purpose | +|---|---| +| `SendMessage(string user, string message)` | Broadcast message to all connected clients | + +## CustomOrderSignalREndpoint + +`Areas/Admin/Controllers/CustomOrderSignalREndpoint.cs` — Order management via SignalR. Implements `ICustomOrderSignalREndpointServer`. + +### Query Methods + +| Method | Parameters | Returns | Broadcast Tag | +|---|---|---|---| +| `GetAllOrderDtos()` | — | Orders from last 15 days with relations | — | +| `GetOrderDtoById` | orderId | Single order with relations | — | +| `GetPendingOrderDtos()` | — | Pending status orders | — | +| `GetPendingOrderDtosForMeasuring` | lastDaysCount | Orders for measurement (unpaid, not cancelled, with DateOfReceipt) | — | +| `GetAllOrderDtoByIds` | orderIds[] | Batch retrieval | — | +| `GetAllOrderDtoByProductId` | productId | Orders containing product | — | + +### Mutation Methods + +| Method | Parameters | Broadcast Tag | Key Logic | +|---|---|---|---| +| `StartMeasuring` | orderId, userId | `SendOrderChanged` | Sets MeasurementOwnerId, OrderStatus=Processing, PaymentStatus=Pending | +| `SetOrderStatusToComplete` | orderId, revisorId | `SendOrderChanged` | Validates pallets, recalculates prices, updates stock, publishes OrderPaidEvent | +| `AddOrUpdateMeasuredOrderItemPallet` | OrderItemPallet | `SendOrderItemPalletChanged` | Saves pallet, triggers price recalculation | + +### Order Item & Pallet Queries + +| Method | Purpose | +|---|---| +| `GetOrderItemDtosByOrderId` | Items for a specific order | +| `GetOrderItemPalletsByOrderItemId` | Pallets for a specific item | +| `GetOrderItemPalletById` | Single pallet | + +## StockSignalREndpointServer + +`Areas/Admin/Controllers/StockSignalREndpointServer.cs` — Inventory management via SignalR. + +| Method | Parameters | Key Logic | +|---|---|---| +| `GetStockTakings` | loadRelations | All stock taking sessions | +| `AddStockTaking` | StockTaking | Creates session, auto-populates items from products + pending order items | +| `CloseStockTaking` | stockTakingId | Validates all measured, applies stock corrections, marks closed | +| `UpdateStockTaking` | StockTaking | Updates session | +| `RefreshStockTakingItem` | stockTakingItemId | Refreshes measured values from pallets | +| `GetStockTakingItems` | loadRelations | All items | +| `GetStockTakingItemsByStockTakingId` | stockTakingId | Filtered items | +| `AddOrUpdateMeasuredStockTakingItemPallet` | StockTakingItemPallet | Saves pallet, refreshes item values | + +## FruitBankDataController + +`Controllers/FruitBankDataController.cs` — Main data API implementing `IFruitBankDataControllerServer`. Methods decorated with `[SignalR]` attributes for FruitBankHybridApp integration. + +### Shipping Operations + +| Method | Purpose | +|---|---| +| `GetShippings()` | Last 15 days with relations | +| `GetNotMeasuredShippings()` | Incomplete shipments | +| `GetShippingById` / `AddShipping` / `UpdateShipping` | CRUD | +| `GetShippingItems` / `AddShippingItem` / `UpdateShippingItem` | Item CRUD | +| `GetShippingItemPallets` / `AddShippingItemPallet` / `UpdateShippingItemPallet` | Pallet CRUD | +| `AddOrUpdateMeasuredShippingItemPallet` | Measurement + average weight update + cascade | +| `GetShippingDocuments` / `AddShippingDocument` | Document operations, updates product costs on add | +| `ProcessAndSaveFullShippingJson` | Parses FullProcessModel JSON, saves complete shipping data | + +### Partner Operations + +| Method | Purpose | +|---|---| +| `GetPartners()` | All partners | +| `GetPartnerById` / `AddPartner` / `UpdatePartner` | CRUD | + +### Product & Attribute Operations + +| Method | Purpose | +|---|---| +| `GetProductDtos()` | All products with GenericAttributes | +| `GetProductDtoById` | Single product | +| `GetGenericAttributeDtosByEntityIdAndKeyGroup` | Custom attributes by entity + key group | +| `AddGenericAttributeDto` / `UpdateGenericAttributeDto` | Attribute CRUD | + +### User & Auth Operations + +| Method | Purpose | +|---|---| +| `GetMeasuringUsers()` | Users in "Measuring" role | +| `GetCustomerRolesByCustomerId` | Customer roles | +| `LoginMeasuringUser` | Authentication with role check | + +### Stock History + +| Method | Purpose | +|---|---| +| `GetStockQuantityHistoryDtos()` | Last 15 days | +| `GetStockQuantityHistoryDtosByProductId` | Product-filtered history | + +## InnVoice Integration (Billingo) + +### InnVoiceOrderService + +`Services/InnVoiceOrderService.cs` — Order synchronization with Billingo/InnVoice accounting system via XML API. + +| Method | Purpose | +|---|---| +| `CreateOrdersAsync(List)` | Batch order creation | +| `CreateOrderAsync(OrderCreateRequest)` | Single order creation | +| `GetOrdersByUpdateTimeAsync(DateTime)` | Query by last update time | +| `GetOrdersByDateAsync(DateTime)` | Query by order date | +| `GetOrderByTechIdAsync(int)` | Query by InnVoice tech ID | +| `GetOrderByTableIdAsync(int)` | Query by table ID | + +**Models:** `OrderCreateRequest`, `OrderCreateResponse`, `InnVoiceOrder`, `InnVoiceOrderLineItem` + +### InnvoiceApiService + +`Services/InnvoiceApiService.cs` — Invoice CRUD with Billingo XML API. + +| Method | Purpose | +|---|---| +| `GetInvoiceByIdAsync` / `GetInvoiceByNumberAsync` / `GetInvoiceByTechIdAsync` | Invoice queries | +| `GetInvoicesByDateRangeAsync` | Date-range filtered invoices | +| `CreateInvoiceAsync` / `UpdateInvoiceAsync` | Invoice CRUD | + +**Models:** `Invoice`, `InvoiceLineItem`, `InvoiceCreateRequest`, `InvoiceCreateResponse` + +Both services build and parse XML payloads. Called from `InnVoiceOrderController` and `InnVoiceOrderSyncController`. + +## FruitBankAttributeService + +`Services/FruitBankAttributeService.cs` — GenericAttribute CRUD helper for measurement-related attributes. + +| Method | Purpose | +|---|---| +| `GetGenericAttributeValueAsync` | Type-safe attribute retrieval | +| `GetMeasuringAttributeValuesAsync` | Get IsMeasurable, NetWeight, Tare, etc. as batch | +| `InsertOrUpdateMeasuringAttributeValuesAsync` | Upsert measurement attributes | +| `IsMeasurableEntityAsync` | Check if entity's product is measurable | + +Supports cumulative weight updates (adds to existing value rather than replacing).