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
This commit is contained in:
parent
8c7a9c31ba
commit
6c776a97ca
|
|
@ -46,6 +46,9 @@ public class PluginNopStartup : INopStartup
|
|||
/// <param name="configuration">Configuration of the application</param>
|
||||
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<IAcLogWriterBase, ConsoleLogWriter>();
|
||||
|
||||
services.AddTransient<INopLoggerMsSqlNopDataProvider, NopLoggerMsSqlNopDataProvider>();
|
||||
|
|
|
|||
|
|
@ -667,6 +667,15 @@
|
|||
<Content Include="$(OutDir)\System.ServiceModel.Primitives.dll" CopyToOutputDirectory="PreserveNewest" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="docs\SCHEMA.md" />
|
||||
<None Include="docs\DOMAIN_MODEL.md" />
|
||||
<None Include="docs\MEASUREMENT.md" />
|
||||
<None Include="docs\AI_SERVICES.md" />
|
||||
<None Include="docs\DATA_LAYER.md" />
|
||||
<None Include="docs\SIGNALR_ENDPOINTS.md" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- This target execute after "Build" target -->
|
||||
<Target Name="NopTarget" AfterTargets="Build">
|
||||
<MSBuild Projects="@(ClearPluginAssemblies)" Properties="PluginPath=$(OutDir)" Targets="NopClear" />
|
||||
|
|
|
|||
|
|
@ -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()`.
|
||||
|
|
|
|||
|
|
@ -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<AIChatMessage>, CancellationToken?) -> Task<string>` | Non-streaming chat completion |
|
||||
| `GetStreamedResponseAsync` | `(List<AIChatMessage>, CancellationToken?) -> IAsyncEnumerable<string>` | 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<string>` | 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<byte[]>` — 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
|
||||
|
|
@ -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<T>` (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<Product>` | Saves custom attributes (IsMeasurable, NetWeight, Tare, AverageWeight, AverageWeightTreshold, IncomingQuantity) |
|
||||
| `EntityUpdatedEvent<Product>` | Updates custom attributes, syncs IsMeasurable/Tare changes to existing ShippingItems |
|
||||
|
||||
**Shipping cascade events:**
|
||||
|
||||
| Event | Action |
|
||||
|---|---|
|
||||
| `EntityDeletedEvent<ShippingItemPallet>` | Refreshes parent ShippingItem measuring values |
|
||||
| `EntityInsertedEvent/UpdatedEvent<ShippingItem>` | Rechecks ShippingDocument.IsAllMeasured |
|
||||
| `EntityInsertedEvent/UpdatedEvent<ShippingDocument>` | Rechecks Shipping.IsAllMeasured |
|
||||
| `EntityDeletedEvent<Shipping>` | Cascade deletes child documents |
|
||||
| `EntityDeletedEvent<ShippingDocument>` | Cascade deletes child items |
|
||||
| `EntityDeletedEvent<ShippingItem>` | Cascade deletes child pallets |
|
||||
|
||||
**Order events:**
|
||||
|
||||
| Event | Action |
|
||||
|---|---|
|
||||
| `EntityDeletedEvent<OrderItem>` | Cleanup via `MeasurementService.DeleteOrderItemConstraintsAsync()` |
|
||||
| `EntityInsertedEvent/UpdatedEvent<OrderItem>` | 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.
|
||||
|
|
@ -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<OrderDto>` | All active orders with full graph |
|
||||
| `Shippings` | `List<Shipping>` | All active shipments with full graph |
|
||||
| `StockTakings` | `List<StockTaking>` | 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.
|
||||
|
|
@ -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
|
||||
|
|
@ -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<double>('AverageWeight')"
|
||||
constraints: "readonly, not-mapped"
|
||||
AverageWeightTreshold: double
|
||||
business-logic: "get => GenericAttributes.GetValueOrDefault<double>('AverageWeightTreshold')"
|
||||
constraints: "readonly, not-mapped"
|
||||
GenericAttributes: List<GenericAttributeDto>
|
||||
navigation: "one-to-many"
|
||||
IncomingQuantity: int
|
||||
business-logic: "get => GenericAttributes.GetValueOrDefault<int>('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<bool>('IsMeasurable')"
|
||||
constraints: "not-mapped"
|
||||
NetWeight: double
|
||||
business-logic: "get => GenericAttributes.GetValueOrDefault<double>('NetWeight')"
|
||||
constraints: "not-mapped"
|
||||
Tare: double
|
||||
business-logic: "get => GenericAttributes.GetValueOrDefault<double>('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<OrderDto>
|
||||
navigation: "one-to-many"
|
||||
Shippings: List<Shipping>
|
||||
navigation: "one-to-many"
|
||||
StockTakings: List<StockTaking>
|
||||
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<DateTime>('DateOfReceipt')"
|
||||
constraints: "readonly, not-mapped"
|
||||
DateOfReceiptOrCreated: DateTime
|
||||
constraints: "readonly, not-mapped"
|
||||
GenericAttributes: List<GenericAttributeDto>
|
||||
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<OrderItemDto>
|
||||
other-key: "OrderId"
|
||||
navigation: "one-to-many"
|
||||
inverse-property: "OrderDto"
|
||||
OrderNotes: List<OrderNote>
|
||||
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<GenericAttributeDto>
|
||||
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<OrderItemPallet>
|
||||
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<ShippingDocument>
|
||||
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<ShippingDocument>
|
||||
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<ShippingDocumentToFiles>
|
||||
navigation: "one-to-many"
|
||||
ShippingId: int?
|
||||
ShippingItems: List<ShippingItem>
|
||||
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<ShippingItemPallet>
|
||||
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<StockTakingItem>
|
||||
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<StockTakingItemPallet>
|
||||
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
|
||||
}
|
||||
|
|
@ -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<OrderCreateRequest>)` | 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<TEntity, TPropType>` | Type-safe attribute retrieval |
|
||||
| `GetMeasuringAttributeValuesAsync<TEntity>` | Get IsMeasurable, NetWeight, Tare, etc. as batch |
|
||||
| `InsertOrUpdateMeasuringAttributeValuesAsync<TEntity>` | Upsert measurement attributes |
|
||||
| `IsMeasurableEntityAsync<TEntity>` | Check if entity's product is measurable |
|
||||
|
||||
Supports cumulative weight updates (adds to existing value rather than replacing).
|
||||
Loading…
Reference in New Issue