Compare commits
19 Commits
fcd7866d09
...
45195b9cdf
| Author | SHA1 | Date |
|---|---|---|
|
|
45195b9cdf | |
|
|
ecd7275cee | |
|
|
c5e841f207 | |
|
|
d1c254d5d1 | |
|
|
aab628b329 | |
|
|
5bd5e14953 | |
|
|
1b68599acc | |
|
|
623a01e3e3 | |
|
|
22bda45ade | |
|
|
f369491a1d | |
|
|
9a3817dff0 | |
|
|
dd3c1c58c0 | |
|
|
3700bfdb29 | |
|
|
ca186c9e90 | |
|
|
0bb0b06af4 | |
|
|
18b119c7a8 | |
|
|
6d689d3632 | |
|
|
38f268ec1d | |
|
|
10eea9e70c |
|
|
@ -3,7 +3,14 @@
|
||||||
"allow": [
|
"allow": [
|
||||||
"Bash(dir:*)",
|
"Bash(dir:*)",
|
||||||
"Bash(dotnet list:*)",
|
"Bash(dotnet list:*)",
|
||||||
"Bash(find:*)"
|
"Bash(find:*)",
|
||||||
|
"Bash(grep:*)",
|
||||||
|
"Bash(dotnet test:*)",
|
||||||
|
"Bash(dotnet build:*)",
|
||||||
|
"Bash(ls:*)",
|
||||||
|
"Bash(while read:*)",
|
||||||
|
"Bash(do sed -i '1a using AyCode.Core.Serializers.Toons;\\\\n' \"$f\")",
|
||||||
|
"Bash(done)"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
# FruitBankHybridApp — Domain Rules
|
||||||
|
|
||||||
|
> This is the **single source of truth** for domain rules. Do not duplicate these elsewhere.
|
||||||
|
> For detailed docs see: `README.md` → `docs/`
|
||||||
|
|
||||||
|
## Business Domain
|
||||||
|
1. **FruitBank** = fruit & vegetable wholesaler. This is a **nopCommerce plugin** — Customer, Order, Product, GenericAttribute are nopCommerce entities.
|
||||||
|
2. **Shipping** = INBOUND delivery (supplier → warehouse). **Order** = OUTBOUND delivery (warehouse → customer). Never confuse the two directions.
|
||||||
|
3. **"Pallet"** (`XxxItemPallet`) = a **measurement record**, NOT a physical pallet. Always created for every item, even non-measurable ones.
|
||||||
|
|
||||||
|
## Measurement Logic
|
||||||
|
4. **IsMeasurable=false** → weights are 0.0, only `TrayQuantity` is recorded. A Pallet record is still created.
|
||||||
|
5. **NetWeight = GrossWeight − PalletWeight − (TrayQuantity × TareWeight)** — this formula is universal across ShippingItemPallet, OrderItemPallet, and StockTakingItemPallet.
|
||||||
|
6. **MeasuringStatus.Finnished** — intentional legacy typo with double-n. Do NOT fix the spelling.
|
||||||
|
7. **MeasuringStatus** progression: NotStarted(0) → Started(10) → Finnished(20) → Audited(30). OrderItemPallet adds Audited when RevisorId > 0.
|
||||||
|
|
||||||
|
## Data Model
|
||||||
|
8. **GenericAttribute** = polymorphic key-value store. `KeyGroup` = owner type name, `EntityId` = owner ID. ProductDto reads IsMeasurable, Tare, AverageWeight, IncomingQuantity, NetWeight from GenericAttributes.
|
||||||
|
9. **Three parallel measurement hierarchies** share the same base (`MeasuringItemPalletBase`):
|
||||||
|
- Shipping: ShippingItem → ShippingItemPallet
|
||||||
|
- Order: OrderItemDto → OrderItemPallet (adds RevisorId for audit)
|
||||||
|
- StockTaking: StockTakingItem → StockTakingItemPallet
|
||||||
|
|
||||||
|
## Technical
|
||||||
|
10. SignalR uses **AcBinaryHubProtocol** (custom binary), not default JSON.
|
||||||
|
11. Do not suggest removal/rollback as a solution — find a fix for the problem.
|
||||||
|
12. All AyCode references are via **DLL** (not ProjectReference) — this is intentional.
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
# FruitBankHybridApp — Claude Code Instructions
|
||||||
|
|
||||||
|
Before writing any code, read these files:
|
||||||
|
1. `.github/copilot-instructions.md` — Domain rules and critical pitfalls (single source of truth)
|
||||||
|
2. `docs/GLOSSARY.md` — Domain terms, measurement logic, and common traps
|
||||||
|
3. `docs/SCHEMA.md` — Full domain model in Toon format
|
||||||
|
4. The relevant project's `README.md` for folder-specific context
|
||||||
|
|
||||||
|
This solution depends on **AyCode.Core** and **AyCode.Blazor** frameworks.
|
||||||
|
|
||||||
|
When modifying code, update the corresponding README.md if it becomes out of sync with the code.
|
||||||
|
|
@ -8,6 +8,8 @@
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
||||||
|
|
||||||
<PackageReference Include="MessagePack" Version="3.1.4" />
|
<PackageReference Include="MessagePack" Version="3.1.4" />
|
||||||
<PackageReference Include="MessagePack.Annotations" Version="3.1.4" />
|
<PackageReference Include="MessagePack.Annotations" Version="3.1.4" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="9.0.11" />
|
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="9.0.11" />
|
||||||
|
|
@ -15,7 +17,6 @@
|
||||||
<PackageReference Include="Microsoft.AspNetCore.SignalR.Common" Version="9.0.11" />
|
<PackageReference Include="Microsoft.AspNetCore.SignalR.Common" Version="9.0.11" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.Json" Version="9.0.11" />
|
<PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.Json" Version="9.0.11" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.NewtonsoftJson" Version="9.0.11" />
|
<PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.NewtonsoftJson" Version="9.0.11" />
|
||||||
<!--<PackageReference Include="Microsoft.AspNetCore.SignalR.Core" Version="1.2.0" />-->
|
|
||||||
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="9.0.11" />
|
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="9.0.11" />
|
||||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
@ -26,25 +27,22 @@
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Reference Include="AyCode.Core">
|
<Reference Include="AyCode.Core">
|
||||||
<HintPath>..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Services.Server\bin\FruitBank\Debug\net9.0\AyCode.Core.dll</HintPath>
|
<HintPath>..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Services.Server\bin\FruitBank\$(Configuration)\net9.0\AyCode.Core.dll</HintPath>
|
||||||
</Reference>
|
</Reference>
|
||||||
<Reference Include="AyCode.Interfaces">
|
<Reference Include="AyCode.Interfaces">
|
||||||
<HintPath>..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Services.Server\bin\FruitBank\Debug\net9.0\AyCode.Interfaces.dll</HintPath>
|
<HintPath>..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Services.Server\bin\FruitBank\$(Configuration)\net9.0\AyCode.Interfaces.dll</HintPath>
|
||||||
</Reference>
|
</Reference>
|
||||||
<Reference Include="AyCode.Models.Server">
|
<Reference Include="AyCode.Models.Server">
|
||||||
<HintPath>..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Services.Server\bin\FruitBank\Debug\net9.0\AyCode.Models.Server.dll</HintPath>
|
<HintPath>..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Services.Server\bin\FruitBank\$(Configuration)\net9.0\AyCode.Models.Server.dll</HintPath>
|
||||||
</Reference>
|
</Reference>
|
||||||
<Reference Include="AyCode.Services">
|
<Reference Include="AyCode.Services">
|
||||||
<HintPath>..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Services.Server\bin\FruitBank\Debug\net9.0\AyCode.Services.dll</HintPath>
|
<HintPath>..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Services.Server\bin\FruitBank\$(Configuration)\net9.0\AyCode.Services.dll</HintPath>
|
||||||
</Reference>
|
</Reference>
|
||||||
<Reference Include="AyCode.Services.Server">
|
<Reference Include="AyCode.Services.Server">
|
||||||
<HintPath>..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Services.Server\bin\FruitBank\Debug\net9.0\AyCode.Services.Server.dll</HintPath>
|
<HintPath>..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Services.Server\bin\FruitBank\$(Configuration)\net9.0\AyCode.Services.Server.dll</HintPath>
|
||||||
</Reference>
|
|
||||||
<Reference Include="Microsoft.AspNetCore.SignalR.Core">
|
|
||||||
<HintPath>C:\Program Files\dotnet\packs\Microsoft.AspNetCore.App.Ref\9.0.14\ref\net9.0\Microsoft.AspNetCore.SignalR.Core.dll</HintPath>
|
|
||||||
</Reference>
|
</Reference>
|
||||||
<Reference Include="Mango.Nop.Core">
|
<Reference Include="Mango.Nop.Core">
|
||||||
<HintPath>..\..\NopCommerce.Common\4.70\Libraries\Mango.Nop.Core\bin\FruitBank\Debug\net9.0\Mango.Nop.Core.dll</HintPath>
|
<HintPath>..\..\NopCommerce.Common\4.70\Libraries\Mango.Nop.Core\bin\FruitBank\$(Configuration)\net9.0\Mango.Nop.Core.dll</HintPath>
|
||||||
</Reference>
|
</Reference>
|
||||||
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
# Interfaces
|
||||||
|
|
||||||
|
Server-side marker interfaces extending the shared Common interfaces. Used for DI registration and type safety.
|
||||||
|
|
||||||
|
## Key Files
|
||||||
|
|
||||||
|
- **`IFruitBankDataControllerServer.cs`** — Extends IFruitBankDataControllerCommon. Empty server marker.
|
||||||
|
- **`ICustomOrderSignalREndpointServer.cs`** — Extends ICustomOrderSignalREndpointCommon. Empty server marker.
|
||||||
|
- **`IStockSignalREndpointServer.cs`** — Extends IStockSignalREndpointCommon. Empty server marker.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
> **LLM Maintenance:** If you modify code in this folder, update this README to reflect the changes. If you notice the README content does not match the current code, automatically update the README to match the code.
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
# FruitBank.Common.Server
|
||||||
|
|
||||||
|
Server-side library: SignalR hubs, real-time broadcast service, logging infrastructure, and nopCommerce integration constants.
|
||||||
|
|
||||||
|
## Folder Structure
|
||||||
|
|
||||||
|
| Folder | Purpose |
|
||||||
|
|---|---|
|
||||||
|
| [`Interfaces/`](Interfaces/README.md) | Server-side endpoint marker interfaces |
|
||||||
|
| [`Services/`](Services/README.md) | SignalR hubs, broadcast service, logging |
|
||||||
|
|
||||||
|
## Key Files (Root)
|
||||||
|
|
||||||
|
- **`FruitBankConst.cs`** — Server constants: project GUID, role system names ("Measuring", "MeasuringRevisor"), product attribute "IsMeasurable", project salt.
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
- nopCommerce via Mango.Nop.Core
|
||||||
|
- AyCode.Core, AyCode.Services.Server (DLL references)
|
||||||
|
- Microsoft.AspNetCore.SignalR
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
> **LLM Maintenance:** If you modify code in this folder, update this README to reflect the changes. If you notice the README content does not match the current code, automatically update the README to match the code.
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
# Loggers
|
||||||
|
|
||||||
|
Server-side logging implementations.
|
||||||
|
|
||||||
|
## Key Files
|
||||||
|
|
||||||
|
- **`ConsoleLogWriter.cs`** — Lightweight console logger extending AcConsoleLogWriter. Configurable by AppType, LogLevel, category.
|
||||||
|
- **`LoggerToLoggerApiController.cs`** — Aggregates multiple IAcLogWriterBase implementations into a single logger for API controllers.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
> **LLM Maintenance:** If you modify code in this folder, update this README to reflect the changes. If you notice the README content does not match the current code, automatically update the README to match the code.
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
# Services
|
||||||
|
|
||||||
|
Server-side SignalR hubs, real-time broadcast, and logging infrastructure.
|
||||||
|
|
||||||
|
## Subfolders
|
||||||
|
|
||||||
|
| Folder | Purpose |
|
||||||
|
|---|---|
|
||||||
|
| [`Loggers/`](Loggers/README.md) | Console and API controller log writers |
|
||||||
|
| [`SignalRs/`](SignalRs/README.md) | SignalR hubs and broadcast service |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
> **LLM Maintenance:** If you modify code in this folder, update this README to reflect the changes. If you notice the README content does not match the current code, automatically update the README to match the code.
|
||||||
|
|
@ -13,22 +13,6 @@ using Microsoft.Extensions.Configuration;
|
||||||
|
|
||||||
namespace FruitBank.Common.Server.Services.SignalRs;
|
namespace FruitBank.Common.Server.Services.SignalRs;
|
||||||
|
|
||||||
//public class DevAdminSignalRHub : AcWebSignalRHubWithSessionBase<SignalRTags, Logger<DevAdminSignalRHub>>
|
|
||||||
//{
|
|
||||||
// public DevAdminSignalRHub(IConfiguration configuration, IEnumerable<IAcLogWriterBase> logWriters)
|
|
||||||
// : base(configuration, new Logger<DevAdminSignalRHub>(logWriters.ToArray()))
|
|
||||||
// {
|
|
||||||
// Logger.Info("DevAdminSignalRHub");
|
|
||||||
// }
|
|
||||||
|
|
||||||
// public Task ReceiveMessage(int messageTag, byte[]? message, int? requestId)
|
|
||||||
// {
|
|
||||||
|
|
||||||
// Clients.All.SendAsync("TestMessage", "Hello from server");
|
|
||||||
// }
|
|
||||||
|
|
||||||
//}
|
|
||||||
|
|
||||||
public class DevAdminSignalRHub : AcWebSignalRHubWithSessionBase<SignalRTags, Logger<DevAdminSignalRHub>>
|
public class DevAdminSignalRHub : AcWebSignalRHubWithSessionBase<SignalRTags, Logger<DevAdminSignalRHub>>
|
||||||
{
|
{
|
||||||
public DevAdminSignalRHub(IConfiguration configuration, IFruitBankDataControllerServer fruitBankDataController/*, SessionService sessionService*/,
|
public DevAdminSignalRHub(IConfiguration configuration, IFruitBankDataControllerServer fruitBankDataController/*, SessionService sessionService*/,
|
||||||
|
|
@ -37,11 +21,13 @@ public class DevAdminSignalRHub : AcWebSignalRHubWithSessionBase<SignalRTags, Lo
|
||||||
{
|
{
|
||||||
EnableBinaryDiagnostics = FruitBankConstClient.SignalRSerializerDiagnosticLog;
|
EnableBinaryDiagnostics = FruitBankConstClient.SignalRSerializerDiagnosticLog;
|
||||||
SerializerOptions = new AcBinarySerializerOptions();
|
SerializerOptions = new AcBinarySerializerOptions();
|
||||||
//SerializerOptions = new AcJsonSerializerOptions();
|
|
||||||
|
|
||||||
DynamicMethodCallModels.Add(new AcDynamicMethodCallModel<SignalRAttribute>(fruitBankDataController));
|
// Use the new lazy Registry - no reflection at construction time
|
||||||
DynamicMethodCallModels.Add(new AcDynamicMethodCallModel<SignalRAttribute>(customOrderSignalREndpoint));
|
DynamicMethodRegistry.CahcheSizeCapacity = 3;
|
||||||
DynamicMethodCallModels.Add(new AcDynamicMethodCallModel<SignalRAttribute>(stockSignalREndpointServer));
|
|
||||||
|
DynamicMethodRegistry.Register(fruitBankDataController);
|
||||||
|
DynamicMethodRegistry.Register(customOrderSignalREndpoint);
|
||||||
|
DynamicMethodRegistry.Register(stockSignalREndpointServer);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void LogContextUserNameAndId()
|
protected override void LogContextUserNameAndId()
|
||||||
|
|
@ -49,83 +35,5 @@ public class DevAdminSignalRHub : AcWebSignalRHubWithSessionBase<SignalRTags, Lo
|
||||||
return;
|
return;
|
||||||
base.LogContextUserNameAndId();
|
base.LogContextUserNameAndId();
|
||||||
}
|
}
|
||||||
//public override Task OnReceiveMessage(int messageTag, byte[]? message, int? requestId)
|
// ...existing commented code...
|
||||||
//{
|
|
||||||
// return ProcessOnReceiveMessage(messageTag, message, requestId, async tagName =>
|
|
||||||
// {
|
|
||||||
// switch (messageTag)
|
|
||||||
// {
|
|
||||||
// case SignalRTags.GetAddress:
|
|
||||||
// //var id = Guid.Parse((string)message!.MessagePackTo<SignalPostJsonDataMessage<IdMessage>>().PostData.Ids[0]);
|
|
||||||
// var id = message!.MessagePackTo<SignalPostJsonDataMessage<IdMessage>>().PostData.Ids[0].JsonTo<Guid[]>()![0];
|
|
||||||
|
|
||||||
// var address = await _adminDal.GetAddressByIdAsync(id);
|
|
||||||
// await ResponseToCaller(messageTag, new SignalResponseJsonMessage(messageTag, SignalResponseStatus.Success, address), requestId);
|
|
||||||
|
|
||||||
// return;
|
|
||||||
|
|
||||||
// case SignalRTags.GetAddressesByContextId:
|
|
||||||
// //id = Guid.Parse((string)message!.MessagePackTo<SignalPostJsonDataMessage<IdMessage>>().PostData.Ids[0]);
|
|
||||||
// id = message!.MessagePackTo<SignalPostJsonDataMessage<IdMessage>>().PostData.Ids[0].JsonTo<Guid[]>()![0];
|
|
||||||
|
|
||||||
// address = await _adminDal.GetAddressByIdAsync(id);
|
|
||||||
// await ResponseToCaller(messageTag, new SignalResponseJsonMessage(messageTag, SignalResponseStatus.Success, new List<Address> { address! }), requestId);
|
|
||||||
|
|
||||||
// return;
|
|
||||||
|
|
||||||
// case SignalRTags.UpdateAddress:
|
|
||||||
// address = message!.MessagePackTo<SignalPostJsonDataMessage<Address>>().PostData;
|
|
||||||
|
|
||||||
// await _adminDal.UpdateAddressAsync(address);
|
|
||||||
// await ResponseToCaller(messageTag, new SignalResponseJsonMessage(messageTag, SignalResponseStatus.Success, address), requestId);
|
|
||||||
|
|
||||||
// return;
|
|
||||||
|
|
||||||
// case SignalRTags.UpdateProfile:
|
|
||||||
// var profile = message!.MessagePackTo<SignalPostJsonDataMessage<Profile>>().PostData;
|
|
||||||
|
|
||||||
// await _adminDal.UpdateProfileAsync(profile);
|
|
||||||
// await ResponseToCaller(messageTag, new SignalResponseJsonMessage(messageTag, SignalResponseStatus.Success, profile), requestId);
|
|
||||||
|
|
||||||
// return;
|
|
||||||
|
|
||||||
// //case SignalRTags.GetTransfersAsync:
|
|
||||||
// // await ResponseToCaller(messageTag, new SignalResponseJsonMessage(SignalResponseStatus.Success, await _transferDataApiController.GetTransfers()), requestId);
|
|
||||||
|
|
||||||
// // return;
|
|
||||||
|
|
||||||
// //case SignalRTags.GetPropertiesByOwnerIdAsync:
|
|
||||||
// // var ownerId = message!.MessagePackTo<SignalRequestByIdMessage>().Id;
|
|
||||||
|
|
||||||
// // await ResponseToCaller(messageTag, new SignalResponseJsonMessage(SignalResponseStatus.Success, await _serviceProviderApiController.GetServiceProvidersByOwnerId(ownerId)), requestId);
|
|
||||||
|
|
||||||
// // return;
|
|
||||||
|
|
||||||
// //case SignalRTags.UpdateTransferAsync:
|
|
||||||
// // var transfer = message!.MessagePackTo<SignalPostJsonDataMessage<Transfer>>().PostData;
|
|
||||||
|
|
||||||
// // await _transferDataApiController.UpdateTransfer(transfer);
|
|
||||||
// // await ResponseToCaller(messageTag, new SignalResponseJsonMessage(SignalResponseStatus.Success, transfer), requestId);
|
|
||||||
|
|
||||||
// // return;
|
|
||||||
|
|
||||||
// //case SignalRTags.GetCompaniesAsync:
|
|
||||||
// // await ResponseToCaller(messageTag, new SignalResponseJsonMessage(SignalResponseStatus.Success, await _serviceProviderApiController.GetServiceProviders()), requestId);
|
|
||||||
|
|
||||||
// // return;
|
|
||||||
// //case SignalRTags.UpdateCompanyAsync:
|
|
||||||
|
|
||||||
// // var updateCompany = message!.MessagePackTo<SignalPostJsonDataMessage<Company>>().PostData;
|
|
||||||
|
|
||||||
// // await _serviceProviderApiController.UpdateServiceProvider(updateCompany);
|
|
||||||
// // await ResponseToCaller(messageTag, new SignalResponseJsonMessage(SignalResponseStatus.Success, updateCompany), requestId);
|
|
||||||
|
|
||||||
// // return;
|
|
||||||
|
|
||||||
// default:
|
|
||||||
// Logger.Error($"Server OnReceiveMessage; messageTag not found! {tagName}");
|
|
||||||
// break;
|
|
||||||
// }
|
|
||||||
// });
|
|
||||||
//}
|
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
# SignalRs
|
||||||
|
|
||||||
|
SignalR hub implementations and real-time broadcast service.
|
||||||
|
|
||||||
|
## Key Files
|
||||||
|
|
||||||
|
- **`DevAdminSignalRHub.cs`** — Main admin hub. Dependencies: IConfiguration, IFruitBankDataControllerServer, ICustomOrderSignalREndpointServer, IStockSignalREndpointServer. Registers all three endpoint interfaces with DynamicMethodRegistry. Uses AcBinary serialization.
|
||||||
|
- **`AcWebSignalRHubWithSessionBase.cs`** — Generic base hub with session management (OnConnected/OnDisconnected hooks).
|
||||||
|
- **`SignalRSendToClientService.cs`** — Broadcasts real-time notifications to all clients: SendOrderChanged, SendOrderItemChanged, SendShippingChanged, SendProductChanged, etc.
|
||||||
|
- **`LoggerSignalRHub.cs`** — Minimal hub for logging/diagnostics.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
> **LLM Maintenance:** If you modify code in this folder, update this README to reflect the changes. If you notice the README content does not match the current code, automatically update the README to match the code.
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
# Databases
|
||||||
|
|
||||||
|
Local in-memory database abstraction for offline/cached data using ConcurrentDictionary.
|
||||||
|
|
||||||
|
## Key Files
|
||||||
|
|
||||||
|
- **`DatabaseLocalBase.cs`** — Abstract base with generic table management for IEntityInt entities. Thread-safe AddTable, GetRows, GetRow, AddRow, AddRows, DeleteRow.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
> **LLM Maintenance:** If you modify code in this folder, update this README to reflect the changes. If you notice the README content does not match the current code, automatically update the README to match the code.
|
||||||
|
|
@ -1,7 +1,15 @@
|
||||||
using Mango.Nop.Core.Dtos;
|
using AyCode.Core.Serializers.Attributes;
|
||||||
|
using AyCode.Core.Serializers.Toons;
|
||||||
|
using Mango.Nop.Core.Dtos;
|
||||||
|
using Nop.Core.Domain.Common;
|
||||||
|
using Nop.Core.Domain.Orders;
|
||||||
|
|
||||||
namespace FruitBank.Common.Dtos;
|
namespace FruitBank.Common.Dtos;
|
||||||
|
|
||||||
|
[AcBinarySerializable(false, true, false, true)]
|
||||||
|
[LinqToDB.Mapping.Table(Name = nameof(GenericAttribute))]
|
||||||
|
[System.ComponentModel.DataAnnotations.Schema.Table(nameof(GenericAttribute))]
|
||||||
|
[ToonDescription($"Data transfer object for {nameof(GenericAttribute)}", TypeRelation = ToonTypeRelation.DtoOf, RelatedTypes = [typeof(GenericAttribute)])]
|
||||||
public class GenericAttributeDto : MgGenericAttributeDto
|
public class GenericAttributeDto : MgGenericAttributeDto
|
||||||
{
|
{
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,6 @@
|
||||||
using AyCode.Core.Extensions;
|
using AyCode.Core.Extensions;
|
||||||
|
using AyCode.Core.Serializers.Attributes;
|
||||||
|
using AyCode.Core.Serializers.Toons;
|
||||||
using AyCode.Utils.Extensions;
|
using AyCode.Utils.Extensions;
|
||||||
using FruitBank.Common.Entities;
|
using FruitBank.Common.Entities;
|
||||||
using FruitBank.Common.Enums;
|
using FruitBank.Common.Enums;
|
||||||
|
|
@ -20,6 +22,10 @@ using System.Linq.Expressions;
|
||||||
|
|
||||||
namespace FruitBank.Common.Dtos;
|
namespace FruitBank.Common.Dtos;
|
||||||
|
|
||||||
|
[AcBinarySerializable(false, true, false, true)]
|
||||||
|
[LinqToDB.Mapping.Table(Name = nameof(Order))]
|
||||||
|
[System.ComponentModel.DataAnnotations.Schema.Table(nameof(Order))]
|
||||||
|
[ToonDescription($"Data transfer object for {nameof(Order)}", TypeRelation = ToonTypeRelation.DtoOf, RelatedTypes = [typeof(Order)])]
|
||||||
public class OrderDto : MgOrderDto<OrderItemDto, ProductDto>, IOrderDto
|
public class OrderDto : MgOrderDto<OrderItemDto, ProductDto>, IOrderDto
|
||||||
{
|
{
|
||||||
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
||||||
|
|
@ -31,6 +37,7 @@ public class OrderDto : MgOrderDto<OrderItemDto, ProductDto>, IOrderDto
|
||||||
public List<GenericAttributeDto> GenericAttributes { get; set; }
|
public List<GenericAttributeDto> GenericAttributes { get; set; }
|
||||||
|
|
||||||
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
||||||
|
[ToonDescription(BusinessRule = "get => Id > 0 && OrderItemDtos.Count > 0 && OrderItemDtos.All(x => x.IsMeasured)", Constraints = "[#SmartTypeConstraints], readonly")]
|
||||||
public bool IsMeasured
|
public bool IsMeasured
|
||||||
{
|
{
|
||||||
get => IsMeasuredAndValid();
|
get => IsMeasuredAndValid();
|
||||||
|
|
@ -38,6 +45,7 @@ public class OrderDto : MgOrderDto<OrderItemDto, ProductDto>, IOrderDto
|
||||||
}
|
}
|
||||||
|
|
||||||
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
||||||
|
[ToonDescription(BusinessRule = "get => OrderItemDtos.Any(oi => oi.IsMeasurable)", Constraints = "[#SmartTypeConstraints], readonly")]
|
||||||
public bool IsMeasurable
|
public bool IsMeasurable
|
||||||
{
|
{
|
||||||
get => OrderItemDtos.Any(oi => oi.IsMeasurable);
|
get => OrderItemDtos.Any(oi => oi.IsMeasurable);
|
||||||
|
|
@ -65,18 +73,23 @@ public class OrderDto : MgOrderDto<OrderItemDto, ProductDto>, IOrderDto
|
||||||
public DateTime DateOfReceiptOrCreated => DateOfReceipt ?? CreatedOnUtc;
|
public DateTime DateOfReceiptOrCreated => DateOfReceipt ?? CreatedOnUtc;
|
||||||
|
|
||||||
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
||||||
|
[ToonDescription(BusinessRule = "get => GenericAttributes.GetValueOrNull<DateTime>('DateOfReceipt')")]
|
||||||
public DateTime? DateOfReceipt => GenericAttributes.GetValueOrNull<DateTime>(nameof(IOrderDto.DateOfReceipt));
|
public DateTime? DateOfReceipt => GenericAttributes.GetValueOrNull<DateTime>(nameof(IOrderDto.DateOfReceipt));
|
||||||
|
|
||||||
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
||||||
|
[ToonDescription(BusinessRule = "get => GenericAttributes.GetValueOrDefault('RevisorId', 0)")]
|
||||||
public int RevisorId => GenericAttributes.GetValueOrDefault(nameof(IOrderDto.RevisorId), 0);
|
public int RevisorId => GenericAttributes.GetValueOrDefault(nameof(IOrderDto.RevisorId), 0);
|
||||||
|
|
||||||
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
||||||
|
[ToonDescription(BusinessRule = "get => GenericAttributes.GetValueOrDefault('MeasurementOwnerId', 0)")]
|
||||||
public int MeasurementOwnerId => GenericAttributes.GetValueOrDefault(nameof(IOrderDto.MeasurementOwnerId), 0);
|
public int MeasurementOwnerId => GenericAttributes.GetValueOrDefault(nameof(IOrderDto.MeasurementOwnerId), 0);
|
||||||
|
|
||||||
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
||||||
|
[ToonDescription(BusinessRule = "get => OrderItemDtos.Count > 0 && OrderItemDtos.All(oi => oi.IsAudited)")]
|
||||||
public bool IsAllOrderItemAudited => OrderItemDtos.Count > 0 && OrderItemDtos.All(oi => oi.IsAudited);
|
public bool IsAllOrderItemAudited => OrderItemDtos.Count > 0 && OrderItemDtos.All(oi => oi.IsAudited);
|
||||||
|
|
||||||
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
||||||
|
[ToonDescription(BusinessRule = "get => OrderItemDtos.All(oi => oi.AverageWeightIsValid)")]
|
||||||
public bool IsAllOrderItemAvgWeightValid => OrderItemDtos.All(oi => oi.AverageWeightIsValid);
|
public bool IsAllOrderItemAvgWeightValid => OrderItemDtos.All(oi => oi.AverageWeightIsValid);
|
||||||
|
|
||||||
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
||||||
|
|
@ -101,6 +114,7 @@ public class OrderDto : MgOrderDto<OrderItemDto, ProductDto>, IOrderDto
|
||||||
{ }
|
{ }
|
||||||
|
|
||||||
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
||||||
|
[ToonDescription(BusinessRule = "get => OrderStatus == OrderStatus.Complete")]
|
||||||
public bool IsComplete => OrderStatus == OrderStatus.Complete;
|
public bool IsComplete => OrderStatus == OrderStatus.Complete;
|
||||||
|
|
||||||
public bool HasMeasuringAccess(int? customerId, bool isRevisorUser = false)
|
public bool HasMeasuringAccess(int? customerId, bool isRevisorUser = false)
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,6 @@
|
||||||
using FruitBank.Common.Entities;
|
using AyCode.Core.Serializers.Attributes;
|
||||||
|
using AyCode.Core.Serializers.Toons;
|
||||||
|
using FruitBank.Common.Entities;
|
||||||
using FruitBank.Common.Enums;
|
using FruitBank.Common.Enums;
|
||||||
using FruitBank.Common.Interfaces;
|
using FruitBank.Common.Interfaces;
|
||||||
using LinqToDB.Mapping;
|
using LinqToDB.Mapping;
|
||||||
|
|
@ -14,6 +16,10 @@ using System.Linq.Expressions;
|
||||||
|
|
||||||
namespace FruitBank.Common.Dtos;
|
namespace FruitBank.Common.Dtos;
|
||||||
|
|
||||||
|
[AcBinarySerializable(false, true, false, true)]
|
||||||
|
[LinqToDB.Mapping.Table(Name = nameof(OrderItem))]
|
||||||
|
[System.ComponentModel.DataAnnotations.Schema.Table(nameof(OrderItem))]
|
||||||
|
[ToonDescription("Order item with measurements, pallets, and validation", TypeRelation = ToonTypeRelation.DtoOf, RelatedTypes = [typeof(OrderItem)])]
|
||||||
public class OrderItemDto : MgOrderItemDto<ProductDto>, IOrderItemDto
|
public class OrderItemDto : MgOrderItemDto<ProductDto>, IOrderItemDto
|
||||||
{
|
{
|
||||||
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
||||||
|
|
@ -31,6 +37,7 @@ public class OrderItemDto : MgOrderItemDto<ProductDto>, IOrderItemDto
|
||||||
public OrderDto OrderDto { get; set; }
|
public OrderDto OrderDto { get; set; }
|
||||||
|
|
||||||
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
||||||
|
[ToonDescription(Purpose = "Status flag", BusinessRule = "get => IsMeasuredAndValid()")]
|
||||||
public bool IsMeasured
|
public bool IsMeasured
|
||||||
{
|
{
|
||||||
get => IsMeasuredAndValid();
|
get => IsMeasuredAndValid();
|
||||||
|
|
@ -38,6 +45,7 @@ public class OrderItemDto : MgOrderItemDto<ProductDto>, IOrderItemDto
|
||||||
}
|
}
|
||||||
|
|
||||||
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
||||||
|
[ToonDescription(Purpose = "Status flag", BusinessRule = "get => ProductDto!.IsMeasurable")]
|
||||||
public bool IsMeasurable
|
public bool IsMeasurable
|
||||||
{
|
{
|
||||||
get => ProductDto!.IsMeasurable;
|
get => ProductDto!.IsMeasurable;
|
||||||
|
|
@ -45,6 +53,7 @@ public class OrderItemDto : MgOrderItemDto<ProductDto>, IOrderItemDto
|
||||||
}
|
}
|
||||||
|
|
||||||
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
||||||
|
[ToonDescription(BusinessRule = "get => OrderItemPallets.Sum(x => x.TrayQuantity)")]
|
||||||
public int TrayQuantity
|
public int TrayQuantity
|
||||||
{
|
{
|
||||||
get => OrderItemPallets.Sum(x => x.TrayQuantity);
|
get => OrderItemPallets.Sum(x => x.TrayQuantity);
|
||||||
|
|
@ -52,6 +61,7 @@ public class OrderItemDto : MgOrderItemDto<ProductDto>, IOrderItemDto
|
||||||
}
|
}
|
||||||
|
|
||||||
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
||||||
|
[ToonDescription(BusinessRule = "get => double.Round(OrderItemPallets.Sum(x => x.NetWeight), 1)")]
|
||||||
public double NetWeight
|
public double NetWeight
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
|
|
@ -69,6 +79,7 @@ public class OrderItemDto : MgOrderItemDto<ProductDto>, IOrderItemDto
|
||||||
}
|
}
|
||||||
|
|
||||||
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
||||||
|
[ToonDescription(BusinessRule = "get => double.Round(OrderItemPallets.Sum(x => x.NetWeight), 1)")]
|
||||||
public double GrossWeight
|
public double GrossWeight
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
|
|
@ -86,18 +97,23 @@ public class OrderItemDto : MgOrderItemDto<ProductDto>, IOrderItemDto
|
||||||
}
|
}
|
||||||
|
|
||||||
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
||||||
|
[ToonDescription(BusinessRule = "get => IsMeasurable && OrderItemPallets.Count > 0 ? double.Round(OrderItemPallets.Sum(oip => oip.AverageWeight) / OrderItemPallets.Count, 1) : 0d")]
|
||||||
public double AverageWeight => IsMeasurable && OrderItemPallets.Count > 0 ? double.Round(OrderItemPallets.Sum(oip => oip.AverageWeight) / OrderItemPallets.Count, 1) : 0d;
|
public double AverageWeight => IsMeasurable && OrderItemPallets.Count > 0 ? double.Round(OrderItemPallets.Sum(oip => oip.AverageWeight) / OrderItemPallets.Count, 1) : 0d;
|
||||||
|
|
||||||
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
||||||
|
[ToonDescription(BusinessRule = "get => IsMeasurable ? double.Round(ProductDto!.AverageWeight - AverageWeight, 1) : 0")]
|
||||||
public double AverageWeightDifference => IsMeasurable ? double.Round(ProductDto!.AverageWeight - AverageWeight, 1) : 0;
|
public double AverageWeightDifference => IsMeasurable ? double.Round(ProductDto!.AverageWeight - AverageWeight, 1) : 0;
|
||||||
|
|
||||||
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
||||||
|
[ToonDescription(BusinessRule = "get => !IsMeasurable || (ProductDto!.AverageWeight > 0 && ((AverageWeightDifference / ProductDto!.AverageWeight) * 100) < ProductDto!.AverageWeightTreshold)")]
|
||||||
public bool AverageWeightIsValid => !IsMeasurable || (ProductDto!.AverageWeight > 0 && Math.Abs((AverageWeightDifference / ProductDto!.AverageWeight) * 100) < ProductDto!.AverageWeightTreshold);
|
public bool AverageWeightIsValid => !IsMeasurable || (ProductDto!.AverageWeight > 0 && Math.Abs((AverageWeightDifference / ProductDto!.AverageWeight) * 100) < ProductDto!.AverageWeightTreshold);
|
||||||
|
|
||||||
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
||||||
|
[ToonDescription(Purpose = "Status flag", BusinessRule = "get => OrderItemPallets.Count > 0 && OrderItemPallets.All(oip => oip.IsAudited)")]
|
||||||
public bool IsAudited => OrderItemPallets.Count > 0 && OrderItemPallets.All(oip => oip.IsAudited);
|
public bool IsAudited => OrderItemPallets.Count > 0 && OrderItemPallets.All(oip => oip.IsAudited);
|
||||||
|
|
||||||
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
||||||
|
[ToonDescription(BusinessRule = "get => complex conditional logic based on IsAudited, IsMeasured, and OrderItemPallets status")]
|
||||||
public MeasuringStatus MeasuringStatus
|
public MeasuringStatus MeasuringStatus
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,30 @@
|
||||||
using FruitBank.Common.Interfaces;
|
using AyCode.Core.Serializers.Attributes;
|
||||||
|
using AyCode.Core.Serializers.Toons;
|
||||||
|
using FruitBank.Common.Interfaces;
|
||||||
using LinqToDB.Mapping;
|
using LinqToDB.Mapping;
|
||||||
using Mango.Nop.Core.Dtos;
|
using Mango.Nop.Core.Dtos;
|
||||||
using Mango.Nop.Core.Extensions;
|
using Mango.Nop.Core.Extensions;
|
||||||
using Mango.Nop.Core.Interfaces.ForeignKeys;
|
using Mango.Nop.Core.Interfaces.ForeignKeys;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
|
using Nop.Core.Domain.Catalog;
|
||||||
|
|
||||||
//using Nop.Core.Domain.Catalog;
|
//using Nop.Core.Domain.Catalog;
|
||||||
using Nop.Core.Domain.Common;
|
using Nop.Core.Domain.Common;
|
||||||
|
using Nop.Core.Domain.Orders;
|
||||||
using System.ComponentModel.DataAnnotations.Schema;
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
using System.Linq.Expressions;
|
using System.Linq.Expressions;
|
||||||
|
|
||||||
namespace FruitBank.Common.Dtos;
|
namespace FruitBank.Common.Dtos;
|
||||||
|
|
||||||
|
[AcBinarySerializable(false, true, false, true)]
|
||||||
|
[LinqToDB.Mapping.Table(Name = nameof(Product))]
|
||||||
|
[System.ComponentModel.DataAnnotations.Schema.Table(nameof(Product))]
|
||||||
|
[ToonDescription("Product data with measurements and generic attributes", TypeRelation = ToonTypeRelation.DtoOf, RelatedTypes = [typeof(Product)])]
|
||||||
public class ProductDto : MgProductDto, IProductDto
|
public class ProductDto : MgProductDto, IProductDto
|
||||||
{
|
{
|
||||||
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
||||||
private static Expression<Func<ProductDto, GenericAttributeDto, bool>> RelationWithGenericAttribute => (orderItemDto, genericAttributeDto) =>
|
private static Expression<Func<ProductDto, GenericAttributeDto, bool>> RelationWithGenericAttribute => (orderItemDto, genericAttributeDto) =>
|
||||||
orderItemDto.Id == genericAttributeDto.EntityId && genericAttributeDto.KeyGroup == "Product";// nameof(Product);
|
orderItemDto.Id == genericAttributeDto.EntityId && genericAttributeDto.KeyGroup == nameof(Product);// nameof(Product);
|
||||||
|
|
||||||
|
|
||||||
[Association(ThisKey = nameof(Id), OtherKey = nameof(GenericAttributeDto.EntityId), ExpressionPredicate = nameof(RelationWithGenericAttribute), CanBeNull = false)]
|
[Association(ThisKey = nameof(Id), OtherKey = nameof(GenericAttributeDto.EntityId), ExpressionPredicate = nameof(RelationWithGenericAttribute), CanBeNull = false)]
|
||||||
|
|
@ -29,6 +38,7 @@ public class ProductDto : MgProductDto, IProductDto
|
||||||
//{ }
|
//{ }
|
||||||
|
|
||||||
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
||||||
|
[ToonDescription(Purpose = "Master flag: if false, the system bypasses weight validation but still creates one Measurement Record (PalletItem) with TrayQuantity.", BusinessRule = "get => GenericAttributes.GetValueOrDefault<bool>('IsMeasurable')")]
|
||||||
public bool IsMeasurable
|
public bool IsMeasurable
|
||||||
{
|
{
|
||||||
get => GenericAttributes.GetValueOrDefault<bool>(nameof(IMeasurable.IsMeasurable));
|
get => GenericAttributes.GetValueOrDefault<bool>(nameof(IMeasurable.IsMeasurable));
|
||||||
|
|
@ -43,6 +53,7 @@ public class ProductDto : MgProductDto, IProductDto
|
||||||
}
|
}
|
||||||
|
|
||||||
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
||||||
|
[ToonDescription(BusinessRule = "get => GenericAttributes.GetValueOrDefault<double>('Tare')")]
|
||||||
public double Tare
|
public double Tare
|
||||||
{
|
{
|
||||||
get => GenericAttributes.GetValueOrDefault<double>(nameof(ITare.Tare));
|
get => GenericAttributes.GetValueOrDefault<double>(nameof(ITare.Tare));
|
||||||
|
|
@ -51,6 +62,7 @@ public class ProductDto : MgProductDto, IProductDto
|
||||||
}
|
}
|
||||||
|
|
||||||
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
||||||
|
[ToonDescription(BusinessRule = "get => GenericAttributes.GetValueOrDefault<double>('NetWeight')")]
|
||||||
public double NetWeight
|
public double NetWeight
|
||||||
{
|
{
|
||||||
get => GenericAttributes.GetValueOrDefault<double>(nameof(IMeasuringNetWeight.NetWeight));
|
get => GenericAttributes.GetValueOrDefault<double>(nameof(IMeasuringNetWeight.NetWeight));
|
||||||
|
|
@ -58,6 +70,7 @@ public class ProductDto : MgProductDto, IProductDto
|
||||||
}
|
}
|
||||||
|
|
||||||
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
||||||
|
[ToonDescription(BusinessRule = "get => GenericAttributes.GetValueOrDefault<int>('IncomingQuantity')")]
|
||||||
public int IncomingQuantity
|
public int IncomingQuantity
|
||||||
{
|
{
|
||||||
get => GenericAttributes.GetValueOrDefault<int>(nameof(IIncomingQuantity.IncomingQuantity));
|
get => GenericAttributes.GetValueOrDefault<int>(nameof(IIncomingQuantity.IncomingQuantity));
|
||||||
|
|
@ -65,19 +78,22 @@ public class ProductDto : MgProductDto, IProductDto
|
||||||
//set
|
//set
|
||||||
//{
|
//{
|
||||||
// var ga = GenericAttributes.FirstOrDefault(ga => ga.Key == nameof(IIncomingQuantity.IncomingQuantity)) ??
|
// var ga = GenericAttributes.FirstOrDefault(ga => ga.Key == nameof(IIncomingQuantity.IncomingQuantity)) ??
|
||||||
// GenericAttributes.AddNewGenericAttribute("Product", nameof(IIncomingQuantity.IncomingQuantity), value.ToString(), Id);
|
// GenericAttributes.AddNewGenericAttribute(nameof(Product), nameof(IIncomingQuantity.IncomingQuantity), value.ToString(), Id);
|
||||||
|
|
||||||
// ga.Value = value.ToString();
|
// ga.Value = value.ToString();
|
||||||
//}
|
//}
|
||||||
}
|
}
|
||||||
|
|
||||||
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
||||||
|
[ToonDescription(BusinessRule = "get => StockQuantity + IncomingQuantity")]
|
||||||
public int AvailableQuantity => StockQuantity + IncomingQuantity;
|
public int AvailableQuantity => StockQuantity + IncomingQuantity;
|
||||||
|
|
||||||
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
||||||
|
[ToonDescription(BusinessRule = "get => GenericAttributes.GetValueOrDefault<double>('AverageWeight')")]
|
||||||
public double AverageWeight => GenericAttributes.GetValueOrDefault<double>(nameof(IProductDto.AverageWeight));
|
public double AverageWeight => GenericAttributes.GetValueOrDefault<double>(nameof(IProductDto.AverageWeight));
|
||||||
|
|
||||||
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
||||||
|
[ToonDescription(BusinessRule = "get => GenericAttributes.GetValueOrDefault<double>('AverageWeightTreshold')")]
|
||||||
public double AverageWeightTreshold => GenericAttributes.GetValueOrDefault<double>(nameof(IProductDto.AverageWeightTreshold));
|
public double AverageWeightTreshold => GenericAttributes.GetValueOrDefault<double>(nameof(IProductDto.AverageWeightTreshold));
|
||||||
|
|
||||||
public bool HasMeasuringValues() => Id > 0 && NetWeight != 0 && IsMeasurable;
|
public bool HasMeasuringValues() => Id > 0 && NetWeight != 0 && IsMeasurable;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
# Dtos
|
||||||
|
|
||||||
|
Binary-serializable DTOs for efficient SignalR communication. All marked with `[AcBinarySerializable]`.
|
||||||
|
|
||||||
|
## Key Files
|
||||||
|
|
||||||
|
- **`OrderDto.cs`** — Order with items, measurement status, auditing, receipt date, GenericAttributes. Computed: IsMeasured, IsComplete, MeasuringStatus, RevisorId.
|
||||||
|
- **`OrderItemDto.cs`** — Order line item with OrderItemPallet collection. Computed: NetWeight, GrossWeight, AverageWeight, AverageWeightIsValid, IsMeasured, IsAudited.
|
||||||
|
- **`ProductDto.cs`** — Product with GenericAttribute-backed properties: IsMeasurable, Tare, AverageWeight, AverageWeightTreshold, IncomingQuantity, NetWeight.
|
||||||
|
- **`StockQuantityHistoryDto.cs`** — Stock history with net weight adjustments and inconsistency detection.
|
||||||
|
- **`GenericAttributeDto.cs`** — Key-value attribute wrapper. Polymorphic: KeyGroup = owner type, EntityId = owner ID.
|
||||||
|
|
||||||
|
## Why DTOs Exist
|
||||||
|
|
||||||
|
nopCommerce entities (Order, OrderItem, Product) are extended with measurement logic via these DTOs. The DTOs add computed properties and GenericAttribute access that the raw nopCommerce entities don't have.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
> **LLM Maintenance:** If you modify code in this folder, update this README to reflect the changes. If you notice the README content does not match the current code, automatically update the README to match the code.
|
||||||
|
|
@ -1,22 +1,31 @@
|
||||||
using FruitBank.Common.Interfaces;
|
using AyCode.Core.Serializers.Attributes;
|
||||||
|
using AyCode.Core.Serializers.Toons;
|
||||||
|
using FruitBank.Common.Interfaces;
|
||||||
using LinqToDB.Mapping;
|
using LinqToDB.Mapping;
|
||||||
using Mango.Nop.Core.Dtos;
|
using Mango.Nop.Core.Dtos;
|
||||||
using Mango.Nop.Core.Entities;
|
using Mango.Nop.Core.Entities;
|
||||||
using Mango.Nop.Core.Interfaces;
|
using Mango.Nop.Core.Interfaces;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using Nop.Core.Domain.Catalog;
|
using Nop.Core.Domain.Catalog;
|
||||||
|
using Nop.Core.Domain.Common;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.ComponentModel.DataAnnotations.Schema;
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using FruitBank.Common.Entities;
|
||||||
|
|
||||||
namespace FruitBank.Common.Dtos
|
namespace FruitBank.Common.Dtos
|
||||||
{
|
{
|
||||||
|
[AcBinarySerializable(false, true, false, true)]
|
||||||
|
[LinqToDB.Mapping.Table(Name = nameof(StockQuantityHistory))]
|
||||||
|
[System.ComponentModel.DataAnnotations.Schema.Table(nameof(StockQuantityHistory))]
|
||||||
|
[ToonDescription("Stock quantity history with net weight adjustments", TypeRelation = ToonTypeRelation.DtoOf, RelatedTypes = [typeof(StockQuantityHistory)])]
|
||||||
public class StockQuantityHistoryDto : MgStockQuantityHistoryDto<ProductDto>, IStockQuantityHistoryDto
|
public class StockQuantityHistoryDto : MgStockQuantityHistoryDto<ProductDto>, IStockQuantityHistoryDto
|
||||||
{
|
{
|
||||||
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
||||||
|
[ToonDescription(BusinessRule = "get => StockQuantityHistoryExt?.StockQuantityHistoryId")]
|
||||||
public int? StockQuantityHistoryId
|
public int? StockQuantityHistoryId
|
||||||
{
|
{
|
||||||
get => StockQuantityHistoryExt?.StockQuantityHistoryId;
|
get => StockQuantityHistoryExt?.StockQuantityHistoryId;
|
||||||
|
|
@ -24,6 +33,7 @@ namespace FruitBank.Common.Dtos
|
||||||
}
|
}
|
||||||
|
|
||||||
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
||||||
|
[ToonDescription(BusinessRule = "get => StockQuantityHistoryExt?.NetWeightAdjustment")]
|
||||||
public double? NetWeightAdjustment
|
public double? NetWeightAdjustment
|
||||||
{
|
{
|
||||||
get => StockQuantityHistoryExt?.NetWeightAdjustment;
|
get => StockQuantityHistoryExt?.NetWeightAdjustment;
|
||||||
|
|
@ -31,6 +41,7 @@ namespace FruitBank.Common.Dtos
|
||||||
}
|
}
|
||||||
|
|
||||||
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
||||||
|
[ToonDescription(BusinessRule = "get => StockQuantityHistoryExt?.NetWeight")]
|
||||||
public double? NetWeight
|
public double? NetWeight
|
||||||
{
|
{
|
||||||
get => StockQuantityHistoryExt?.NetWeight;
|
get => StockQuantityHistoryExt?.NetWeight;
|
||||||
|
|
@ -38,6 +49,7 @@ namespace FruitBank.Common.Dtos
|
||||||
}
|
}
|
||||||
|
|
||||||
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
||||||
|
[ToonDescription(Purpose = "Status flag", BusinessRule = "get => StockQuantityHistoryExt?.IsInconsistent ?? false")]
|
||||||
public bool IsInconsistent
|
public bool IsInconsistent
|
||||||
{
|
{
|
||||||
get => StockQuantityHistoryExt?.IsInconsistent ?? false;
|
get => StockQuantityHistoryExt?.IsInconsistent ?? false;
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,13 @@
|
||||||
using FruitBank.Common.Interfaces;
|
using AyCode.Core.Serializers.Attributes;
|
||||||
|
using AyCode.Core.Serializers.Toons;
|
||||||
|
using FruitBank.Common.Interfaces;
|
||||||
using LinqToDB.Mapping;
|
using LinqToDB.Mapping;
|
||||||
using Mango.Nop.Core.Entities;
|
using Mango.Nop.Core.Entities;
|
||||||
|
|
||||||
namespace FruitBank.Common.Entities;
|
namespace FruitBank.Common.Entities;
|
||||||
|
|
||||||
|
[AcBinarySerializable(false, true, false, true)]
|
||||||
|
[ToonDescription("Uploaded file with extracted text content", 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")]
|
||||||
[Table(Name = FruitBankConstClient.FilesDbTableName)]
|
[Table(Name = FruitBankConstClient.FilesDbTableName)]
|
||||||
[System.ComponentModel.DataAnnotations.Schema.Table(FruitBankConstClient.FilesDbTableName)]
|
[System.ComponentModel.DataAnnotations.Schema.Table(FruitBankConstClient.FilesDbTableName)]
|
||||||
public class Files : MgEntityBase, IFiles
|
public class Files : MgEntityBase, IFiles
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
using AyCode.Core.Serializers.Attributes;
|
||||||
|
using AyCode.Core.Serializers.Toons;
|
||||||
using FruitBank.Common.Dtos;
|
using FruitBank.Common.Dtos;
|
||||||
using FruitBank.Common.Enums;
|
using FruitBank.Common.Enums;
|
||||||
using FruitBank.Common.Interfaces;
|
using FruitBank.Common.Interfaces;
|
||||||
|
|
@ -8,6 +10,8 @@ using Newtonsoft.Json;
|
||||||
|
|
||||||
namespace FruitBank.Common.Entities;
|
namespace FruitBank.Common.Entities;
|
||||||
|
|
||||||
|
[ToonDescription("Base class for pallet measurements with net weight calculation",
|
||||||
|
Purpose = "Technically named 'Pallet' for legacy reasons, but represents a General Measurement Record. It is ALWAYS created for every item. If the product is not measurable, weights are 0 and only TrayQuantity is used.")]
|
||||||
public abstract class MeasuringItemPalletBase : MgEntityBase, IMeasuringItemPalletBase
|
public abstract class MeasuringItemPalletBase : MgEntityBase, IMeasuringItemPalletBase
|
||||||
{
|
{
|
||||||
private double _palletWeight;
|
private double _palletWeight;
|
||||||
|
|
@ -16,9 +20,12 @@ public abstract class MeasuringItemPalletBase : MgEntityBase, IMeasuringItemPall
|
||||||
|
|
||||||
[NotColumn]
|
[NotColumn]
|
||||||
protected int ForeignItemId;
|
protected int ForeignItemId;
|
||||||
|
|
||||||
[NotColumn]
|
[NotColumn]
|
||||||
|
[ToonDescription(BusinessRule = "get => ForeignItemId", Constraints = "[#SmartTypeConstraints]")]
|
||||||
public int ForeignKey => ForeignItemId;
|
public int ForeignKey => ForeignItemId;
|
||||||
|
|
||||||
|
[ToonDescription(Purpose = "Always recorded, regardless of measurability")]
|
||||||
public int TrayQuantity { get; set; }
|
public int TrayQuantity { get; set; }
|
||||||
|
|
||||||
[Column(DataType = DataType.DecFloat)]
|
[Column(DataType = DataType.DecFloat)]
|
||||||
|
|
@ -29,6 +36,7 @@ public abstract class MeasuringItemPalletBase : MgEntityBase, IMeasuringItemPall
|
||||||
}
|
}
|
||||||
|
|
||||||
[Column(DataType = DataType.DecFloat)]
|
[Column(DataType = DataType.DecFloat)]
|
||||||
|
[ToonDescription(Purpose = "Weight of the physical pallet if used; 0.0 if goods arrive without a pallet")]
|
||||||
public double PalletWeight
|
public double PalletWeight
|
||||||
{
|
{
|
||||||
get => _palletWeight;
|
get => _palletWeight;
|
||||||
|
|
@ -36,6 +44,7 @@ public abstract class MeasuringItemPalletBase : MgEntityBase, IMeasuringItemPall
|
||||||
}
|
}
|
||||||
|
|
||||||
[NotColumn, System.ComponentModel.DataAnnotations.Schema.NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
[NotColumn, System.ComponentModel.DataAnnotations.Schema.NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
||||||
|
[ToonDescription(BusinessRule = "get => GrossWeight - PalletWeight - (TrayQuantity * TareWeight)", Constraints = "[#SmartTypeConstraints], readonly")]
|
||||||
public double NetWeight
|
public double NetWeight
|
||||||
{
|
{
|
||||||
get => CalculateNetWeight();
|
get => CalculateNetWeight();
|
||||||
|
|
@ -43,6 +52,7 @@ public abstract class MeasuringItemPalletBase : MgEntityBase, IMeasuringItemPall
|
||||||
}
|
}
|
||||||
|
|
||||||
[Column(DataType = DataType.DecFloat, CanBeNull = false)]
|
[Column(DataType = DataType.DecFloat, CanBeNull = false)]
|
||||||
|
[ToonDescription(Purpose = "Measured gross weight; 0.0 if product is not measurable")]
|
||||||
public double GrossWeight
|
public double GrossWeight
|
||||||
{
|
{
|
||||||
get => _grossWeight;
|
get => _grossWeight;
|
||||||
|
|
@ -60,6 +70,7 @@ public abstract class MeasuringItemPalletBase : MgEntityBase, IMeasuringItemPall
|
||||||
public DateTime Modified { get; set; }
|
public DateTime Modified { get; set; }
|
||||||
|
|
||||||
[NotColumn, System.ComponentModel.DataAnnotations.Schema.NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
[NotColumn, System.ComponentModel.DataAnnotations.Schema.NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
||||||
|
[ToonDescription(BusinessRule = "get => IsMeasured ? MeasuringStatus.Finnished : Id > 0 ? MeasuringStatus.Started : MeasuringStatus.NotStarted")]
|
||||||
public virtual MeasuringStatus MeasuringStatus => IsMeasured ? MeasuringStatus.Finnished : Id > 0 ? MeasuringStatus.Started : MeasuringStatus.NotStarted;
|
public virtual MeasuringStatus MeasuringStatus => IsMeasured ? MeasuringStatus.Finnished : Id > 0 ? MeasuringStatus.Started : MeasuringStatus.NotStarted;
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
using AyCode.Core.Serializers.Attributes;
|
||||||
|
using AyCode.Core.Serializers.Toons;
|
||||||
using FruitBank.Common.Dtos;
|
using FruitBank.Common.Dtos;
|
||||||
using FruitBank.Common.Enums;
|
using FruitBank.Common.Enums;
|
||||||
using FruitBank.Common.Interfaces;
|
using FruitBank.Common.Interfaces;
|
||||||
|
|
@ -10,6 +12,8 @@ using Table = LinqToDB.Mapping.TableAttribute;
|
||||||
|
|
||||||
namespace FruitBank.Common.Entities;
|
namespace FruitBank.Common.Entities;
|
||||||
|
|
||||||
|
[AcBinarySerializable(false, true, false, true)]
|
||||||
|
[ToonDescription("Pallet measurements for order items with audit tracking", Purpose = "A measurement record for outgoing goods, used to verify that the net weight being sent to the customer is accurate and audited. NOTE: Despite the 'Pallet' name, this is a general measurement record that is ALWAYS created for every item. If the product is not measurable (IsMeasurable=false), weights are recorded as 0.0 and only TrayQuantity is stored.")]
|
||||||
[Table(Name = FruitBankConstClient.OrderItemPalletDbTableName)]
|
[Table(Name = FruitBankConstClient.OrderItemPalletDbTableName)]
|
||||||
[System.ComponentModel.DataAnnotations.Schema.Table(FruitBankConstClient.OrderItemPalletDbTableName)]
|
[System.ComponentModel.DataAnnotations.Schema.Table(FruitBankConstClient.OrderItemPalletDbTableName)]
|
||||||
public class OrderItemPallet : MeasuringItemPalletBase, IOrderItemPallet
|
public class OrderItemPallet : MeasuringItemPalletBase, IOrderItemPallet
|
||||||
|
|
@ -20,7 +24,11 @@ public class OrderItemPallet : MeasuringItemPalletBase, IOrderItemPallet
|
||||||
set => ForeignItemId = value;
|
set => ForeignItemId = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[ToonDescription(Purpose = "User/Customer ID of the quality auditor")]
|
||||||
public int RevisorId { get; set; }
|
public int RevisorId { get; set; }
|
||||||
|
|
||||||
|
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
||||||
|
[ToonDescription(Purpose = "Status flag", BusinessRule = "get => RevisorId > 0")]
|
||||||
public bool IsAudited => RevisorId > 0;
|
public bool IsAudited => RevisorId > 0;
|
||||||
|
|
||||||
//[JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
//[JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
||||||
|
|
@ -28,6 +36,7 @@ public class OrderItemPallet : MeasuringItemPalletBase, IOrderItemPallet
|
||||||
public OrderItemDto? OrderItemDto { get; set; }
|
public OrderItemDto? OrderItemDto { get; set; }
|
||||||
|
|
||||||
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
||||||
|
[ToonDescription(BusinessRule = "get => IsAudited ? MeasuringStatus.Audited : base.MeasuringStatus")]
|
||||||
public override MeasuringStatus MeasuringStatus => IsAudited ? MeasuringStatus.Audited : base.MeasuringStatus;
|
public override MeasuringStatus MeasuringStatus => IsAudited ? MeasuringStatus.Audited : base.MeasuringStatus;
|
||||||
public override double CalculateNetWeight() => base.CalculateNetWeight();
|
public override double CalculateNetWeight() => base.CalculateNetWeight();
|
||||||
|
|
||||||
|
|
@ -37,6 +46,7 @@ public class OrderItemPallet : MeasuringItemPalletBase, IOrderItemPallet
|
||||||
}
|
}
|
||||||
|
|
||||||
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
||||||
|
[ToonDescription(BusinessRule = "get => double.Round(NetWeight / TrayQuantity, 1)")]
|
||||||
public double AverageWeight => double.Round(NetWeight / TrayQuantity, 1);
|
public double AverageWeight => double.Round(NetWeight / TrayQuantity, 1);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,14 @@
|
||||||
using FruitBank.Common.Interfaces;
|
using AyCode.Core.Serializers.Attributes;
|
||||||
|
using AyCode.Core.Serializers.Toons;
|
||||||
|
using FruitBank.Common.Interfaces;
|
||||||
using LinqToDB;
|
using LinqToDB;
|
||||||
using LinqToDB.Mapping;
|
using LinqToDB.Mapping;
|
||||||
using Mango.Nop.Core.Entities;
|
using Mango.Nop.Core.Entities;
|
||||||
|
|
||||||
namespace FruitBank.Common.Entities;
|
namespace FruitBank.Common.Entities;
|
||||||
|
|
||||||
|
[AcBinarySerializable(false, true, false, true)]
|
||||||
|
[ToonDescription("Pallet type definition with size and weight")]
|
||||||
[Table(Name = FruitBankConstClient.PalletDbTableName)]
|
[Table(Name = FruitBankConstClient.PalletDbTableName)]
|
||||||
[System.ComponentModel.DataAnnotations.Schema.Table(FruitBankConstClient.PalletDbTableName)]
|
[System.ComponentModel.DataAnnotations.Schema.Table(FruitBankConstClient.PalletDbTableName)]
|
||||||
public class Pallet : MgEntityBase, IPallet
|
public class Pallet : MgEntityBase, IPallet
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,13 @@
|
||||||
using FruitBank.Common.Interfaces;
|
using AyCode.Core.Serializers.Attributes;
|
||||||
|
using AyCode.Core.Serializers.Toons;
|
||||||
|
using FruitBank.Common.Interfaces;
|
||||||
using LinqToDB.Mapping;
|
using LinqToDB.Mapping;
|
||||||
using Mango.Nop.Core.Entities;
|
using Mango.Nop.Core.Entities;
|
||||||
|
|
||||||
namespace FruitBank.Common.Entities;
|
namespace FruitBank.Common.Entities;
|
||||||
|
|
||||||
|
[AcBinarySerializable(false, true, false, true)]
|
||||||
|
[ToonDescription("Business partner with address and tax information", Purpose = "Represents an external legal entity, specifically a Supplier who provides goods or a business partner involved in the procurement chain")]
|
||||||
[Table(Name = FruitBankConstClient.PartnerDbTableName)]
|
[Table(Name = FruitBankConstClient.PartnerDbTableName)]
|
||||||
[System.ComponentModel.DataAnnotations.Schema.Table(FruitBankConstClient.PartnerDbTableName)]
|
[System.ComponentModel.DataAnnotations.Schema.Table(FruitBankConstClient.PartnerDbTableName)]
|
||||||
public class Partner : MgEntityBase, IPartner
|
public class Partner : MgEntityBase, IPartner
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
# Entities
|
||||||
|
|
||||||
|
Domain entities for inbound/outbound goods tracking and inventory. All map to `fb`-prefixed database tables.
|
||||||
|
|
||||||
|
## Shipping (Inbound)
|
||||||
|
|
||||||
|
- **`Shipping.cs`** — Physical delivery event (truck arrival). Table: `fbShipping`.
|
||||||
|
- **`ShippingDocument.cs`** — Supplier delivery note/invoice. Table: `fbShippingDocument`.
|
||||||
|
- **`ShippingItem.cs`** — Product line on document with declared vs measured discrepancies. Table: `fbShippingItem`.
|
||||||
|
- **`ShippingItemPallet.cs`** — Measurement record for incoming goods. Table: `fbShippingItemPallet`.
|
||||||
|
- **`ShippingDocumentToFiles.cs`** — Many-to-many link: document ↔ file with DocumentType. Table: `fbShippingDocumentToFiles`.
|
||||||
|
- **`Partner.cs`** — External supplier with address and tax info. Table: `fbPartner`.
|
||||||
|
|
||||||
|
## Order (Outbound)
|
||||||
|
|
||||||
|
- **`OrderItemPallet.cs`** — Measurement record for outgoing goods with RevisorId for audit. Table: `fbOrderItemPallet`.
|
||||||
|
|
||||||
|
## Inventory
|
||||||
|
|
||||||
|
- **`StockTaking.cs`** — Inventory session record. Table: `fbStockTaking`.
|
||||||
|
- **`StockTakingItem.cs`** — Line item reconciling snapshot vs measured quantities. Table: `fbStockTakingItem`.
|
||||||
|
- **`StockTakingItemPallet.cs`** — Measurement record for inventory. Table: `fbStockTakingItemPallet`.
|
||||||
|
- **`StockQuantityHistoryExt.cs`** — Extended weight metadata for stock reconciliation.
|
||||||
|
|
||||||
|
## Shared
|
||||||
|
|
||||||
|
- **`MeasuringItemPalletBase.cs`** — Abstract base for all three measurement hierarchies. Defines NetWeight formula, validation methods, CreatorId/ModifierId tracking.
|
||||||
|
- **`Pallet.cs`** — Physical pallet type definition (name, size, weight). Table: `fbPallet`.
|
||||||
|
- **`Files.cs`** — Uploaded file with OCR-extracted RawText. Table: `fbFiles`.
|
||||||
|
|
||||||
|
## Critical: "Pallet" Naming
|
||||||
|
|
||||||
|
Despite the name, `XxxItemPallet` entities are **measurement records**, NOT physical pallets. They are ALWAYS created for every item. For non-measurable products, weights = 0.0 and only TrayQuantity is tracked.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
> **LLM Maintenance:** If you modify code in this folder, update this README to reflect the changes. If you notice the README content does not match the current code, automatically update the README to match the code.
|
||||||
|
|
@ -1,10 +1,14 @@
|
||||||
using AyCode.Interfaces.EntityComment;
|
using AyCode.Core.Serializers.Attributes;
|
||||||
|
using AyCode.Core.Serializers.Toons;
|
||||||
|
using AyCode.Interfaces.EntityComment;
|
||||||
using FruitBank.Common.Interfaces;
|
using FruitBank.Common.Interfaces;
|
||||||
using LinqToDB.Mapping;
|
using LinqToDB.Mapping;
|
||||||
using Mango.Nop.Core.Entities;
|
using Mango.Nop.Core.Entities;
|
||||||
|
|
||||||
namespace FruitBank.Common.Entities;
|
namespace FruitBank.Common.Entities;
|
||||||
|
|
||||||
|
[AcBinarySerializable(false, true, false, true)]
|
||||||
|
[ToonDescription("Shipping record with documents and measurement tracking", Purpose = "Represents a physical inbound delivery event (truck arrival) at the warehouse, tracking the vehicle and the overall measurement status of the shipment")]
|
||||||
[Table(Name = FruitBankConstClient.ShippingDbTableName)]
|
[Table(Name = FruitBankConstClient.ShippingDbTableName)]
|
||||||
[System.ComponentModel.DataAnnotations.Schema.Table(FruitBankConstClient.ShippingDbTableName)]
|
[System.ComponentModel.DataAnnotations.Schema.Table(FruitBankConstClient.ShippingDbTableName)]
|
||||||
public class Shipping : MgEntityBase, IShipping, IEntityComment
|
public class Shipping : MgEntityBase, IShipping, IEntityComment
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,14 @@
|
||||||
using System.Collections.ObjectModel;
|
using AyCode.Core.Serializers.Attributes;
|
||||||
|
using AyCode.Core.Serializers.Toons;
|
||||||
using FruitBank.Common.Interfaces;
|
using FruitBank.Common.Interfaces;
|
||||||
using LinqToDB.Mapping;
|
using LinqToDB.Mapping;
|
||||||
using Mango.Nop.Core.Entities;
|
using Mango.Nop.Core.Entities;
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
|
|
||||||
namespace FruitBank.Common.Entities;
|
namespace FruitBank.Common.Entities;
|
||||||
|
|
||||||
|
[AcBinarySerializable(false, true, false, true)]
|
||||||
|
[ToonDescription("Shipping document with partner, items and files", Purpose = "A digital representation of a supplier's delivery note or invoice associated with the shipment, used for reconciling paper-based data with measured reality")]
|
||||||
[Table(Name = FruitBankConstClient.ShippingDocumentDbTableName)]
|
[Table(Name = FruitBankConstClient.ShippingDocumentDbTableName)]
|
||||||
[System.ComponentModel.DataAnnotations.Schema.Table(FruitBankConstClient.ShippingDocumentDbTableName)]
|
[System.ComponentModel.DataAnnotations.Schema.Table(FruitBankConstClient.ShippingDocumentDbTableName)]
|
||||||
public class ShippingDocument : MgEntityBase, IShippingDocument
|
public class ShippingDocument : MgEntityBase, IShippingDocument
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,28 @@
|
||||||
using System.ComponentModel.DataAnnotations.Schema;
|
using AyCode.Core.Serializers.Attributes;
|
||||||
|
using AyCode.Core.Serializers.Toons;
|
||||||
using FruitBank.Common.Interfaces;
|
using FruitBank.Common.Interfaces;
|
||||||
using LinqToDB.Mapping;
|
using LinqToDB.Mapping;
|
||||||
using Mango.Nop.Core.Entities;
|
using Mango.Nop.Core.Entities;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
|
|
||||||
namespace FruitBank.Common.Entities;
|
namespace FruitBank.Common.Entities;
|
||||||
|
|
||||||
|
[AcBinarySerializable(false, true, false, true)]
|
||||||
|
[ToonDescription("Links shipping documents to files with document type", Purpose = "A many-to-many link table that associates general uploaded files with specific shipping documents, assigning a functional context (DocumentType) to each file, such as identifying which PDF is the supplier's invoice versus the packing list")]
|
||||||
[LinqToDB.Mapping.Table(Name = FruitBankConstClient.ShippingDocumentToFilesDbTableName)]
|
[LinqToDB.Mapping.Table(Name = FruitBankConstClient.ShippingDocumentToFilesDbTableName)]
|
||||||
[System.ComponentModel.DataAnnotations.Schema.Table(FruitBankConstClient.ShippingDocumentToFilesDbTableName)]
|
[System.ComponentModel.DataAnnotations.Schema.Table(FruitBankConstClient.ShippingDocumentToFilesDbTableName)]
|
||||||
public class ShippingDocumentToFiles : MgEntityBase, IShippingDocumentToFiles
|
public class ShippingDocumentToFiles : MgEntityBase, IShippingDocumentToFiles
|
||||||
{
|
{
|
||||||
public int FilesId { get; set; }
|
public int FilesId { get; set; }
|
||||||
|
|
||||||
public int ShippingDocumentId { get; set; }
|
public int ShippingDocumentId { get; set; }
|
||||||
|
|
||||||
|
[ToonDescription(Constraints = "enum-reference: DocumentType")]
|
||||||
public int DocumentTypeId { get; set; }
|
public int DocumentTypeId { get; set; }
|
||||||
|
|
||||||
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
||||||
|
[ToonDescription(Purpose = "Enum wrapper", BusinessRule = "get, set => DocumentTypeId")]
|
||||||
public DocumentType DocumentType
|
public DocumentType DocumentType
|
||||||
{
|
{
|
||||||
get => (DocumentType)DocumentTypeId;
|
get => (DocumentType)DocumentTypeId;
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,6 @@
|
||||||
using AyCode.Core.Interfaces;
|
using AyCode.Core.Interfaces;
|
||||||
|
using AyCode.Core.Serializers.Attributes;
|
||||||
|
using AyCode.Core.Serializers.Toons;
|
||||||
using FruitBank.Common.Dtos;
|
using FruitBank.Common.Dtos;
|
||||||
using FruitBank.Common.Enums;
|
using FruitBank.Common.Enums;
|
||||||
using FruitBank.Common.Interfaces;
|
using FruitBank.Common.Interfaces;
|
||||||
|
|
@ -10,13 +12,15 @@ using Nop.Core.Domain.Customers;
|
||||||
using Nop.Core.Domain.Orders;
|
using Nop.Core.Domain.Orders;
|
||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using System.ComponentModel.DataAnnotations.Schema;
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
|
using Column = LinqToDB.Mapping.ColumnAttribute;
|
||||||
//using Nop.Core.Domain.Catalog;
|
//using Nop.Core.Domain.Catalog;
|
||||||
using DataType = LinqToDB.DataType;
|
using DataType = LinqToDB.DataType;
|
||||||
using Column = LinqToDB.Mapping.ColumnAttribute;
|
|
||||||
using Table = LinqToDB.Mapping.TableAttribute;
|
using Table = LinqToDB.Mapping.TableAttribute;
|
||||||
|
|
||||||
namespace FruitBank.Common.Entities;
|
namespace FruitBank.Common.Entities;
|
||||||
|
|
||||||
|
[AcBinarySerializable(false, true, false, true)]
|
||||||
|
[ToonDescription("Shipping document item with measurements and pallets", Purpose = "Represents a specific product line item within a shipping document, storing the discrepancy between the supplier's declared weight/quantity and the warehouse's measured values")]
|
||||||
[Table(Name = FruitBankConstClient.ShippingItemDbTableName)]
|
[Table(Name = FruitBankConstClient.ShippingItemDbTableName)]
|
||||||
[System.ComponentModel.DataAnnotations.Schema.Table(FruitBankConstClient.ShippingItemDbTableName)]
|
[System.ComponentModel.DataAnnotations.Schema.Table(FruitBankConstClient.ShippingItemDbTableName)]
|
||||||
public class ShippingItem : MgEntityBase, IShippingItem
|
public class ShippingItem : MgEntityBase, IShippingItem
|
||||||
|
|
@ -29,10 +33,8 @@ public class ShippingItem : MgEntityBase, IShippingItem
|
||||||
public string NameOnDocument { get; set; }
|
public string NameOnDocument { get; set; }
|
||||||
public string HungarianName { get; set; }
|
public string HungarianName { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// get => ProductDto?.Name ?? Name
|
|
||||||
/// </summary>
|
|
||||||
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
||||||
|
[ToonDescription(BusinessRule = "get => ProductDto?.Name ?? Name")]
|
||||||
public string ProductName => ProductDto?.Name ?? Name;
|
public string ProductName => ProductDto?.Name ?? Name;
|
||||||
|
|
||||||
public int PalletsOnDocument { get; set; }
|
public int PalletsOnDocument { get; set; }
|
||||||
|
|
@ -84,6 +86,7 @@ public class ShippingItem : MgEntityBase, IShippingItem
|
||||||
public DateTime Modified { get; set; }
|
public DateTime Modified { get; set; }
|
||||||
|
|
||||||
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
||||||
|
[ToonDescription(BusinessRule = "get => complex conditional logic based on IsMeasured and ShippingItemPallets status")]
|
||||||
public MeasuringStatus MeasuringStatus
|
public MeasuringStatus MeasuringStatus
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,6 @@
|
||||||
using FruitBank.Common.Interfaces;
|
using AyCode.Core.Serializers.Attributes;
|
||||||
|
using AyCode.Core.Serializers.Toons;
|
||||||
|
using FruitBank.Common.Interfaces;
|
||||||
using LinqToDB.Mapping;
|
using LinqToDB.Mapping;
|
||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using System.ComponentModel.DataAnnotations.Schema;
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
|
|
@ -6,6 +8,8 @@ using System.Security.Cryptography.X509Certificates;
|
||||||
|
|
||||||
namespace FruitBank.Common.Entities;
|
namespace FruitBank.Common.Entities;
|
||||||
|
|
||||||
|
[AcBinarySerializable(false, true, false, true)]
|
||||||
|
[ToonDescription("Pallet measurements for shipping items", Purpose = "The smallest unit of measurement tracking, representing a single physical measurement event. NOTE: Technically named 'Pallet' for legacy reasons, but it is ALWAYS created even if goods arrive without a physical pallet. For non-measurable products, weights are 0.0 and only TrayQuantity is tracked for tare-weight calculations.")]
|
||||||
[LinqToDB.Mapping.Table(Name = FruitBankConstClient.ShippingItemPalletDbTableName)]
|
[LinqToDB.Mapping.Table(Name = FruitBankConstClient.ShippingItemPalletDbTableName)]
|
||||||
[System.ComponentModel.DataAnnotations.Schema.Table(FruitBankConstClient.ShippingItemPalletDbTableName)]
|
[System.ComponentModel.DataAnnotations.Schema.Table(FruitBankConstClient.ShippingItemPalletDbTableName)]
|
||||||
public class ShippingItemPallet : MeasuringItemPalletBase, IShippingItemPallet
|
public class ShippingItemPallet : MeasuringItemPalletBase, IShippingItemPallet
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,11 @@
|
||||||
using AyCode.Interfaces.Entities;
|
using AyCode.Core.Serializers.Attributes;
|
||||||
using AyCode.Interfaces.TimeStampInfo;
|
using AyCode.Core.Serializers.Toons;
|
||||||
using FruitBank.Common;
|
using AyCode.Interfaces.Entities;
|
||||||
using FruitBank.Common.Interfaces;
|
|
||||||
using LinqToDB;
|
using LinqToDB;
|
||||||
using LinqToDB.Mapping;
|
using LinqToDB.Mapping;
|
||||||
using Nop.Core.Domain.Catalog;
|
using Mango.Nop.Core.Entities;
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace Mango.Nop.Core.Entities
|
namespace FruitBank.Common.Entities
|
||||||
{
|
{
|
||||||
public interface IStockQuantityHistoryExt : IEntityInt
|
public interface IStockQuantityHistoryExt : IEntityInt
|
||||||
{
|
{
|
||||||
|
|
@ -21,8 +15,10 @@ namespace Mango.Nop.Core.Entities
|
||||||
public bool IsInconsistent { get; set; }
|
public bool IsInconsistent { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[AcBinarySerializable(false, true, false, true)]
|
||||||
[Table(Name = FruitBankConstClient.StockQuantityHistoryExtDbTableName)]
|
[Table(Name = FruitBankConstClient.StockQuantityHistoryExtDbTableName)]
|
||||||
[System.ComponentModel.DataAnnotations.Schema.Table(FruitBankConstClient.StockQuantityHistoryExtDbTableName)]
|
[System.ComponentModel.DataAnnotations.Schema.Table(FruitBankConstClient.StockQuantityHistoryExtDbTableName)]
|
||||||
|
[ToonDescription("Extended weight-metadata for StockQuantityHistory", Purpose = "Validates quantity deltas against measured weight to detect inconsistencies")]
|
||||||
public class StockQuantityHistoryExt : MgEntityBase, IStockQuantityHistoryExt
|
public class StockQuantityHistoryExt : MgEntityBase, IStockQuantityHistoryExt
|
||||||
{
|
{
|
||||||
public int StockQuantityHistoryId { get; set; }
|
public int StockQuantityHistoryId { get; set; }
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,12 @@
|
||||||
using LinqToDB.Mapping;
|
using AyCode.Core.Serializers.Attributes;
|
||||||
|
using AyCode.Core.Serializers.Toons;
|
||||||
|
using LinqToDB.Mapping;
|
||||||
using Mango.Nop.Core.Entities;
|
using Mango.Nop.Core.Entities;
|
||||||
|
|
||||||
namespace FruitBank.Common.Entities;
|
namespace FruitBank.Common.Entities;
|
||||||
|
|
||||||
|
[AcBinarySerializable(false, true, false, true)]
|
||||||
|
[ToonDescription("Inventory session record", Purpose = "Orchestrates inventory sessions by freezing logical stock states")]
|
||||||
[Table(Name = FruitBankConstClient.StockTakingDbTableName)]
|
[Table(Name = FruitBankConstClient.StockTakingDbTableName)]
|
||||||
[System.ComponentModel.DataAnnotations.Schema.Table(FruitBankConstClient.StockTakingDbTableName)]
|
[System.ComponentModel.DataAnnotations.Schema.Table(FruitBankConstClient.StockTakingDbTableName)]
|
||||||
public class StockTaking : MgStockTaking<StockTakingItem>
|
public class StockTaking : MgStockTaking<StockTakingItem>
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,18 @@
|
||||||
using FruitBank.Common.Dtos;
|
using AyCode.Core.Serializers.Attributes;
|
||||||
|
using AyCode.Core.Serializers.Toons;
|
||||||
|
using FruitBank.Common.Dtos;
|
||||||
using LinqToDB;
|
using LinqToDB;
|
||||||
using LinqToDB.Mapping;
|
using LinqToDB.Mapping;
|
||||||
using Mango.Nop.Core.Entities;
|
using Mango.Nop.Core.Entities;
|
||||||
using System.ComponentModel.DataAnnotations.Schema;
|
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
using Column = LinqToDB.Mapping.ColumnAttribute;
|
using Column = LinqToDB.Mapping.ColumnAttribute;
|
||||||
using Table = LinqToDB.Mapping.TableAttribute;
|
using Table = LinqToDB.Mapping.TableAttribute;
|
||||||
|
|
||||||
namespace FruitBank.Common.Entities;
|
namespace FruitBank.Common.Entities;
|
||||||
|
|
||||||
|
[AcBinarySerializable(false, true, false, true)]
|
||||||
|
[ToonDescription("Line item for product reconciliation", Purpose = "Reconciles snapshot quantity with physical count to calculate final stock delta")]
|
||||||
[Table(Name = FruitBankConstClient.StockTakingItemDbTableName)]
|
[Table(Name = FruitBankConstClient.StockTakingItemDbTableName)]
|
||||||
[System.ComponentModel.DataAnnotations.Schema.Table(FruitBankConstClient.StockTakingItemDbTableName)]
|
[System.ComponentModel.DataAnnotations.Schema.Table(FruitBankConstClient.StockTakingItemDbTableName)]
|
||||||
public class StockTakingItem : MgStockTakingItem<StockTaking, ProductDto>
|
public class StockTakingItem : MgStockTakingItem<StockTaking, ProductDto>
|
||||||
|
|
@ -21,27 +25,34 @@ public class StockTakingItem : MgStockTakingItem<StockTaking, ProductDto>
|
||||||
[Column(DataType = DataType.DecFloat, CanBeNull = false)]
|
[Column(DataType = DataType.DecFloat, CanBeNull = false)]
|
||||||
public double MeasuredNetWeight { get; set; }
|
public double MeasuredNetWeight { get; set; }
|
||||||
|
|
||||||
|
[ToonDescription(Purpose = "Reserved stock buffer (not yet shipped) to prevent double-deduction during closing")]
|
||||||
public int InProcessOrdersQuantity { get; set; }
|
public int InProcessOrdersQuantity { get; set; }
|
||||||
|
|
||||||
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
||||||
|
[ToonDescription(BusinessRule = "get => OriginalStockQuantity + InProcessOrdersQuantity", Purpose = "Snapshot of total logical stock at session start")]
|
||||||
public int TotalOriginalQuantity => OriginalStockQuantity + InProcessOrdersQuantity;
|
public int TotalOriginalQuantity => OriginalStockQuantity + InProcessOrdersQuantity;
|
||||||
|
|
||||||
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
||||||
|
[ToonDescription(Purpose = "Final adjustment value for Product.StockQuantity", BusinessRule = "get => IsMeasured ? MeasuredStockQuantity - TotalOriginalQuantity : 0")]
|
||||||
public int QuantityDiff => IsMeasured ? MeasuredStockQuantity - TotalOriginalQuantity : 0;
|
public int QuantityDiff => IsMeasured ? MeasuredStockQuantity - TotalOriginalQuantity : 0;
|
||||||
|
|
||||||
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
||||||
|
[ToonDescription(BusinessRule = "get => IsMeasurable && IsMeasured ? double.Round(MeasuredNetWeight - OriginalNetWeight, 1) : 0d")]
|
||||||
public double NetWeightDiff => IsMeasurable && IsMeasured ? double.Round(MeasuredNetWeight - OriginalNetWeight, 1) : 0d;
|
public double NetWeightDiff => IsMeasurable && IsMeasured ? double.Round(MeasuredNetWeight - OriginalNetWeight, 1) : 0d;
|
||||||
|
|
||||||
[Association(ThisKey = nameof(Id), OtherKey = nameof(StockTakingItemPallet.StockTakingItemId), CanBeNull = true)]
|
[Association(ThisKey = nameof(Id), OtherKey = nameof(StockTakingItemPallet.StockTakingItemId), CanBeNull = true)]
|
||||||
public List<StockTakingItemPallet>? StockTakingItemPallets { get; set; }
|
public List<StockTakingItemPallet>? StockTakingItemPallets { get; set; }
|
||||||
|
|
||||||
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
||||||
|
[ToonDescription(Purpose = "Status flag", BusinessRule = "get => !IsInvalid && (TotalOriginalQuantity != 0 || OriginalNetWeight != 0)")]
|
||||||
public bool IsRequiredForMeasuring => !IsInvalid && (TotalOriginalQuantity != 0 || OriginalNetWeight != 0);
|
public bool IsRequiredForMeasuring => !IsInvalid && (TotalOriginalQuantity != 0 || OriginalNetWeight != 0);
|
||||||
|
|
||||||
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
||||||
|
[ToonDescription(Purpose = "Status flag", BusinessRule = "get => TotalOriginalQuantity < 0")]
|
||||||
public bool IsInvalid => TotalOriginalQuantity < 0;
|
public bool IsInvalid => TotalOriginalQuantity < 0;
|
||||||
|
|
||||||
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
|
||||||
|
[ToonDescription(BusinessRule = "get => conditional string based on IsInvalid, IsMeasured, IsRequiredForMeasuring")]
|
||||||
public string DisplayText
|
public string DisplayText
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,6 @@
|
||||||
using FruitBank.Common.Dtos;
|
using AyCode.Core.Serializers.Attributes;
|
||||||
|
using AyCode.Core.Serializers.Toons;
|
||||||
|
using FruitBank.Common.Dtos;
|
||||||
using FruitBank.Common.Interfaces;
|
using FruitBank.Common.Interfaces;
|
||||||
using LinqToDB.Mapping;
|
using LinqToDB.Mapping;
|
||||||
using Mango.Nop.Core.Entities;
|
using Mango.Nop.Core.Entities;
|
||||||
|
|
@ -12,6 +14,8 @@ public interface IStockTakingItemPallet : IMeasuringItemPalletBase
|
||||||
public StockTakingItem? StockTakingItem{ get; set; }
|
public StockTakingItem? StockTakingItem{ get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[AcBinarySerializable(false, true, false, true)]
|
||||||
|
[ToonDescription("Weight record for inventory item", Purpose = "Granular weight-based evidence for a stock taking line item. NOTE: This record is mandatory for every inventory item. If weighing is skipped (non-measurable), it serves as a container for TrayQuantity with zeroed weight fields. The term 'Pallet' is a legacy naming convention.")]
|
||||||
[LinqToDB.Mapping.Table(Name = FruitBankConstClient.StockTakingItemPalletDbTableName)]
|
[LinqToDB.Mapping.Table(Name = FruitBankConstClient.StockTakingItemPalletDbTableName)]
|
||||||
[System.ComponentModel.DataAnnotations.Schema.Table(FruitBankConstClient.StockTakingItemPalletDbTableName)]
|
[System.ComponentModel.DataAnnotations.Schema.Table(FruitBankConstClient.StockTakingItemPalletDbTableName)]
|
||||||
public class StockTakingItemPallet : MeasuringItemPalletBase, IStockTakingItemPallet
|
public class StockTakingItemPallet : MeasuringItemPalletBase, IStockTakingItemPallet
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
# Enums
|
||||||
|
|
||||||
|
Core enumeration types for measurement and document classification.
|
||||||
|
|
||||||
|
## Key Files
|
||||||
|
|
||||||
|
- **`MeasuringStatus.cs`** — NotStarted(0) → Started(10) → **Finnished**(20) → Audited(30). Note: "Finnished" is an intentional legacy typo — do NOT fix.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
> **LLM Maintenance:** If you modify code in this folder, update this README to reflect the changes. If you notice the README content does not match the current code, automatically update the README to match the code.
|
||||||
|
|
@ -5,6 +5,7 @@
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
|
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
|
||||||
|
<EmitCompilerGeneratedFiles>false</EmitCompilerGeneratedFiles>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|
@ -17,26 +18,32 @@
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Reference Include="AyCode.Core">
|
<Reference Include="AyCode.Core">
|
||||||
<HintPath>..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Services.Server\bin\FruitBank\Debug\net9.0\AyCode.Core.dll</HintPath>
|
<HintPath>..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Services.Server\bin\FruitBank\$(Configuration)\net9.0\AyCode.Core.dll</HintPath>
|
||||||
</Reference>
|
</Reference>
|
||||||
<Reference Include="AyCode.Entities">
|
<Reference Include="AyCode.Entities">
|
||||||
<HintPath>..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Services.Server\bin\FruitBank\Debug\net9.0\AyCode.Entities.dll</HintPath>
|
<HintPath>..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Services.Server\bin\FruitBank\$(Configuration)\net9.0\AyCode.Entities.dll</HintPath>
|
||||||
</Reference>
|
</Reference>
|
||||||
<Reference Include="AyCode.Interfaces">
|
<Reference Include="AyCode.Interfaces">
|
||||||
<HintPath>..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Services.Server\bin\FruitBank\Debug\net9.0\AyCode.Interfaces.dll</HintPath>
|
<HintPath>..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Services.Server\bin\FruitBank\$(Configuration)\net9.0\AyCode.Interfaces.dll</HintPath>
|
||||||
</Reference>
|
</Reference>
|
||||||
<Reference Include="AyCode.Models">
|
<Reference Include="AyCode.Models">
|
||||||
<HintPath>..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Services.Server\bin\FruitBank\Debug\net9.0\AyCode.Models.dll</HintPath>
|
<HintPath>..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Services.Server\bin\FruitBank\$(Configuration)\net9.0\AyCode.Models.dll</HintPath>
|
||||||
</Reference>
|
</Reference>
|
||||||
<Reference Include="AyCode.Services">
|
<Reference Include="AyCode.Services">
|
||||||
<HintPath>..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Services.Server\bin\FruitBank\Debug\net9.0\AyCode.Services.dll</HintPath>
|
<HintPath>..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Services.Server\bin\FruitBank\$(Configuration)\net9.0\AyCode.Services.dll</HintPath>
|
||||||
</Reference>
|
</Reference>
|
||||||
<Reference Include="AyCode.Utils">
|
<Reference Include="AyCode.Utils">
|
||||||
<HintPath>..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Services.Server\bin\FruitBank\Debug\net9.0\AyCode.Utils.dll</HintPath>
|
<HintPath>..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Services.Server\bin\FruitBank\$(Configuration)\net9.0\AyCode.Utils.dll</HintPath>
|
||||||
</Reference>
|
</Reference>
|
||||||
<Reference Include="Mango.Nop.Core">
|
<Reference Include="Mango.Nop.Core">
|
||||||
<HintPath>..\..\NopCommerce.Common\4.70\Libraries\Mango.Nop.Core\bin\FruitBank\Debug\net9.0\Mango.Nop.Core.dll</HintPath>
|
<HintPath>..\..\NopCommerce.Common\4.70\Libraries\Mango.Nop.Core\bin\FruitBank\$(Configuration)\net9.0\Mango.Nop.Core.dll</HintPath>
|
||||||
</Reference>
|
</Reference>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Core.Serializers.SourceGenerator\AyCode.Core.Serializers.SourceGenerator.csproj"
|
||||||
|
OutputItemType="Analyzer"
|
||||||
|
ReferenceOutputAssembly="false" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ public static class FruitBankConstClient
|
||||||
|
|
||||||
public static string BaseUrl = "https://localhost:59579"; //FrutiBank nop
|
public static string BaseUrl = "https://localhost:59579"; //FrutiBank nop
|
||||||
//public static string BaseUrl = "https://localhost:44372"; //FrutiBank nop
|
//public static string BaseUrl = "https://localhost:44372"; //FrutiBank nop
|
||||||
//public static string BaseUrl = "https://shop.fruitbank.hu"; //FrutiBank nop
|
//public static string BaseUrl = "https://fruitbank.mangoweb.hu/"; //FrutiBank nop
|
||||||
#if RELEASE
|
#if RELEASE
|
||||||
//public static string BaseUrl = "https://shop.fruitbank.hu"; //FrutiBank nop
|
//public static string BaseUrl = "https://shop.fruitbank.hu"; //FrutiBank nop
|
||||||
#endif
|
#endif
|
||||||
|
|
@ -44,7 +44,7 @@ public static class FruitBankConstClient
|
||||||
public const string StockTakingItemDbTableName = "fbStockTakingItem";
|
public const string StockTakingItemDbTableName = "fbStockTakingItem";
|
||||||
public const string StockTakingItemPalletDbTableName = "fbStockTakingItemPallet";
|
public const string StockTakingItemPalletDbTableName = "fbStockTakingItemPallet";
|
||||||
|
|
||||||
|
public const string DomainDescription = "This is a nopCommerce plugin developed for FruitBank, a fruit and vegetable wholesaler. The plugin manages supplier inbound delivery (receiving), warehouse weighing (net/gross/pallet/tare weights), and inventory stocktaking. The business logic is centered around FruitBank's requirement for precise physical measurement and quantity tracking.";
|
||||||
|
|
||||||
//public static Guid[] DevAdminIds = new Guid[2] { Guid.Parse("dcf451d2-cc4c-4ac2-8c1f-da00041be1fd"), Guid.Parse("4cbaed43-2465-4d99-84f1-c8bc6b7025f7") };
|
//public static Guid[] DevAdminIds = new Guid[2] { Guid.Parse("dcf451d2-cc4c-4ac2-8c1f-da00041be1fd"), Guid.Parse("4cbaed43-2465-4d99-84f1-c8bc6b7025f7") };
|
||||||
//public static Guid[] SysAdmins = new Guid[3]
|
//public static Guid[] SysAdmins = new Guid[3]
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
# Helpers
|
||||||
|
|
||||||
|
Measurement aggregation utilities.
|
||||||
|
|
||||||
|
## Key Files
|
||||||
|
|
||||||
|
- **`MeasuringValuesHelper.cs`** — Static helper for rolling up pallet-level measurements to shipping item level.
|
||||||
|
- `SetShippingItemTotalMeasuringValues()` — Sums quantities and weights from all pallets.
|
||||||
|
- `GetTotalNetAndGrossWeightFromPallets()` — Returns (Quantity, NetWeight, GrossWeight) tuple.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
> **LLM Maintenance:** If you modify code in this folder, update this README to reflect the changes. If you notice the README content does not match the current code, automatically update the README to match the code.
|
||||||
|
|
@ -7,6 +7,7 @@ using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using FruitBank.Common.Entities;
|
||||||
|
|
||||||
namespace FruitBank.Common.Interfaces
|
namespace FruitBank.Common.Interfaces
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
# Interfaces
|
||||||
|
|
||||||
|
SignalR endpoint contracts, measurement composition traits, and entity interfaces.
|
||||||
|
|
||||||
|
## SignalR Endpoints
|
||||||
|
|
||||||
|
- **`IFruitBankDataControllerCommon.cs`** / **`Client.cs`** — Core CRUD: Partners, Shipping, ShippingDocuments, ShippingItems, ShippingItemPallets, Products, Customers, GenericAttributes.
|
||||||
|
- **`ICustomOrderSignalREndpointCommon.cs`** / **`Client.cs`** — Order operations: GetAllOrderDtos, GetPendingOrderDtos, OrderItem/Pallet management, StartMeasuring, SetOrderStatusToComplete.
|
||||||
|
- **`IStockSignalREndpointCommon.cs`** / **`Client.cs`** — Inventory: StockTaking, StockTakingItem, StockTakingItemPallet CRUD, CloseStockTaking.
|
||||||
|
|
||||||
|
## Measurement Traits (Composition Pattern)
|
||||||
|
|
||||||
|
- **`IMeasuringValues`** = IMeasuringWeights + IMeasuringQuantity
|
||||||
|
- **`IMeasuringWeights`** = IMeasuringNetWeight + IMeasuringGrossWeight
|
||||||
|
- **`IMeasurable`** — IsMeasurable flag
|
||||||
|
- **`IMeasured`** — IsMeasured flag
|
||||||
|
- **`IMeasurableStatus`** — MeasuringStatus property
|
||||||
|
- **`IMeasuringItemPalletBase`** — Full measurement contract with validation
|
||||||
|
|
||||||
|
## Entity & DTO Interfaces
|
||||||
|
|
||||||
|
- **`IPallet`**, **`IPartner`**, **`IShipping`**, **`IShippingDocument`**, **`IShippingItem`**, **`IShippingItemPallet`**, **`IFiles`**
|
||||||
|
- **`IOrderDto`**, **`IOrderItemDto`**, **`IProductDto`**, **`IStockQuantityHistoryDto`**
|
||||||
|
- **`ITare`**, **`IAvailableQuantity`**, **`IIncomingQuantity`** — Quantity/weight property interfaces
|
||||||
|
|
||||||
|
## Service Interfaces
|
||||||
|
|
||||||
|
- **`IMeasurementServiceBase<TLogger>`** — Base service marker
|
||||||
|
- **`ISecureCredentialService`** — Save/retrieve/clear credentials with 2-day expiration
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
> **LLM Maintenance:** If you modify code in this folder, update this README to reflect the changes. If you notice the README content does not match the current code, automatically update the README to match the code.
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
# Loggers
|
||||||
|
|
||||||
|
SignalR client-to-server log writer.
|
||||||
|
|
||||||
|
## Key Files
|
||||||
|
|
||||||
|
- **`SignaRClientLogItemWriter.cs`** — Routes client logs to `{BaseUrl}/loggerHub` via SignalR. Configurable by AppType and LogLevel.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
> **LLM Maintenance:** If you modify code in this folder, update this README to reflect the changes. If you notice the README content does not match the current code, automatically update the README to match the code.
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
# Models
|
||||||
|
|
||||||
|
Application and view models for UI state management.
|
||||||
|
|
||||||
|
## Key Files
|
||||||
|
|
||||||
|
- **`LoggedInModel.cs`** — Authentication state: IsLoggedIn, IsRevisor, IsAdministrator, IsDeveloper. Auto-login from stored credentials (2-day expiration). Customer and role management.
|
||||||
|
- **`MeasuringAttributeValues.cs`** — IMeasuringAttributeValues implementation: Id, NetWeight, IsMeasurable, HasMeasuringValues().
|
||||||
|
- **`MeasuringModel.cs`** — ViewModel aggregating Shipping + Partners + ShippingItems + ShippingDocuments.
|
||||||
|
|
||||||
|
## Subfolders
|
||||||
|
|
||||||
|
- **`SignalRs/SignalRMessageToClientWithText<T>.cs`** — Generic message wrapper with optional text and typed content.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
> **LLM Maintenance:** If you modify code in this folder, update this README to reflect the changes. If you notice the README content does not match the current code, automatically update the README to match the code.
|
||||||
|
|
@ -0,0 +1,34 @@
|
||||||
|
# FruitBank.Common
|
||||||
|
|
||||||
|
Shared domain library for the FruitBank nopCommerce plugin. Contains entities, DTOs, interfaces, measurement helpers, SignalR tags, and constants for fruit & vegetable wholesale operations.
|
||||||
|
|
||||||
|
## Folder Structure
|
||||||
|
|
||||||
|
| Folder | Purpose |
|
||||||
|
|---|---|
|
||||||
|
| [`Databases/`](Databases/README.md) | Local in-memory database abstraction for offline/cached data |
|
||||||
|
| [`Dtos/`](Dtos/README.md) | Binary-serializable DTOs for Order, OrderItem, Product, StockQuantityHistory |
|
||||||
|
| [`Entities/`](Entities/README.md) | Domain entities: Shipping, Partner, measurement pallets, inventory |
|
||||||
|
| [`Enums/`](Enums/README.md) | MeasuringStatus and DocumentType enums |
|
||||||
|
| [`Helpers/`](Helpers/README.md) | Measurement aggregation utilities |
|
||||||
|
| [`Interfaces/`](Interfaces/README.md) | SignalR endpoint contracts, measurement traits, entity interfaces |
|
||||||
|
| [`Loggers/`](Loggers/README.md) | SignalR client log writer |
|
||||||
|
| [`Models/`](Models/README.md) | Authentication state, measurement view models |
|
||||||
|
| [`Services/`](Services/README.md) | Measurement service base, credential persistence |
|
||||||
|
| [`SignalRs/`](SignalRs/README.md) | SignalR method tags (numeric constants) |
|
||||||
|
|
||||||
|
## Key Files (Root)
|
||||||
|
|
||||||
|
- **`FruitBankConstClient.cs`** — Global constants: BaseUrl, SignalR hubs, database table names, email templates, system settings.
|
||||||
|
- **`DocumentType.cs`** — Enum: ShippingDocument, OrderConfirmation, Invoice.
|
||||||
|
|
||||||
|
## Key Domain Concepts
|
||||||
|
|
||||||
|
- **Shipping = INBOUND** (supplier → warehouse), **Order = OUTBOUND** (warehouse → customer)
|
||||||
|
- **"Pallet" = measurement record**, always created even for non-measurable products
|
||||||
|
- **NetWeight = GrossWeight − PalletWeight − (TrayQuantity × TareWeight)**
|
||||||
|
- See [`docs/GLOSSARY.md`](../docs/GLOSSARY.md) for full terminology
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
> **LLM Maintenance:** If you modify code in this folder, update this README to reflect the changes. If you notice the README content does not match the current code, automatically update the README to match the code.
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
# Services
|
||||||
|
|
||||||
|
Business logic services and credential management.
|
||||||
|
|
||||||
|
## Key Files
|
||||||
|
|
||||||
|
- **`MeasurementServiceBase.cs`** — Abstract base with generic TLogger injection.
|
||||||
|
- **`ISecureCredentialService.cs`** — Interface: SaveCredentialsAsync (2-day expiry), GetCredentialsAsync, ClearCredentialsAsync. StoredCredentials sealed record.
|
||||||
|
|
||||||
|
Platform implementations: MAUI → SecureStorage, Web → obfuscated localStorage, Server → no-op.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
> **LLM Maintenance:** If you modify code in this folder, update this README to reflect the changes. If you notice the README content does not match the current code, automatically update the README to match the code.
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
# SignalRs
|
||||||
|
|
||||||
|
SignalR method identifiers as numeric constants for type-safe client-server communication.
|
||||||
|
|
||||||
|
## Key Files
|
||||||
|
|
||||||
|
- **`SignalRTags.cs`** — Constant int tags organized by domain:
|
||||||
|
- **0-10:** System (GetMeasuringModels)
|
||||||
|
- **20-27:** Partner CRUD
|
||||||
|
- **40-66:** Shipping, ShippingDocument, ShippingItem
|
||||||
|
- **70-83:** Customer, Product
|
||||||
|
- **94-98:** ShippingItemPallet
|
||||||
|
- **111-138:** Order (OrderDto, OrderItemDto, OrderItemPallet)
|
||||||
|
- **150-151:** StockQuantityHistory
|
||||||
|
- **160-169:** GenericAttribute
|
||||||
|
- **170-179:** StockTaking
|
||||||
|
- **195-200:** Authentication
|
||||||
|
- **500+:** Server→client notifications (SendOrderChanged, SendShippingChanged, etc.)
|
||||||
|
- **1000+:** Diagnostic/Logging
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
> **LLM Maintenance:** If you modify code in this folder, update this README to reflect the changes. If you notice the README content does not match the current code, automatically update the README to match the code.
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
# FruitBankHybrid.Shared.Common
|
||||||
|
|
||||||
|
Shared common library. Currently a placeholder — no source files yet. .NET 10.0 with AOT enabled.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
> **LLM Maintenance:** If you modify code in this folder, update this README to reflect the changes. If you notice the README content does not match the current code, automatically update the README to match the code.
|
||||||
|
|
@ -15,20 +15,18 @@
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="bunit" Version="2.2.2" />
|
<PackageReference Include="bunit" Version="2.4.2" />
|
||||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Core\AyCode.Core.csproj" />
|
||||||
<ProjectReference Include="..\FruitBank.Common\FruitBank.Common.csproj" />
|
<ProjectReference Include="..\FruitBank.Common\FruitBank.Common.csproj" />
|
||||||
<ProjectReference Include="..\FruitBankHybrid.Shared.Common\FruitBankHybrid.Shared.Common.csproj" />
|
<ProjectReference Include="..\FruitBankHybrid.Shared.Common\FruitBankHybrid.Shared.Common.csproj" />
|
||||||
<ProjectReference Include="..\FruitBankHybrid.Shared\FruitBankHybrid.Shared.csproj" />
|
<ProjectReference Include="..\FruitBankHybrid.Shared\FruitBankHybrid.Shared.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Reference Include="AyCode.Core">
|
|
||||||
<HintPath>..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Services.Server\bin\FruitBank\Debug\net9.0\AyCode.Core.dll</HintPath>
|
|
||||||
</Reference>
|
|
||||||
<Reference Include="AyCode.Entities">
|
<Reference Include="AyCode.Entities">
|
||||||
<HintPath>..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Services.Server\bin\FruitBank\Debug\net9.0\AyCode.Entities.dll</HintPath>
|
<HintPath>..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Services.Server\bin\FruitBank\Debug\net9.0\AyCode.Entities.dll</HintPath>
|
||||||
</Reference>
|
</Reference>
|
||||||
|
|
|
||||||
|
|
@ -3,14 +3,16 @@ using AyCode.Core.Extensions;
|
||||||
using AyCode.Core.Loggers;
|
using AyCode.Core.Loggers;
|
||||||
using FruitBank.Common;
|
using FruitBank.Common;
|
||||||
using FruitBank.Common.Dtos;
|
using FruitBank.Common.Dtos;
|
||||||
|
using FruitBank.Common.Entities;
|
||||||
using FruitBank.Common.Loggers;
|
using FruitBank.Common.Loggers;
|
||||||
using FruitBankHybrid.Shared.Services.SignalRs;
|
using FruitBankHybrid.Shared.Services.SignalRs;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
|
using Nop.Core.Domain.Common;
|
||||||
using Nop.Core.Domain.Orders;
|
using Nop.Core.Domain.Orders;
|
||||||
using Nop.Core.Domain.Payments;
|
using Nop.Core.Domain.Payments;
|
||||||
|
using System.Linq.Expressions;
|
||||||
using System.Runtime.Serialization;
|
using System.Runtime.Serialization;
|
||||||
using FruitBank.Common.Entities;
|
using AyCode.Core.Serializers.Toons;
|
||||||
using Nop.Core.Domain.Common;
|
|
||||||
|
|
||||||
namespace FruitBankHybrid.Shared.Tests;
|
namespace FruitBankHybrid.Shared.Tests;
|
||||||
|
|
||||||
|
|
@ -77,6 +79,18 @@ public sealed class OrderClientTests
|
||||||
Assert.IsTrue(orderDtos.All(o => o.OrderItemDtos.All(oi => oi.ProductDto != null && oi.ProductDto.Id == oi.ProductId)));
|
Assert.IsTrue(orderDtos.All(o => o.OrderItemDtos.All(oi => oi.ProductDto != null && oi.ProductDto.Id == oi.ProductId)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public async Task GetAllOrderDtosByFilter()
|
||||||
|
{
|
||||||
|
//Queryable? filter = dto => dto.Id == 15;
|
||||||
|
var orderDtos = await _signalRClient.GetAllOrderDtos();
|
||||||
|
|
||||||
|
Assert.IsNotNull(orderDtos);
|
||||||
|
Assert.IsTrue(orderDtos.Count != 0);
|
||||||
|
|
||||||
|
Assert.IsTrue(orderDtos.All(o => o.OrderItemDtos.All(oi => oi.ProductDto != null && oi.ProductDto.Id == oi.ProductId)));
|
||||||
|
}
|
||||||
|
|
||||||
[TestMethod]
|
[TestMethod]
|
||||||
[DataRow(1)]
|
[DataRow(1)]
|
||||||
[DataRow(2)]
|
[DataRow(2)]
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
# FruitBankHybrid.Shared.Tests
|
||||||
|
|
||||||
|
MSTest integration and serialization tests. Covers SignalR client operations, JSON reference handling, binary serialization, Toon format, and bunit component rendering.
|
||||||
|
|
||||||
|
## Folder Structure
|
||||||
|
|
||||||
|
| Folder | Purpose |
|
||||||
|
|---|---|
|
||||||
|
| [`TestData/`](TestData/README.md) | Test models for Toon serialization |
|
||||||
|
|
||||||
|
## Key Files
|
||||||
|
|
||||||
|
- **`MSTestSettings.cs`** — Parallel test execution at MethodLevel.
|
||||||
|
- **`FruitBankClientTests.cs`** — (~667 lines) Full SignalR integration: Partner, Shipping, ShippingItem, ShippingDocument, Customer, Product, Order, Login tests. Localhost-only safety check.
|
||||||
|
- **`OrderClientTests.cs`** — Order and StockTaking retrieval/manipulation tests.
|
||||||
|
- **`JsonExtensionTests.cs`** — (~715 lines) JSON $id/$ref reference handling, 5-level hierarchies, circular references, DeepPopulateWithMerge.
|
||||||
|
- **`StockTakingSerializerTests.cs`** — Binary serialization round-trips, null collection handling, binary format analysis.
|
||||||
|
- **`ToonTests.cs`** — (~465 lines) Toon format: metadata generation, reference markers, type uniqueness, navigation metadata, property descriptions.
|
||||||
|
- **`SandboxEndpointSimpleTests.cs`** — Endpoint connectivity and SignalR negotiate tests.
|
||||||
|
- **`GridPartnerBaseTests.cs`** — Grid component tests (disabled).
|
||||||
|
- **`GridPartnerRazorTests.cs`** — bunit Blazor rendering tests (disabled).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
> **LLM Maintenance:** If you modify code in this folder, update this README to reflect the changes. If you notice the README content does not match the current code, automatically update the README to match the code.
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
# TestData
|
||||||
|
|
||||||
|
Demo models for Toon serialization testing.
|
||||||
|
|
||||||
|
## Key Files
|
||||||
|
|
||||||
|
- **`ToonTestData.cs`** — TestOrder, TestCustomer, TestOrderItem, TestProduct models with one-to-many relationships and back-references for testing complex object graph serialization.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
> **LLM Maintenance:** If you modify code in this folder, update this README to reflect the changes. If you notice the README content does not match the current code, automatically update the README to match the code.
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
namespace FruitBankHybrid.Shared.Tests.TestData;
|
||||||
|
|
||||||
|
// Demo entity-k a teszteléshez
|
||||||
|
public class TestOrder
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public int CustomerId { get; set; }
|
||||||
|
public TestCustomer? Customer { get; set; }
|
||||||
|
public List<TestOrderItem> OrderItems { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
public class TestCustomer
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public string Name { get; set; } = "";
|
||||||
|
public List<TestOrder> Orders { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
public class TestOrderItem
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public int OrderId { get; set; }
|
||||||
|
public TestOrder? Order { get; set; }
|
||||||
|
public int ProductId { get; set; }
|
||||||
|
public TestProduct? Product { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class TestProduct
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public string Name { get; set; } = "";
|
||||||
|
public List<TestOrderItem> OrderItems { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,465 @@
|
||||||
|
using AyCode.Core.Enums;
|
||||||
|
using AyCode.Core.Extensions;
|
||||||
|
using AyCode.Core.Loggers;
|
||||||
|
using AyCode.Core.Serializers.Binaries;
|
||||||
|
using AyCode.Core.Serializers.Jsons;
|
||||||
|
using AyCode.Core.Serializers.Toons;
|
||||||
|
using FruitBank.Common;
|
||||||
|
using FruitBank.Common.Dtos;
|
||||||
|
using FruitBank.Common.Entities;
|
||||||
|
using FruitBank.Common.Loggers;
|
||||||
|
using FruitBankHybrid.Shared.Services.SignalRs;
|
||||||
|
using Mango.Nop.Core.Entities;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using Nop.Core.Domain.Catalog;
|
||||||
|
using Nop.Core.Domain.Common;
|
||||||
|
using Nop.Core.Domain.Orders;
|
||||||
|
using Nop.Core.Domain.Payments;
|
||||||
|
using System.Linq.Expressions;
|
||||||
|
using System.Runtime.InteropServices.JavaScript;
|
||||||
|
using System.Runtime.Serialization;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace FruitBankHybrid.Shared.Tests;
|
||||||
|
|
||||||
|
//1. "Headered List" (A biztonságos táblázatosítás)
|
||||||
|
|
||||||
|
//Az LLM-eknek nem kell minden sorban megismételni a mezőneveket, ha a lista elején egyszer definiálod a sorrendet. Ez nem találgatás, hanem egy lokális "szerződés".
|
||||||
|
|
||||||
|
//Hagyományos (pazarló):
|
||||||
|
//Kódrészlet
|
||||||
|
|
||||||
|
//OrderItemDtos = [
|
||||||
|
// OrderItemDto { Id = 120, Quantity = 10, ProductName = "Áfonya" }
|
||||||
|
// OrderItemDto { Id = 121, Quantity = 5, ProductName = "Narancs" }
|
||||||
|
//]
|
||||||
|
|
||||||
|
//Optimalizált (pontos és tömör):
|
||||||
|
//Kódrészlet
|
||||||
|
|
||||||
|
//OrderItemDtos: OrderItemDto[] = [
|
||||||
|
// [ Id, Quantity, ProductName ]
|
||||||
|
// [ 120, 10, "Áfonya" ]
|
||||||
|
// [ 121, 5, "Narancs" ]
|
||||||
|
//]
|
||||||
|
|
||||||
|
// Miért jó ez? Az LLM a fejléc alapján (mint egy CSV-nél) rendeli hozzá az értékeket a típushoz. Mivel a típus (OrderItemDto) ott van a definícióban, a szemantikai kapcsolat nem vész el.
|
||||||
|
|
||||||
|
//2. Típus-öröklődés a listákban
|
||||||
|
|
||||||
|
//Ha a @types részben már leírtad, hogy az OrderItemDto.ProductDto mezője egy ProductDto típust vár, akkor a @data részben felesleges kiírni a típusnevet minden egyes elemnél.
|
||||||
|
|
||||||
|
//Példa:
|
||||||
|
//Kódrészlet
|
||||||
|
|
||||||
|
//// A 'ProductDto' elhagyható az objektum elől, mert a sémából tudja
|
||||||
|
//ProductDto = {
|
||||||
|
// Id = 1
|
||||||
|
// Name = "Áfonya..."
|
||||||
|
// GenericAttributes = [
|
||||||
|
// { Id = 99, Key = "NetWeight", Value = "178.3" }
|
||||||
|
// { Id = 100, Key = "GrossWeight", Value = "19" }
|
||||||
|
// ]
|
||||||
|
//}
|
||||||
|
|
||||||
|
//3. Alapértelmezett értékek elhagyása (Implicit Defaults)
|
||||||
|
|
||||||
|
//Ha egy mező értéke megegyezik a @types-ban definiált default-value-val, vagy null/0/false, akkor azt teljesen hagyd ki a @data részből.
|
||||||
|
|
||||||
|
// Szabály: Ami nincs ott, az az alapértelmezett.
|
||||||
|
|
||||||
|
// Token megtakarítás: A FruitBank példádban a GenericAttributes = <GenericAttributeDto[]> (count: 0) [] sorok rengeteg helyet foglalnak. Ha üres, egyszerűen ne küldd el a mezőt.
|
||||||
|
|
||||||
|
//4. String Table helyett: "Object Anchoring"
|
||||||
|
|
||||||
|
//használd az objektum-referenciákat (amit a @ProductDto:1 jelöléssel már el is kezdett a rendszered).
|
||||||
|
|
||||||
|
//Ha ugyanaz a Product szerepel 5 különböző rendelési tételnél, ne írd le ötször.
|
||||||
|
|
||||||
|
// Első alkalommal: ProductDto { ... }
|
||||||
|
|
||||||
|
// Minden további alkalommal: ProductDto = @ProductDto:1
|
||||||
|
|
||||||
|
//[ToonIgnore][ToonDataIgnore]
|
||||||
|
[ToonDescription(Purpose = "Container model for Shipping, Order")]
|
||||||
|
public class FullProcessModel
|
||||||
|
{
|
||||||
|
public List<Shipping> Shippings { get; set; }
|
||||||
|
public List<OrderDto> Orders { get; set; }
|
||||||
|
public List<StockTaking> StockTakings { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestClass]
|
||||||
|
public sealed class ToonTests
|
||||||
|
{
|
||||||
|
private const int CustomerIdAasdDsserverCom = 6; //aasd@dsserver.com
|
||||||
|
|
||||||
|
private FruitBankSignalRClient _signalRClient = null!;
|
||||||
|
|
||||||
|
[TestInitialize]
|
||||||
|
public void TestInit()
|
||||||
|
{
|
||||||
|
if (!FruitBankConstClient.BaseUrl.Contains("localhost:")) throw new Exception("NEM LOCALHOST-ON TESZTELÜNK!");
|
||||||
|
|
||||||
|
_signalRClient = new FruitBankSignalRClient(new List<IAcLogWriterClientBase>
|
||||||
|
{
|
||||||
|
//new ConsoleLogWriter(AppType.TestUnit, LogLevel.Detail, nameof(FruitBankClientTests)),
|
||||||
|
new SignaRClientLogItemWriter(AppType.TestUnit, LogLevel.Detail, nameof(FruitBankClientTests))
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
|
[TestMethod]
|
||||||
|
public async Task GetAnalyzeStringInternCandidatesLog()
|
||||||
|
{
|
||||||
|
var orders = (await _signalRClient.GetAllOrderDtos())!.Where(x=>x.CreatedOnUtc > DateTime.UtcNow.AddDays(-70)).ToList();
|
||||||
|
|
||||||
|
var options = AcBinarySerializerOptions.WithoutReferenceHandling;
|
||||||
|
//options.SetReferenceHandlingUnsafe(ReferenceHandlingMode.OnlyId);
|
||||||
|
var analysisLog = AcBinarySerializer.GetAnalyzeStringInternCandidatesLog(orders, options);
|
||||||
|
|
||||||
|
Assert.IsNotNull(analysisLog);
|
||||||
|
Assert.IsGreaterThan(0, analysisLog.Length);
|
||||||
|
|
||||||
|
// Print results sorted by occurrence count
|
||||||
|
Console.WriteLine(analysisLog.ToString());
|
||||||
|
Console.WriteLine();
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public async Task OrderDtoToToon()
|
||||||
|
{
|
||||||
|
var a = new FullProcessModel();
|
||||||
|
a.Orders = (await _signalRClient.GetAllOrderDtos())!.Where(x=>x.CreatedOnUtc > DateTime.UtcNow.AddDays(-70)).ToList();
|
||||||
|
a.Shippings = (await _signalRClient.GetShippings())!.Where(x=>x.Created > DateTime.UtcNow.AddDays(-70)).ToList();
|
||||||
|
|
||||||
|
//var toon = AcToonSerializer.Serialize(a, FruitBankConstClient.DomainDescription, AcToonSerializerOptions.Default);
|
||||||
|
var toon = AcToonSerializer.SerializeTypeMetadata<FullProcessModel>(FruitBankConstClient.DomainDescription);
|
||||||
|
|
||||||
|
Console.WriteLine(toon);
|
||||||
|
Assert.IsNotEmpty(toon);
|
||||||
|
// Note: @ref: only appears when the same object instance is referenced multiple times.
|
||||||
|
// Data from separate API calls typically don't share object instances.
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void GetMetaInfos()
|
||||||
|
{
|
||||||
|
var a = new FullProcessModel();
|
||||||
|
var toon = AcToonSerializer.SerializeTypeMetadata<FullProcessModel>(FruitBankConstClient.DomainDescription);
|
||||||
|
Console.WriteLine(toon);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void ReferenceHandling_WithSharedReferences_ShouldOutputRefMarkers()
|
||||||
|
{
|
||||||
|
// Create a simple test container with shared references
|
||||||
|
var sharedProduct = new ProductDto { Id = 1, Name = "Shared Product" };
|
||||||
|
|
||||||
|
// Create a container that references the same ProductDto twice
|
||||||
|
var container = new TestContainerWithSharedRefs
|
||||||
|
{
|
||||||
|
Product1 = sharedProduct,
|
||||||
|
Product2 = sharedProduct // Same instance, should create @ref
|
||||||
|
};
|
||||||
|
|
||||||
|
var toon = AcToonSerializer.Serialize(container, FruitBankConstClient.DomainDescription, AcToonSerializerOptions.Default);
|
||||||
|
|
||||||
|
Console.WriteLine(toon);
|
||||||
|
Assert.IsNotEmpty(toon);
|
||||||
|
Assert.IsTrue(toon.Contains("@ref:"), "ReferenceHandling should detect shared Product instance");
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void ReferenceHandling_WithSharedIIdReferences_ShouldOutputRefMarkers()
|
||||||
|
{
|
||||||
|
// Create a simple test container with shared references
|
||||||
|
var sharedProduct = new ProductDto { Id = 1, Name = "Shared Product" };
|
||||||
|
var sharedProduct2 = new ProductDto { Id = 1, Name = "Shared Product" };
|
||||||
|
|
||||||
|
// Create a container that references the same ProductDto twice
|
||||||
|
var container = new TestContainerWithSharedRefs
|
||||||
|
{
|
||||||
|
Product1 = sharedProduct,
|
||||||
|
Product2 = sharedProduct2 // Same instance, should create @ref
|
||||||
|
};
|
||||||
|
|
||||||
|
var toon = AcToonSerializer.Serialize(container, FruitBankConstClient.DomainDescription, AcToonSerializerOptions.Default);
|
||||||
|
|
||||||
|
Console.WriteLine(toon);
|
||||||
|
Assert.IsNotEmpty(toon);
|
||||||
|
Assert.IsTrue(toon.Contains("@ref:"), "ReferenceHandling should detect shared Product instance");
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void ToonTypes_ShouldNotContainList1OrGenericTypeNames()
|
||||||
|
{
|
||||||
|
var toon = AcToonSerializer.SerializeTypeMetadata<OrderDto>();
|
||||||
|
StringAssert.DoesNotMatch(toon, new System.Text.RegularExpressions.Regex(@"List`?1"), "A @meta.types vagy @types szekcióban nem szerepelhet List`1 vagy generikus típusnév.");
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void ToonTypes_ShouldNotContainDuplicateTypeNames()
|
||||||
|
{
|
||||||
|
var toon = AcToonSerializer.SerializeTypeMetadata<OrderDto>();
|
||||||
|
var typesLine = toon.Split('\n').FirstOrDefault(x => x.TrimStart().StartsWith("types = ["));
|
||||||
|
Assert.IsNotNull(typesLine, "Nem található types lista a @meta szekcióban.");
|
||||||
|
var typeNames = typesLine.Substring(typesLine.IndexOf('[') + 1, typesLine.LastIndexOf(']') - typesLine.IndexOf('[') - 1)
|
||||||
|
.Split(',').Select(x => x.Trim(' ', '"')).Where(x => !string.IsNullOrWhiteSpace(x)).ToList();
|
||||||
|
var duplicates = typeNames.GroupBy(x => x).Where(g => g.Count() > 1).Select(g => g.Key).ToList();
|
||||||
|
Assert.IsTrue(duplicates.Count == 0, $"A types listában duplikált típusnév található: {string.Join(", ", duplicates)}");
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void ToonTypes_EachTypeShouldBeDefinedOnceInTypesSection()
|
||||||
|
{
|
||||||
|
var toon = AcToonSerializer.SerializeTypeMetadata<OrderDto>();
|
||||||
|
var typeDefLines = toon.Split('\n').Where(x => x.TrimEnd().EndsWith(": \"Object of type") || x.TrimEnd().EndsWith(": enum") || x.TrimEnd().EndsWith(": \"Object of type "));
|
||||||
|
var typeNames = typeDefLines.Select(x => x.Trim().Split(':')[0]).ToList();
|
||||||
|
var duplicates = typeNames.GroupBy(x => x).Where(g => g.Count() > 1).Select(g => g.Key).ToList();
|
||||||
|
Assert.IsTrue(duplicates.Count == 0, $"A @types szekcióban duplikált típusdefiníció található: {string.Join(", ", duplicates)}");
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void ToonTypes_PropertyTypesShouldNotReferenceList1()
|
||||||
|
{
|
||||||
|
var toon = AcToonSerializer.SerializeTypeMetadata<OrderDto>();
|
||||||
|
var lines = toon.Split('\n');
|
||||||
|
foreach (var line in lines)
|
||||||
|
{
|
||||||
|
if (line.Trim().EndsWith(": List`1"))
|
||||||
|
{
|
||||||
|
Assert.Fail($"Property List`1 típusra hivatkozik: {line.Trim()}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void ToonTypes_PropertyDescriptions_ShouldNotBeRedundantOrMisleading()
|
||||||
|
{
|
||||||
|
var toon = AcToonSerializer.SerializeTypeMetadata<OrderDto>(FruitBankConstClient.DomainDescription);
|
||||||
|
var lines = toon.Split('\n');
|
||||||
|
foreach (var line in lines)
|
||||||
|
{
|
||||||
|
if (line.Trim().StartsWith("description:") && line.Contains("Collection of Object for"))
|
||||||
|
{
|
||||||
|
Assert.Fail($"Redundáns vagy félrevezető description: {line.Trim()}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void ToonTypes_NavigationMetadata_ShouldBeComplete()
|
||||||
|
{
|
||||||
|
var toon = AcToonSerializer.SerializeMetadata(FruitBankConstClient.DomainDescription, [typeof(Shipping), typeof(OrderDto), typeof(StockTaking), typeof(StockQuantityHistory), typeof(StockQuantityHistoryExt)]);
|
||||||
|
Console.WriteLine(toon);
|
||||||
|
Console.WriteLine("\n=== NAVIGATION METADATA ELLENŐRZÉS ===\n");
|
||||||
|
|
||||||
|
var lines = toon.Split('\n').Select(x => x.TrimEnd()).ToList();
|
||||||
|
|
||||||
|
// Ismerten egyirányú kapcsolatok - ezeknél nincs inverse property a másik oldalon
|
||||||
|
// Customer: NopCommerce domain entity, nincs benne Orders kollekció
|
||||||
|
// OrderNotes: OrderNote osztályban nincs Order navigation property
|
||||||
|
// ProductDto: nincs benne OrderItems kollekció
|
||||||
|
// GenericAttributes: polimorf kapcsolat ExpressionPredicate-tel, nincs inverse ÉS nincs egyértelmű FK
|
||||||
|
// FONTOS: Csak az inverse-property hiányát engedjük! Az other-key-nek léteznie kell!
|
||||||
|
var knownUnidirectionalNavigations = new HashSet<string>
|
||||||
|
{
|
||||||
|
"Customer",
|
||||||
|
"OrderNotes",
|
||||||
|
"ProductDto",
|
||||||
|
"GenericAttributes",
|
||||||
|
"ShippingDocumentFile",
|
||||||
|
"Pallet"
|
||||||
|
};
|
||||||
|
|
||||||
|
// GenericAttributes speciális eset - polimorf, nincs other-key sem
|
||||||
|
var knownPolymorphicNavigations = new HashSet<string>
|
||||||
|
{
|
||||||
|
"GenericAttributes"
|
||||||
|
};
|
||||||
|
|
||||||
|
// FK property-k NEM tartalmazhatnak foreign-key attribútumot
|
||||||
|
for (int i = 0; i < lines.Count; i++)
|
||||||
|
{
|
||||||
|
var line = lines[i].Trim();
|
||||||
|
if (line.EndsWith("Id: int") || line.EndsWith("Id: int?"))
|
||||||
|
{
|
||||||
|
int j = i + 1;
|
||||||
|
while (j < lines.Count)
|
||||||
|
{
|
||||||
|
if (lines[j].StartsWith(" ") && !lines[j].StartsWith(" "))
|
||||||
|
break;
|
||||||
|
|
||||||
|
if (lines[j].StartsWith(" "))
|
||||||
|
{
|
||||||
|
var metaLine = lines[j].Trim();
|
||||||
|
if (metaLine.StartsWith("foreign-key:"))
|
||||||
|
{
|
||||||
|
Assert.Fail($"FK property nem tartalmazhat foreign-key attribútumot: {line} -> {metaLine}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
j++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.WriteLine("✓ FK property-k nem tartalmaznak foreign-key attribútumot\n");
|
||||||
|
|
||||||
|
// Számoljuk meg a hiányzó navigation metadatokat
|
||||||
|
var missingMetadata = new List<string>();
|
||||||
|
var skippedUnidirectional = new List<string>();
|
||||||
|
|
||||||
|
for (int i = 0; i < lines.Count; i++)
|
||||||
|
{
|
||||||
|
var line = lines[i].Trim();
|
||||||
|
|
||||||
|
// Navigation property-k keresése
|
||||||
|
if (line.Contains(": ") &&
|
||||||
|
!line.Contains(": int") && !line.Contains(": string") &&
|
||||||
|
!line.Contains(": DateTime") && !line.Contains(": decimal") &&
|
||||||
|
!line.Contains(": bool") && !line.Contains(": Guid") &&
|
||||||
|
!line.Contains(": double") && !line.Contains(": float") &&
|
||||||
|
!line.Contains("description:") && !line.Contains("purpose:") &&
|
||||||
|
!line.Contains("navigation:") && !line.Contains("foreign-key:") &&
|
||||||
|
!line.Contains("table-name:") && !line.Contains("constraints:") &&
|
||||||
|
!line.Contains("inverse-property:") && !line.Contains("other-key:") &&
|
||||||
|
!line.Contains("primary-key:") && !line.Contains("examples:") &&
|
||||||
|
!line.Contains("@meta") && !line.Contains("@types") &&
|
||||||
|
!line.Contains("version") && !line.Contains("format") && !line.Contains("source-code-language") &&
|
||||||
|
!line.Contains("underlying-type:") && !line.Contains("default-value:") && !line.Contains("values:"))
|
||||||
|
{
|
||||||
|
var propName = line.Split(':')[0].Trim();
|
||||||
|
if (string.IsNullOrEmpty(propName) || propName == "types") continue;
|
||||||
|
|
||||||
|
// Következő sorok metadatainak összegyűjtése
|
||||||
|
var metadata = new HashSet<string>();
|
||||||
|
int j = i + 1;
|
||||||
|
while (j < lines.Count && lines[j].StartsWith(" ") && lines[j].Trim().Contains(':'))
|
||||||
|
{
|
||||||
|
var metaLine = lines[j].Trim();
|
||||||
|
if (metaLine.StartsWith("navigation:")) metadata.Add("navigation");
|
||||||
|
if (metaLine.StartsWith("foreign-key:")) metadata.Add("foreign-key");
|
||||||
|
if (metaLine.StartsWith("inverse-property:")) metadata.Add("inverse-property");
|
||||||
|
if (metaLine.StartsWith("other-key:")) metadata.Add("other-key");
|
||||||
|
j++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ha van navigation attribútum, ellenőrizzük a szükséges metadatokat
|
||||||
|
if (metadata.Contains("navigation"))
|
||||||
|
{
|
||||||
|
var navLine = lines.Skip(i + 1).FirstOrDefault(x => x.Trim().StartsWith("navigation:"));
|
||||||
|
if (navLine != null)
|
||||||
|
{
|
||||||
|
var isUnidirectional = knownUnidirectionalNavigations.Contains(propName);
|
||||||
|
var isPolymorphic = knownPolymorphicNavigations.Contains(propName);
|
||||||
|
|
||||||
|
if (navLine.Contains("many-to-one"))
|
||||||
|
{
|
||||||
|
if (!metadata.Contains("foreign-key"))
|
||||||
|
missingMetadata.Add($"{propName} (ManyToOne): hiányzik foreign-key");
|
||||||
|
|
||||||
|
if (!metadata.Contains("inverse-property"))
|
||||||
|
{
|
||||||
|
if (isUnidirectional)
|
||||||
|
skippedUnidirectional.Add($"{propName} (ManyToOne): egyirányú kapcsolat, nincs inverse");
|
||||||
|
else
|
||||||
|
missingMetadata.Add($"{propName} (ManyToOne): hiányzik inverse-property");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (navLine.Contains("one-to-many"))
|
||||||
|
{
|
||||||
|
// other-key: polimorf kapcsolatoknál nem kötelező
|
||||||
|
if (!metadata.Contains("other-key"))
|
||||||
|
{
|
||||||
|
if (isPolymorphic)
|
||||||
|
{
|
||||||
|
skippedUnidirectional.Add($"{propName} (OneToMany): polimorf kapcsolat, nincs other-key");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// DEBUG: részletes info
|
||||||
|
Console.WriteLine($"\n[DEBUG] {propName} (OneToMany) - other-key hiányzik!");
|
||||||
|
|
||||||
|
// Keressük meg a property típusát a Toon outputban
|
||||||
|
var propTypePart = line.Split(':').LastOrDefault()?.Trim() ?? "";
|
||||||
|
Console.WriteLine($" Property type: {propTypePart}");
|
||||||
|
|
||||||
|
// Ha List<X> formátum, keressük meg X-et
|
||||||
|
if (propTypePart.StartsWith("List<") && propTypePart.EndsWith(">"))
|
||||||
|
{
|
||||||
|
var elementTypeName = propTypePart.Substring(5, propTypePart.Length - 6);
|
||||||
|
Console.WriteLine($" Element type: {elementTypeName}");
|
||||||
|
|
||||||
|
// Keressük meg az element type definícióját
|
||||||
|
var elementTypeDefIndex = lines.FindIndex(l => l.Trim().StartsWith($"{elementTypeName}:"));
|
||||||
|
if (elementTypeDefIndex >= 0)
|
||||||
|
{
|
||||||
|
Console.WriteLine($" Element type definition found at line {elementTypeDefIndex}");
|
||||||
|
// Listázzuk ki az element type property-jeit amik "Id"-re végződnek
|
||||||
|
for (int k = elementTypeDefIndex + 1; k < lines.Count; k++)
|
||||||
|
{
|
||||||
|
var propLine = lines[k];
|
||||||
|
if (!propLine.StartsWith(" ")) break; // Új típus definíció
|
||||||
|
if (propLine.StartsWith(" ")) continue; // Metaadat, skip
|
||||||
|
|
||||||
|
var trimmed = propLine.Trim();
|
||||||
|
if (trimmed.EndsWith(": int") && trimmed.Contains("Id"))
|
||||||
|
{
|
||||||
|
Console.WriteLine($" FK candidate: {trimmed}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
missingMetadata.Add($"{propName} (OneToMany): hiányzik other-key");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!metadata.Contains("inverse-property"))
|
||||||
|
{
|
||||||
|
if (isUnidirectional)
|
||||||
|
skippedUnidirectional.Add($"{propName} (OneToMany): egyirányú kapcsolat, nincs inverse");
|
||||||
|
else
|
||||||
|
missingMetadata.Add($"{propName} (OneToMany): hiányzik inverse-property");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (skippedUnidirectional.Count > 0)
|
||||||
|
{
|
||||||
|
Console.WriteLine("EGYIRÁNYÚ/POLIMORF KAPCSOLATOK (nem hiba):");
|
||||||
|
foreach (var skipped in skippedUnidirectional)
|
||||||
|
{
|
||||||
|
Console.WriteLine($" ℹ {skipped}");
|
||||||
|
}
|
||||||
|
Console.WriteLine();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (missingMetadata.Count > 0)
|
||||||
|
{
|
||||||
|
Console.WriteLine("HIÁNYZÓ METAADATOK:");
|
||||||
|
foreach (var missing in missingMetadata)
|
||||||
|
{
|
||||||
|
Console.WriteLine($" - {missing}");
|
||||||
|
}
|
||||||
|
Assert.Fail($"Hiányzó navigation metaadatok: {missingMetadata.Count} db");
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.WriteLine("✓ Minden navigation property tartalmazza a szükséges metadatokat");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Test helper class to verify reference handling with shared object instances.
|
||||||
|
/// </summary>
|
||||||
|
public class TestContainerWithSharedRefs
|
||||||
|
{
|
||||||
|
public ProductDto? Product1 { get; set; }
|
||||||
|
public ProductDto? Product2 { get; set; }
|
||||||
|
}
|
||||||
|
|
@ -1,17 +0,0 @@
|
||||||
# Test debugger script for JsonExtensionTests
|
|
||||||
$projectPath = "H:\Applications\Mango\Source\FruitBankHybridApp"
|
|
||||||
Set-Location $projectPath
|
|
||||||
|
|
||||||
Write-Host "Building test project..."
|
|
||||||
dotnet build FruitBankHybrid.Shared.Tests/FruitBankHybrid.Shared.Tests.csproj -c Debug
|
|
||||||
|
|
||||||
Write-Host "`nRunning JsonExtensionTests..."
|
|
||||||
# Use --no-build to avoid the MSBuild conflict
|
|
||||||
dotnet test FruitBankHybrid.Shared.Tests/FruitBankHybrid.Shared.Tests.csproj `
|
|
||||||
--no-build `
|
|
||||||
-c Debug `
|
|
||||||
--filter "ClassName=FruitBankHybrid.Shared.Tests.JsonExtensionTests" `
|
|
||||||
2>&1 | Tee-Object -FilePath "test_results.txt"
|
|
||||||
|
|
||||||
Write-Host "`n=== Test Results ==="
|
|
||||||
Get-Content "test_results.txt" | Select-String -Pattern "FAILED|PASSED|Error|Assert" | tail -50
|
|
||||||
|
|
@ -7,6 +7,7 @@ using FruitBank.Common.Interfaces;
|
||||||
using FruitBank.Common.SignalRs;
|
using FruitBank.Common.SignalRs;
|
||||||
using FruitBankHybrid.Shared.Pages;
|
using FruitBankHybrid.Shared.Pages;
|
||||||
using Microsoft.AspNetCore.Components;
|
using Microsoft.AspNetCore.Components;
|
||||||
|
using Nop.Core.Domain.Catalog;
|
||||||
using Nop.Core.Domain.Common;
|
using Nop.Core.Domain.Common;
|
||||||
using Nop.Core.Domain.Orders;
|
using Nop.Core.Domain.Orders;
|
||||||
|
|
||||||
|
|
@ -51,7 +52,7 @@ public class GridGenericAttributeBase: FruitBankGridBase<GenericAttributeDto>, I
|
||||||
switch (ParentDataItem)
|
switch (ParentDataItem)
|
||||||
{
|
{
|
||||||
case IProductDto:
|
case IProductDto:
|
||||||
if (!hasContextIdParameter) ContextIds![ContextKeyGroupIndex] = "Product";
|
if (!hasContextIdParameter) ContextIds![ContextKeyGroupIndex] = nameof(Product);
|
||||||
|
|
||||||
break;
|
break;
|
||||||
case IOrderDto:
|
case IOrderDto:
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
# Grids
|
||||||
|
|
||||||
|
Domain-specific DevExpress grid components, one per entity type. All extend MgGridBase for layout persistence.
|
||||||
|
|
||||||
|
## Subfolders
|
||||||
|
|
||||||
|
| Folder | Entity |
|
||||||
|
|---|---|
|
||||||
|
| `GenericAttributes/` | GridGenericAttributeBase — context-based attribute grids |
|
||||||
|
| `OrderItems/` | GridOrderItem — order item grid (partial) |
|
||||||
|
| `Partners/` | GridPartnerBase — partner grid with SignalR |
|
||||||
|
| `Products/` | GridStockQuantityHistoryDtoBase — stock history visualization |
|
||||||
|
| `ShippingDocuments/` | GridShippingDocumentBase — shipping document management |
|
||||||
|
| `ShippingItems/` | GridShippingItemBase — shipping item detail with context awareness |
|
||||||
|
| `Shippings/` | GridShippingBase — master shipping grid |
|
||||||
|
| `StockTakingItems/` | GridStockTakingItemBase — stock taking item grid |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
> **LLM Maintenance:** If you modify code in this folder, update this README to reflect the changes. If you notice the README content does not match the current code, automatically update the README to match the code.
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
# Components
|
||||||
|
|
||||||
|
DevExpress Blazor grid wrappers, pallet measurement components, and toast notifications.
|
||||||
|
|
||||||
|
## Key Files (Root)
|
||||||
|
|
||||||
|
- **`MgGridBase.cs`** — Base class for all DevExpress grids with layout persistence via localStorage.
|
||||||
|
- **`GridProductDto.cs`** — Product data grid component.
|
||||||
|
- **`OrderNotificationToast.razor`** — Toast notification for order updates.
|
||||||
|
- **Pallet components** — PalletItemComponent.razor, GridShippingItemPallets.razor, GridDetailOrderItemPallets.razor.
|
||||||
|
|
||||||
|
## Subfolders
|
||||||
|
|
||||||
|
| Folder | Purpose |
|
||||||
|
|---|---|
|
||||||
|
| [`Grids/`](Grids/README.md) | Domain-specific grid components by entity type |
|
||||||
|
| [`FileUploads/`](FileUploads/README.md) | File upload components |
|
||||||
|
| [`StockTakings/`](StockTakings/README.md) | Stock taking UI components |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
> **LLM Maintenance:** If you modify code in this folder, update this README to reflect the changes. If you notice the README content does not match the current code, automatically update the README to match the code.
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
# Databases
|
||||||
|
|
||||||
|
Client-side in-memory table cache using ConcurrentDictionary for offline/fast data access.
|
||||||
|
|
||||||
|
## Key Files
|
||||||
|
|
||||||
|
- **`DatabaseClient.cs`** — (~250 lines) Local client-side database with typed tables (Shipping, ShippingDocument, ShippingItem, etc.). ProductDtoTable and OrderDtoTable with semaphore-based async loading. DatabaseTableBase<T> generic base. ObjectLock for thread-safe type-based locking. LoadingPanelVisibility global flag.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
> **LLM Maintenance:** If you modify code in this folder, update this README to reflect the changes. If you notice the README content does not match the current code, automatically update the README to match the code.
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
# Extensions
|
||||||
|
|
||||||
|
DevExpress dialog helper extensions.
|
||||||
|
|
||||||
|
## Key Files
|
||||||
|
|
||||||
|
- **`DevexpressComponentExtensions.cs`** — ShowMessageBoxAsync() and ShowConfirmBoxAsync() for DevExpress dialogs.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
> **LLM Maintenance:** If you modify code in this folder, update this README to reflect the changes. If you notice the README content does not match the current code, automatically update the README to match the code.
|
||||||
|
|
@ -29,25 +29,25 @@
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Reference Include="AyCode.Core">
|
<Reference Include="AyCode.Core">
|
||||||
<HintPath>..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Services.Server\bin\FruitBank\Debug\net9.0\AyCode.Core.dll</HintPath>
|
<HintPath>..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Services.Server\bin\FruitBank\$(Configuration)\net9.0\AyCode.Core.dll</HintPath>
|
||||||
</Reference>
|
</Reference>
|
||||||
<Reference Include="AyCode.Entities">
|
<Reference Include="AyCode.Entities">
|
||||||
<HintPath>..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Services.Server\bin\FruitBank\Debug\net9.0\AyCode.Entities.dll</HintPath>
|
<HintPath>..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Services.Server\bin\FruitBank\$(Configuration)\net9.0\AyCode.Entities.dll</HintPath>
|
||||||
</Reference>
|
</Reference>
|
||||||
<Reference Include="AyCode.Interfaces">
|
<Reference Include="AyCode.Interfaces">
|
||||||
<HintPath>..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Services.Server\bin\FruitBank\Debug\net9.0\AyCode.Interfaces.dll</HintPath>
|
<HintPath>..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Services.Server\bin\FruitBank\$(Configuration)\net9.0\AyCode.Interfaces.dll</HintPath>
|
||||||
</Reference>
|
</Reference>
|
||||||
<Reference Include="AyCode.Models">
|
<Reference Include="AyCode.Models">
|
||||||
<HintPath>..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Services.Server\bin\FruitBank\Debug\net9.0\AyCode.Models.dll</HintPath>
|
<HintPath>..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Services.Server\bin\FruitBank\$(Configuration)\net9.0\AyCode.Models.dll</HintPath>
|
||||||
</Reference>
|
</Reference>
|
||||||
<Reference Include="AyCode.Services">
|
<Reference Include="AyCode.Services">
|
||||||
<HintPath>..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Services.Server\bin\FruitBank\Debug\net9.0\AyCode.Services.dll</HintPath>
|
<HintPath>..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Services.Server\bin\FruitBank\$(Configuration)\net9.0\AyCode.Services.dll</HintPath>
|
||||||
</Reference>
|
</Reference>
|
||||||
<Reference Include="AyCode.Services.Server">
|
<Reference Include="AyCode.Services.Server">
|
||||||
<HintPath>..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Services.Server\bin\FruitBank\Debug\net9.0\AyCode.Services.Server.dll</HintPath>
|
<HintPath>..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Services.Server\bin\FruitBank\$(Configuration)\net9.0\AyCode.Services.Server.dll</HintPath>
|
||||||
</Reference>
|
</Reference>
|
||||||
<Reference Include="AyCode.Utils">
|
<Reference Include="AyCode.Utils">
|
||||||
<HintPath>..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Services.Server\bin\FruitBank\Debug\net9.0\AyCode.Utils.dll</HintPath>
|
<HintPath>..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Services.Server\bin\FruitBank\$(Configuration)\net9.0\AyCode.Utils.dll</HintPath>
|
||||||
</Reference>
|
</Reference>
|
||||||
<Reference Include="DevExpress.Blazor.Resources.v25.1">
|
<Reference Include="DevExpress.Blazor.Resources.v25.1">
|
||||||
|
|
||||||
|
|
@ -65,7 +65,7 @@
|
||||||
|
|
||||||
</Reference>
|
</Reference>
|
||||||
<Reference Include="Mango.Nop.Core">
|
<Reference Include="Mango.Nop.Core">
|
||||||
<HintPath>..\..\NopCommerce.Common\4.70\Libraries\Mango.Nop.Core\bin\FruitBank\Debug\net9.0\Mango.Nop.Core.dll</HintPath>
|
<HintPath>..\..\NopCommerce.Common\4.70\Libraries\Mango.Nop.Core\bin\FruitBank\$(Configuration)\net9.0\Mango.Nop.Core.dll</HintPath>
|
||||||
</Reference>
|
</Reference>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
# Layout
|
||||||
|
|
||||||
|
Application shell: root layout, navigation menu, auto-login, and toast notifications.
|
||||||
|
|
||||||
|
## Key Files
|
||||||
|
|
||||||
|
- **`MainLayout.razor`** — Root layout with navigation menu.
|
||||||
|
- **`MainLayout.razor.cs`** — SignalR message handling, auto-login on first render, toast notification for orders, login/logout handling, navigation guards.
|
||||||
|
- **`NavMenu.razor`** — Navigation component.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
> **LLM Maintenance:** If you modify code in this folder, update this README to reflect the changes. If you notice the README content does not match the current code, automatically update the README to match the code.
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
# Models
|
||||||
|
|
||||||
|
View models for measuring pages.
|
||||||
|
|
||||||
|
## Key Files
|
||||||
|
|
||||||
|
- **`MeasuringDateSelectorModel.cs`** — Date picker model: ShippingId, DateTime, IsMeasured flag.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
> **LLM Maintenance:** If you modify code in this folder, update this README to reflect the changes. If you notice the README content does not match the current code, automatically update the README to match the code.
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
# Pages
|
||||||
|
|
||||||
|
Routed Blazor pages for the FruitBank application.
|
||||||
|
|
||||||
|
## Key Files
|
||||||
|
|
||||||
|
- **`Home.razor.cs`** — Landing page showing form factor and platform.
|
||||||
|
- **`Login.razor.cs`** — Authentication with user selection and password validation.
|
||||||
|
- **`ShippingsAdmin.razor.cs`** — Inbound shipping management with tabbed interface.
|
||||||
|
- **`OrdersAdmin.razor.cs`** — Outbound order management with product and order item tabs.
|
||||||
|
- **`MeasuringIn.razor.cs`** — Shipping measurement: calendar date picker, item detail, pallet recording.
|
||||||
|
- **`MeasuringOut.razor.cs`** — Order measurement/audit: measurement tracking, approval workflow, RevisorId assignment.
|
||||||
|
- **`StockTaking.razor.cs`** — Inventory management: stock taking sessions, item reconciliation.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
> **LLM Maintenance:** If you modify code in this folder, update this README to reflect the changes. If you notice the README content does not match the current code, automatically update the README to match the code.
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
# FruitBankHybrid.Shared
|
||||||
|
|
||||||
|
Main Blazor UI library shared across all three deployment targets (Server, WASM, MAUI). Contains pages, DevExpress grid components, SignalR client, measurement service, and layout.
|
||||||
|
|
||||||
|
## Folder Structure
|
||||||
|
|
||||||
|
| Folder | Purpose |
|
||||||
|
|---|---|
|
||||||
|
| [`Components/`](Components/README.md) | DevExpress grid wrappers, pallet components, notifications |
|
||||||
|
| [`Pages/`](Pages/README.md) | Routed pages: Login, ShippingsAdmin, OrdersAdmin, MeasuringIn/Out, StockTaking |
|
||||||
|
| [`Services/`](Services/README.md) | SignalR client, measurement service, form factor, loggers |
|
||||||
|
| [`Layout/`](Layout/README.md) | MainLayout with navigation, auto-login, toast notifications |
|
||||||
|
| [`Models/`](Models/README.md) | Date selector model for measuring pages |
|
||||||
|
| [`Extensions/`](Extensions/README.md) | DevExpress MessageBox/ConfirmBox helpers |
|
||||||
|
| [`Databases/`](Databases/README.md) | Client-side ConcurrentDictionary table cache |
|
||||||
|
|
||||||
|
## Key Files (Root)
|
||||||
|
|
||||||
|
- **`_Imports.razor`** — Global Blazor imports.
|
||||||
|
- **`Routes.razor`** — Route definitions.
|
||||||
|
|
||||||
|
## Target Framework
|
||||||
|
|
||||||
|
.NET 10.0 with AOT compilation and WASM IL stripping enabled.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
> **LLM Maintenance:** If you modify code in this folder, update this README to reflect the changes. If you notice the README content does not match the current code, automatically update the README to match the code.
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
# Loggers
|
||||||
|
|
||||||
|
Custom logger implementations for the FruitBank client.
|
||||||
|
|
||||||
|
## Key Files
|
||||||
|
|
||||||
|
- **`LoggerClient.cs`** — Non-generic and generic `LoggerClient<T>` extending AyCode logger base.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
> **LLM Maintenance:** If you modify code in this folder, update this README to reflect the changes. If you notice the README content does not match the current code, automatically update the README to match the code.
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
# Services
|
||||||
|
|
||||||
|
Business logic, SignalR client, measurement helpers, and platform abstractions.
|
||||||
|
|
||||||
|
## Key Files
|
||||||
|
|
||||||
|
- **`IFormFactor.cs`** — Interface for device form factor detection.
|
||||||
|
- **`IMeasurementService.cs`** — Measurement operation interface.
|
||||||
|
- **`MeasurementService.cs`** — CSS styling for MeasuringStatus, pallet item creation/validation, status badge/text generation, shipping-level status calculation.
|
||||||
|
|
||||||
|
## Subfolders
|
||||||
|
|
||||||
|
| Folder | Purpose |
|
||||||
|
|---|---|
|
||||||
|
| [`Loggers/`](Loggers/README.md) | LoggerClient and LoggerClient<T> extending AyCode logger |
|
||||||
|
| [`SignalRs/`](SignalRs/README.md) | FruitBankSignalRClient hub client + DataSource wrappers |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
> **LLM Maintenance:** If you modify code in this folder, update this README to reflect the changes. If you notice the README content does not match the current code, automatically update the README to match the code.
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
# SignalRs
|
||||||
|
|
||||||
|
Main SignalR hub client and data source wrappers.
|
||||||
|
|
||||||
|
## Key Files
|
||||||
|
|
||||||
|
- **`FruitBankSignalRClient.cs`** — (~343 lines) Central hub client for ALL server communication. Methods for Partners, Shippings, ShippingItems, ShippingDocuments, Orders, OrderItems, OrderItemPallets, Products, StockTaking, GenericAttributes, Authentication.
|
||||||
|
- **`SignalRDataSource.cs`** — `SignalRDataSourceList<T>` and `SignalRDataSourceObservable<T>` wrappers for DevExpress grid binding.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
> **LLM Maintenance:** If you modify code in this folder, update this README to reflect the changes. If you notice the README content does not match the current code, automatically update the README to match the code.
|
||||||
|
|
@ -28,37 +28,37 @@
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Reference Include="AyCode.Core">
|
<Reference Include="AyCode.Core">
|
||||||
<HintPath>..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Services.Server\bin\FruitBank\Debug\net9.0\AyCode.Core.dll</HintPath>
|
<HintPath>..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Services.Server\bin\FruitBank\$(Configuration)\net9.0\AyCode.Core.dll</HintPath>
|
||||||
</Reference>
|
</Reference>
|
||||||
<Reference Include="AyCode.Core.Server">
|
<Reference Include="AyCode.Core.Server">
|
||||||
<HintPath>..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Services.Server\bin\FruitBank\Debug\net9.0\AyCode.Core.Server.dll</HintPath>
|
<HintPath>..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Services.Server\bin\FruitBank\$(Configuration)\net9.0\AyCode.Core.Server.dll</HintPath>
|
||||||
</Reference>
|
</Reference>
|
||||||
<Reference Include="AyCode.Entities">
|
<Reference Include="AyCode.Entities">
|
||||||
<HintPath>..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Services.Server\bin\FruitBank\Debug\net9.0\AyCode.Entities.dll</HintPath>
|
<HintPath>..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Services.Server\bin\FruitBank\$(Configuration)\net9.0\AyCode.Entities.dll</HintPath>
|
||||||
</Reference>
|
</Reference>
|
||||||
<Reference Include="AyCode.Entities.Server">
|
<Reference Include="AyCode.Entities.Server">
|
||||||
<HintPath>..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Services.Server\bin\FruitBank\Debug\net9.0\AyCode.Entities.Server.dll</HintPath>
|
<HintPath>..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Services.Server\bin\FruitBank\$(Configuration)\net9.0\AyCode.Entities.Server.dll</HintPath>
|
||||||
</Reference>
|
</Reference>
|
||||||
<Reference Include="AyCode.Interfaces">
|
<Reference Include="AyCode.Interfaces">
|
||||||
<HintPath>..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Services.Server\bin\FruitBank\Debug\net9.0\AyCode.Interfaces.dll</HintPath>
|
<HintPath>..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Services.Server\bin\FruitBank\$(Configuration)\net9.0\AyCode.Interfaces.dll</HintPath>
|
||||||
</Reference>
|
</Reference>
|
||||||
<Reference Include="AyCode.Interfaces.Server">
|
<Reference Include="AyCode.Interfaces.Server">
|
||||||
<HintPath>..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Services.Server\bin\FruitBank\Debug\net9.0\AyCode.Interfaces.Server.dll</HintPath>
|
<HintPath>..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Services.Server\bin\FruitBank\$(Configuration)\net9.0\AyCode.Interfaces.Server.dll</HintPath>
|
||||||
</Reference>
|
</Reference>
|
||||||
<Reference Include="AyCode.Models">
|
<Reference Include="AyCode.Models">
|
||||||
<HintPath>..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Services.Server\bin\FruitBank\Debug\net9.0\AyCode.Models.dll</HintPath>
|
<HintPath>..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Services.Server\bin\FruitBank\$(Configuration)\net9.0\AyCode.Models.dll</HintPath>
|
||||||
</Reference>
|
</Reference>
|
||||||
<Reference Include="AyCode.Models.Server">
|
<Reference Include="AyCode.Models.Server">
|
||||||
<HintPath>..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Services.Server\bin\FruitBank\Debug\net9.0\AyCode.Models.Server.dll</HintPath>
|
<HintPath>..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Services.Server\bin\FruitBank\$(Configuration)\net9.0\AyCode.Models.Server.dll</HintPath>
|
||||||
</Reference>
|
</Reference>
|
||||||
<Reference Include="AyCode.Services">
|
<Reference Include="AyCode.Services">
|
||||||
<HintPath>..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Services.Server\bin\FruitBank\Debug\net9.0\AyCode.Services.dll</HintPath>
|
<HintPath>..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Services.Server\bin\FruitBank\$(Configuration)\net9.0\AyCode.Services.dll</HintPath>
|
||||||
</Reference>
|
</Reference>
|
||||||
<Reference Include="AyCode.Services.Server">
|
<Reference Include="AyCode.Services.Server">
|
||||||
<HintPath>..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Services.Server\bin\FruitBank\Debug\net9.0\AyCode.Services.Server.dll</HintPath>
|
<HintPath>..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Services.Server\bin\FruitBank\$(Configuration)\net9.0\AyCode.Services.Server.dll</HintPath>
|
||||||
</Reference>
|
</Reference>
|
||||||
<Reference Include="AyCode.Utils">
|
<Reference Include="AyCode.Utils">
|
||||||
<HintPath>..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Services.Server\bin\FruitBank\Debug\net9.0\AyCode.Utils.dll</HintPath>
|
<HintPath>..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Services.Server\bin\FruitBank\$(Configuration)\net9.0\AyCode.Utils.dll</HintPath>
|
||||||
</Reference>
|
</Reference>
|
||||||
<Reference Include="DevExpress.Blazor.Resources.v25.1"></Reference>
|
<Reference Include="DevExpress.Blazor.Resources.v25.1"></Reference>
|
||||||
<Reference Include="DevExpress.Blazor.v25.1"></Reference>
|
<Reference Include="DevExpress.Blazor.v25.1"></Reference>
|
||||||
|
|
@ -66,7 +66,7 @@
|
||||||
<Reference Include="DevExpress.Data.v25.1"></Reference>
|
<Reference Include="DevExpress.Data.v25.1"></Reference>
|
||||||
<Reference Include="DevExpress.Utils.v25.1"></Reference>
|
<Reference Include="DevExpress.Utils.v25.1"></Reference>
|
||||||
<Reference Include="Mango.Nop.Core">
|
<Reference Include="Mango.Nop.Core">
|
||||||
<HintPath>..\..\NopCommerce.Common\4.70\Libraries\Mango.Nop.Core\bin\FruitBank\Debug\net9.0\Mango.Nop.Core.dll</HintPath>
|
<HintPath>..\..\NopCommerce.Common\4.70\Libraries\Mango.Nop.Core\bin\FruitBank\$(Configuration)\net9.0\Mango.Nop.Core.dll</HintPath>
|
||||||
</Reference>
|
</Reference>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
# FruitBankHybrid.Web.Client
|
||||||
|
|
||||||
|
Blazor WebAssembly client running in the browser after server prerendering. .NET 10.0.
|
||||||
|
|
||||||
|
## Folder Structure
|
||||||
|
|
||||||
|
| Folder | Purpose |
|
||||||
|
|---|---|
|
||||||
|
| [`Services/`](Services/README.md) | WASM-specific: FormFactor, credential storage, console logging |
|
||||||
|
|
||||||
|
## Key Files (Root)
|
||||||
|
|
||||||
|
- **`Program.cs`** — WASM startup, DI registration.
|
||||||
|
- **`_Imports.razor`** — Global imports.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
> **LLM Maintenance:** If you modify code in this folder, update this README to reflect the changes. If you notice the README content does not match the current code, automatically update the README to match the code.
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
# Services
|
||||||
|
|
||||||
|
WASM-specific service implementations.
|
||||||
|
|
||||||
|
## Key Files
|
||||||
|
|
||||||
|
- **`FormFactor.cs`** — Returns "WebAssembly" as form factor.
|
||||||
|
- **`WebSecureCredentialService.cs`** — localStorage-backed credential storage with XOR obfuscation + Base64 encoding (NOT cryptographically secure). 2-day expiration.
|
||||||
|
|
||||||
|
## Subfolders
|
||||||
|
|
||||||
|
- **`Loggers/BrowserConsoleLogWriter.cs`** — Browser console logging via JS interop.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
> **LLM Maintenance:** If you modify code in this folder, update this README to reflect the changes. If you notice the README content does not match the current code, automatically update the README to match the code.
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
# Components
|
||||||
|
|
||||||
|
Blazor Server app shell components.
|
||||||
|
|
||||||
|
## Key Files
|
||||||
|
|
||||||
|
- **`App.razor`** — Root component: DevExpress theme, asset configuration, render mode.
|
||||||
|
- **`_Imports.razor`** — Global imports.
|
||||||
|
- **`Pages/Error.razor`** — Error page with request ID tracking.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
> **LLM Maintenance:** If you modify code in this folder, update this README to reflect the changes. If you notice the README content does not match the current code, automatically update the README to match the code.
|
||||||
|
|
@ -6,7 +6,7 @@
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<!--<PublishTrimmed>true</PublishTrimmed>-->
|
<!--<PublishTrimmed>true</PublishTrimmed>-->
|
||||||
|
|
||||||
<RunAOTCompilation>false</RunAOTCompilation>
|
<RunAOTCompilation>true</RunAOTCompilation>
|
||||||
<WasmStripILAfterAOT>true</WasmStripILAfterAOT>
|
<WasmStripILAfterAOT>true</WasmStripILAfterAOT>
|
||||||
<OverrideHtmlAssetPlaceholders>true</OverrideHtmlAssetPlaceholders>
|
<OverrideHtmlAssetPlaceholders>true</OverrideHtmlAssetPlaceholders>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
@ -28,37 +28,37 @@
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Reference Include="AyCode.Core">
|
<Reference Include="AyCode.Core">
|
||||||
<HintPath>..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Services.Server\bin\FruitBank\Debug\net9.0\AyCode.Core.dll</HintPath>
|
<HintPath>..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Services.Server\bin\FruitBank\$(Configuration)\net9.0\AyCode.Core.dll</HintPath>
|
||||||
</Reference>
|
</Reference>
|
||||||
<Reference Include="AyCode.Core.Server">
|
<Reference Include="AyCode.Core.Server">
|
||||||
<HintPath>..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Services.Server\bin\FruitBank\Debug\net9.0\AyCode.Core.Server.dll</HintPath>
|
<HintPath>..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Services.Server\bin\FruitBank\$(Configuration)\net9.0\AyCode.Core.Server.dll</HintPath>
|
||||||
</Reference>
|
</Reference>
|
||||||
<Reference Include="AyCode.Entities">
|
<Reference Include="AyCode.Entities">
|
||||||
<HintPath>..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Services.Server\bin\FruitBank\Debug\net9.0\AyCode.Entities.dll</HintPath>
|
<HintPath>..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Services.Server\bin\FruitBank\$(Configuration)\net9.0\AyCode.Entities.dll</HintPath>
|
||||||
</Reference>
|
</Reference>
|
||||||
<Reference Include="AyCode.Entities.Server">
|
<Reference Include="AyCode.Entities.Server">
|
||||||
<HintPath>..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Services.Server\bin\FruitBank\Debug\net9.0\AyCode.Entities.Server.dll</HintPath>
|
<HintPath>..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Services.Server\bin\FruitBank\$(Configuration)\net9.0\AyCode.Entities.Server.dll</HintPath>
|
||||||
</Reference>
|
</Reference>
|
||||||
<Reference Include="AyCode.Interfaces">
|
<Reference Include="AyCode.Interfaces">
|
||||||
<HintPath>..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Services.Server\bin\FruitBank\Debug\net9.0\AyCode.Interfaces.dll</HintPath>
|
<HintPath>..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Services.Server\bin\FruitBank\$(Configuration)\net9.0\AyCode.Interfaces.dll</HintPath>
|
||||||
</Reference>
|
</Reference>
|
||||||
<Reference Include="AyCode.Interfaces.Server">
|
<Reference Include="AyCode.Interfaces.Server">
|
||||||
<HintPath>..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Services.Server\bin\FruitBank\Debug\net9.0\AyCode.Interfaces.Server.dll</HintPath>
|
<HintPath>..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Services.Server\bin\FruitBank\$(Configuration)\net9.0\AyCode.Interfaces.Server.dll</HintPath>
|
||||||
</Reference>
|
</Reference>
|
||||||
<Reference Include="AyCode.Models">
|
<Reference Include="AyCode.Models">
|
||||||
<HintPath>..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Services.Server\bin\FruitBank\Debug\net9.0\AyCode.Models.dll</HintPath>
|
<HintPath>..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Services.Server\bin\FruitBank\$(Configuration)\net9.0\AyCode.Models.dll</HintPath>
|
||||||
</Reference>
|
</Reference>
|
||||||
<Reference Include="AyCode.Models.Server">
|
<Reference Include="AyCode.Models.Server">
|
||||||
<HintPath>..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Services.Server\bin\FruitBank\Debug\net9.0\AyCode.Models.Server.dll</HintPath>
|
<HintPath>..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Services.Server\bin\FruitBank\$(Configuration)\net9.0\AyCode.Models.Server.dll</HintPath>
|
||||||
</Reference>
|
</Reference>
|
||||||
<Reference Include="AyCode.Services">
|
<Reference Include="AyCode.Services">
|
||||||
<HintPath>..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Services.Server\bin\FruitBank\Debug\net9.0\AyCode.Services.dll</HintPath>
|
<HintPath>..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Services.Server\bin\FruitBank\$(Configuration)\net9.0\AyCode.Services.dll</HintPath>
|
||||||
</Reference>
|
</Reference>
|
||||||
<Reference Include="AyCode.Services.Server">
|
<Reference Include="AyCode.Services.Server">
|
||||||
<HintPath>..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Services.Server\bin\FruitBank\Debug\net9.0\AyCode.Services.Server.dll</HintPath>
|
<HintPath>..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Services.Server\bin\FruitBank\$(Configuration)\net9.0\AyCode.Services.Server.dll</HintPath>
|
||||||
</Reference>
|
</Reference>
|
||||||
<Reference Include="AyCode.Utils">
|
<Reference Include="AyCode.Utils">
|
||||||
<HintPath>..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Services.Server\bin\FruitBank\Debug\net9.0\AyCode.Utils.dll</HintPath>
|
<HintPath>..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Services.Server\bin\FruitBank\$(Configuration)\net9.0\AyCode.Utils.dll</HintPath>
|
||||||
</Reference>
|
</Reference>
|
||||||
<Reference Include="DevExpress.Blazor.Resources.v25.1">
|
<Reference Include="DevExpress.Blazor.Resources.v25.1">
|
||||||
|
|
||||||
|
|
@ -76,7 +76,7 @@
|
||||||
|
|
||||||
</Reference>
|
</Reference>
|
||||||
<Reference Include="Mango.Nop.Core">
|
<Reference Include="Mango.Nop.Core">
|
||||||
<HintPath>..\..\NopCommerce.Common\4.70\Libraries\Mango.Nop.Core\bin\FruitBank\Debug\net9.0\Mango.Nop.Core.dll</HintPath>
|
<HintPath>..\..\NopCommerce.Common\4.70\Libraries\Mango.Nop.Core\bin\FruitBank\$(Configuration)\net9.0\Mango.Nop.Core.dll</HintPath>
|
||||||
</Reference>
|
</Reference>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
# FruitBankHybrid.Web
|
||||||
|
|
||||||
|
ASP.NET Core Blazor Server host. Serves the web interface, hosts SignalR hubs, and supports interactive WebAssembly rendering.
|
||||||
|
|
||||||
|
## Folder Structure
|
||||||
|
|
||||||
|
| Folder | Purpose |
|
||||||
|
|---|---|
|
||||||
|
| [`Services/`](Services/README.md) | Server-side FormFactor, SecureCredentialService (no-op), SignalR hub setup |
|
||||||
|
| [`Components/`](Components/README.md) | App.razor with DevExpress theme, Error page |
|
||||||
|
| `Controllers/` | Empty placeholder |
|
||||||
|
|
||||||
|
## Key Files (Root)
|
||||||
|
|
||||||
|
- **`Program.cs`** — DI, SignalR hub mapping (DevAdminSignalRHub, LoggerSignalRHub), 256KB max message size, DevExpress Fluent theme, static asset versioning.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
> **LLM Maintenance:** If you modify code in this folder, update this README to reflect the changes. If you notice the README content does not match the current code, automatically update the README to match the code.
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
# Services
|
||||||
|
|
||||||
|
Server-side service implementations.
|
||||||
|
|
||||||
|
## Key Files
|
||||||
|
|
||||||
|
- **`FormFactor.cs`** — Returns "Web" as form factor.
|
||||||
|
- **`ServerSecureCredentialService.cs`** — No-op implementation (clients handle credential storage).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
> **LLM Maintenance:** If you modify code in this folder, update this README to reflect the changes. If you notice the README content does not match the current code, automatically update the README to match the code.
|
||||||
|
|
@ -37,6 +37,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
|
||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AyCode.Blazor.Components.Tests", "..\..\..\Aycode\Source\AyCode.Blazor\AyCode.Blazor.Components.Tests\AyCode.Blazor.Components.Tests.csproj", "{5EFD44C6-DC9E-FEB8-F229-3E07C2E224FA}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AyCode.Blazor.Components.Tests", "..\..\..\Aycode\Source\AyCode.Blazor\AyCode.Blazor.Components.Tests\AyCode.Blazor.Components.Tests.csproj", "{5EFD44C6-DC9E-FEB8-F229-3E07C2E224FA}"
|
||||||
EndProject
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AyCode.Core", "..\..\..\Aycode\Source\AyCode.Core\AyCode.Core\AyCode.Core.csproj", "{EC0E3D9A-40DE-52EB-9E66-CFFBB36B5326}"
|
||||||
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AyCode.Core.Serializers.SourceGenerator", "..\..\..\Aycode\Source\AyCode.Core\AyCode.Core.Serializers.SourceGenerator\AyCode.Core.Serializers.SourceGenerator.csproj", "{1C882DAC-5027-BD65-9F22-A5FFF813FA36}"
|
||||||
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
Debug|Any CPU = Debug|Any CPU
|
||||||
|
|
@ -101,6 +105,14 @@ Global
|
||||||
{5EFD44C6-DC9E-FEB8-F229-3E07C2E224FA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{5EFD44C6-DC9E-FEB8-F229-3E07C2E224FA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{5EFD44C6-DC9E-FEB8-F229-3E07C2E224FA}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{5EFD44C6-DC9E-FEB8-F229-3E07C2E224FA}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{5EFD44C6-DC9E-FEB8-F229-3E07C2E224FA}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{5EFD44C6-DC9E-FEB8-F229-3E07C2E224FA}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{EC0E3D9A-40DE-52EB-9E66-CFFBB36B5326}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{EC0E3D9A-40DE-52EB-9E66-CFFBB36B5326}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{EC0E3D9A-40DE-52EB-9E66-CFFBB36B5326}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{EC0E3D9A-40DE-52EB-9E66-CFFBB36B5326}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{1C882DAC-5027-BD65-9F22-A5FFF813FA36}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{1C882DAC-5027-BD65-9F22-A5FFF813FA36}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{1C882DAC-5027-BD65-9F22-A5FFF813FA36}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{1C882DAC-5027-BD65-9F22-A5FFF813FA36}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
|
|
|
||||||
|
|
@ -123,13 +123,13 @@
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Reference Include="AyCode.Core">
|
<Reference Include="AyCode.Core">
|
||||||
<HintPath>..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Services.Server\bin\FruitBank\Debug\net9.0\AyCode.Core.dll</HintPath>
|
<HintPath>..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Services.Server\bin\FruitBank\$(Configuration)\net9.0\AyCode.Core.dll</HintPath>
|
||||||
</Reference>
|
</Reference>
|
||||||
<Reference Include="AyCode.Services">
|
<Reference Include="AyCode.Services">
|
||||||
<HintPath>..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Services.Server\bin\FruitBank\Debug\net9.0\AyCode.Services.dll</HintPath>
|
<HintPath>..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Services.Server\bin\FruitBank\$(Configuration)\net9.0\AyCode.Services.dll</HintPath>
|
||||||
</Reference>
|
</Reference>
|
||||||
<Reference Include="AyCode.Entities">
|
<Reference Include="AyCode.Entities">
|
||||||
<HintPath>..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Services.Server\bin\FruitBank\Debug\net9.0\AyCode.Entities.dll</HintPath>
|
<HintPath>..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Services.Server\bin\FruitBank\$(Configuration)\net9.0\AyCode.Entities.dll</HintPath>
|
||||||
</Reference>
|
</Reference>
|
||||||
<!--<Reference Include="Mango.Nop.Core">
|
<!--<Reference Include="Mango.Nop.Core">
|
||||||
<HintPath>..\..\NopCommerce.Common\4.70\Libraries\Mango.Nop.Core\bin\FruitBank\Debug\net9.0\Mango.Nop.Core.dll</HintPath>
|
<HintPath>..\..\NopCommerce.Common\4.70\Libraries\Mango.Nop.Core\bin\FruitBank\Debug\net9.0\Mango.Nop.Core.dll</HintPath>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
# Platforms
|
||||||
|
|
||||||
|
Per-platform entry points for MAUI.
|
||||||
|
|
||||||
|
## Folders
|
||||||
|
|
||||||
|
- **`Android/`** — MainActivity.cs, MainApplication.cs (custom keystore config).
|
||||||
|
- **`iOS/`** — AppDelegate.cs, Program.cs.
|
||||||
|
- **`MacCatalyst/`** — AppDelegate.cs, Program.cs.
|
||||||
|
- **`Windows/`** — App.xaml.cs (WinUI entry point).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
> **LLM Maintenance:** If you modify code in this folder, update this README to reflect the changes. If you notice the README content does not match the current code, automatically update the README to match the code.
|
||||||
|
|
@ -16,7 +16,7 @@ namespace FruitBankHybrid.WinUI
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public App()
|
public App()
|
||||||
{
|
{
|
||||||
this.InitializeComponent();
|
//this.InitializeComponent();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();
|
protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
# FruitBankHybrid
|
||||||
|
|
||||||
|
.NET MAUI Hybrid cross-platform app hosting Blazor components via BlazorWebView. Targets Android (API 33+), iOS (15.0+), and Windows.
|
||||||
|
|
||||||
|
## Folder Structure
|
||||||
|
|
||||||
|
| Folder | Purpose |
|
||||||
|
|---|---|
|
||||||
|
| [`Services/`](Services/README.md) | Platform-specific: FormFactor, SecureCredentialService, BrowserConsoleLogWriter |
|
||||||
|
| [`Platforms/`](Platforms/README.md) | Per-platform entry points: Android, iOS, Windows |
|
||||||
|
| `Components/` | Razor component imports (_Imports.razor) |
|
||||||
|
| `Resources/` | AppIcon, splash screens, fonts, images |
|
||||||
|
|
||||||
|
## Key Files (Root)
|
||||||
|
|
||||||
|
- **`MauiProgram.cs`** — DI registration, DevExpress init, SignalR client setup.
|
||||||
|
- **`MainPage.xaml.cs`** — BlazorWebView host page.
|
||||||
|
- **`App.xaml.cs`** — MAUI Application entry point.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
> **LLM Maintenance:** If you modify code in this folder, update this README to reflect the changes. If you notice the README content does not match the current code, automatically update the README to match the code.
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
# Services
|
||||||
|
|
||||||
|
MAUI platform-specific service implementations.
|
||||||
|
|
||||||
|
## Key Files
|
||||||
|
|
||||||
|
- **`FormFactor.cs`** — Device idiom detection (Phone, Tablet, Desktop).
|
||||||
|
- **`MauiSecureCredentialService.cs`** — SecureStorage-backed credential persistence with 2-day expiration.
|
||||||
|
|
||||||
|
## Subfolders
|
||||||
|
|
||||||
|
- **`Loggers/BrowserConsoleLogWriter.cs`** — Browser console logging bridge for BlazorWebView.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
> **LLM Maintenance:** If you modify code in this folder, update this README to reflect the changes. If you notice the README content does not match the current code, automatically update the README to match the code.
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
# FruitBankHybridApp
|
||||||
|
|
||||||
|
nopCommerce plugin for FruitBank, a fruit & vegetable wholesaler. Manages supplier inbound delivery (Shipping), outgoing orders (Order), warehouse weighing, and inventory stocktaking. Runs as Blazor Server, Blazor WASM, and MAUI Hybrid (Android/iOS/Windows).
|
||||||
|
|
||||||
|
## LLM Context
|
||||||
|
|
||||||
|
Domain rules and critical pitfalls live in a single file: [`.github/copilot-instructions.md`](.github/copilot-instructions.md)
|
||||||
|
|
||||||
|
| Tool | Auto-loaded | Action needed |
|
||||||
|
|------|------------|---------------|
|
||||||
|
| GitHub Copilot | ✅ `copilot-instructions.md` | None |
|
||||||
|
| Claude Code | ✅ `CLAUDE.md` → references above | None |
|
||||||
|
| Cursor / Windsurf | ✅ `README.md` | Read `copilot-instructions.md` via @file |
|
||||||
|
|
||||||
|
Detailed docs: [`docs/`](docs/) — GLOSSARY.md, ARCHITECTURE.md, CONVENTIONS.md, SCHEMA.md
|
||||||
|
|
||||||
|
## Solution Structure
|
||||||
|
|
||||||
|
| Project | Purpose | README |
|
||||||
|
|---|---|---|
|
||||||
|
| [`FruitBank.Common`](FruitBank.Common/README.md) | Shared domain: entities, DTOs, interfaces, SignalR tags, measurement helpers | [README](FruitBank.Common/README.md) |
|
||||||
|
| [`FruitBank.Common.Server`](FruitBank.Common.Server/README.md) | Server-side: SignalR hubs, broadcast service, logging, nopCommerce integration | [README](FruitBank.Common.Server/README.md) |
|
||||||
|
| [`FruitBankHybrid.Shared`](FruitBankHybrid.Shared/README.md) | Blazor UI: pages, grids, SignalR client, measurement service, layout | [README](FruitBankHybrid.Shared/README.md) |
|
||||||
|
| [`FruitBankHybrid.Shared.Common`](FruitBankHybrid.Shared.Common/README.md) | Shared common library (placeholder) | [README](FruitBankHybrid.Shared.Common/README.md) |
|
||||||
|
| [`FruitBankHybrid`](FruitBankHybrid/README.md) | MAUI Hybrid app: Android, iOS, Windows | [README](FruitBankHybrid/README.md) |
|
||||||
|
| [`FruitBankHybrid.Web`](FruitBankHybrid.Web/README.md) | Blazor Server host with SignalR hubs | [README](FruitBankHybrid.Web/README.md) |
|
||||||
|
| [`FruitBankHybrid.Web.Client`](FruitBankHybrid.Web.Client/README.md) | Blazor WebAssembly client | [README](FruitBankHybrid.Web.Client/README.md) |
|
||||||
|
|
||||||
|
### Test Projects
|
||||||
|
|
||||||
|
| Project | Purpose | README |
|
||||||
|
|---|---|---|
|
||||||
|
| [`FruitBankHybrid.Shared.Tests`](FruitBankHybrid.Shared.Tests/README.md) | Integration + serialization tests (SignalR, JSON, Toon, bunit) | [README](FruitBankHybrid.Shared.Tests/README.md) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
> **LLM Maintenance:** If you modify this solution's structure, update this README to reflect the changes.
|
||||||
|
|
@ -0,0 +1,55 @@
|
||||||
|
# Architecture
|
||||||
|
|
||||||
|
## Solution Dependency Graph
|
||||||
|
|
||||||
|
```
|
||||||
|
FruitBank.Common (shared domain)
|
||||||
|
↑
|
||||||
|
FruitBank.Common.Server (server-side services, nopCommerce)
|
||||||
|
↑
|
||||||
|
FruitBankHybrid.Shared.Common (shared utilities, placeholder)
|
||||||
|
↑
|
||||||
|
FruitBankHybrid.Shared (Blazor UI components, pages, services)
|
||||||
|
↑ ↑ ↑
|
||||||
|
FruitBankHybrid FruitBankHybrid.Web FruitBankHybrid.Web.Client
|
||||||
|
(MAUI Hybrid) (Blazor Server) (Blazor WASM)
|
||||||
|
```
|
||||||
|
|
||||||
|
All projects also reference **AyCode.Core** and **AyCode.Blazor** via DLL references (not ProjectReference).
|
||||||
|
|
||||||
|
## Three Deployment Targets
|
||||||
|
|
||||||
|
| Target | Project | How UI Runs |
|
||||||
|
|---|---|---|
|
||||||
|
| **MAUI Hybrid** | FruitBankHybrid | Native app with BlazorWebView |
|
||||||
|
| **Blazor Server** | FruitBankHybrid.Web | Server-side rendering + SignalR |
|
||||||
|
| **Blazor WASM** | FruitBankHybrid.Web.Client | Downloaded to browser, runs in WASM |
|
||||||
|
|
||||||
|
All three share the same UI components from `FruitBankHybrid.Shared`.
|
||||||
|
|
||||||
|
## Data Flow
|
||||||
|
|
||||||
|
```
|
||||||
|
User → DevExpress Grid → AcSignalRDataSource → SignalR (AcBinary) → DevAdminSignalRHub
|
||||||
|
↓
|
||||||
|
DynamicMethodRegistry
|
||||||
|
↓
|
||||||
|
IFruitBankDataControllerServer
|
||||||
|
ICustomOrderSignalREndpointServer
|
||||||
|
IStockSignalREndpointServer
|
||||||
|
↓
|
||||||
|
nopCommerce Database
|
||||||
|
```
|
||||||
|
|
||||||
|
## Key Architectural Decisions
|
||||||
|
|
||||||
|
- **nopCommerce plugin** — Customer, Order, Product are nopCommerce entities extended via GenericAttributes and DTOs
|
||||||
|
- **SignalR over REST** — all data flows through SignalR with AcBinary protocol
|
||||||
|
- **DevExpress Blazor 25.1.3** — exclusive UI component library
|
||||||
|
- **Three measurement hierarchies** — Shipping/Order/StockTaking share same base but have different audit rules
|
||||||
|
- **Client-side database** — `DatabaseClient` caches entities in ConcurrentDictionary for offline/fast access
|
||||||
|
- **Platform-specific credential storage** — MAUI uses SecureStorage, Web uses obfuscated localStorage, Server uses no-op
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
> **LLM Maintenance:** If you modify the architecture, update this document.
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
# Conventions
|
||||||
|
|
||||||
|
## Naming
|
||||||
|
|
||||||
|
- **fb prefix** on database tables: `fbPallet`, `fbShipping`, `fbShippingItem`, etc.
|
||||||
|
- **Dto suffix** for DTOs wrapping nopCommerce entities: `OrderDto`, `OrderItemDto`, `ProductDto`.
|
||||||
|
- **XxxItemPallet** for measurement records: `ShippingItemPallet`, `OrderItemPallet`, `StockTakingItemPallet`.
|
||||||
|
- **Grid prefix** for Blazor grid components: `GridPartnerBase`, `GridShippingBase`, etc.
|
||||||
|
- **SignalRTags** constants use numeric ranges by domain (see `FruitBank.Common/SignalRs/`).
|
||||||
|
|
||||||
|
## Patterns
|
||||||
|
|
||||||
|
- **MeasuringItemPalletBase** as abstract base for all three measurement hierarchies.
|
||||||
|
- **GenericAttributes** for extending nopCommerce entities with custom data (IsMeasurable, Tare, AverageWeight).
|
||||||
|
- **Composition interfaces** for measurement traits: IMeasuringValues = IMeasuringWeights + IMeasuringQuantity.
|
||||||
|
- **DevExpress DxGrid** with `AcSignalRDataSource` for real-time grid data.
|
||||||
|
- **MgGridBase** for grid layout persistence via localStorage.
|
||||||
|
- **FruitBankSignalRClient** as single hub client for all server communication.
|
||||||
|
- **DatabaseClient** for client-side caching with ConcurrentDictionary tables.
|
||||||
|
|
||||||
|
## Critical Rules
|
||||||
|
|
||||||
|
- **MeasuringStatus.Finnished** — intentional legacy typo. Do NOT fix.
|
||||||
|
- **Shipping = INBOUND, Order = OUTBOUND** — never confuse directions.
|
||||||
|
- **Pallet = measurement record** — always created, even for non-measurable products.
|
||||||
|
- **NetWeight is calculated, never stored** — GrossWeight − PalletWeight − (TrayQuantity × TareWeight).
|
||||||
|
- **DLL references** to AyCode projects are intentional — do not convert to ProjectReference.
|
||||||
|
- Do not suggest removal/rollback as a solution — find a fix.
|
||||||
|
|
||||||
|
## UI (Hungarian Locale)
|
||||||
|
|
||||||
|
- Status labels and UI text are in Hungarian.
|
||||||
|
- MeasuringStatus display: "Nincs elkezdve", "Elkezdve", "Kész", "Auditált".
|
||||||
|
- Date format follows Hungarian conventions.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
> **LLM Maintenance:** If you establish new conventions, document them here.
|
||||||
|
|
@ -0,0 +1,64 @@
|
||||||
|
# Glossary / Fogalomtár
|
||||||
|
|
||||||
|
Domain terminology for the FruitBank system. **Read this before making changes.**
|
||||||
|
|
||||||
|
## Business Domain
|
||||||
|
|
||||||
|
| English | Magyar | Definition |
|
||||||
|
|---|---|---|
|
||||||
|
| **Shipping** | Beszállítás | **INBOUND** delivery: supplier → warehouse. A truck arrival event. |
|
||||||
|
| **Order** | Megrendelés | **OUTBOUND** delivery: warehouse → customer. |
|
||||||
|
| **Pallet** (XxxItemPallet) | Mérési rekord | A **measurement record**, NOT a physical pallet. Always created, even for non-measurable products. |
|
||||||
|
| **Partner** | Partner / Beszállító | External supplier providing goods. |
|
||||||
|
| **ShippingDocument** | Szállítólevél | Supplier's delivery note or invoice, linked to a Shipping. |
|
||||||
|
| **ShippingItem** | Szállítólevél tétel | Product line on a shipping document. Tracks declared vs measured discrepancies. |
|
||||||
|
| **StockTaking** | Leltározás | Inventory session that freezes logical stock and reconciles with physical count. |
|
||||||
|
| **GenericAttribute** | Generikus attribútum | nopCommerce polymorphic key-value store. KeyGroup = owner type, EntityId = owner ID. |
|
||||||
|
|
||||||
|
## Measurement System
|
||||||
|
|
||||||
|
| Term | Definition |
|
||||||
|
|---|---|
|
||||||
|
| **IsMeasurable** | Product-level flag. If `false`: weights = 0.0, only TrayQuantity matters. A Pallet record is still created. |
|
||||||
|
| **NetWeight** | `GrossWeight − PalletWeight − (TrayQuantity × TareWeight)` — universal formula across all three hierarchies. |
|
||||||
|
| **TrayQuantity** | Always recorded, regardless of measurability. Count of trays/crates. |
|
||||||
|
| **GrossWeight** | Total weight including pallet and packaging. 0.0 if not measurable. |
|
||||||
|
| **PalletWeight** | Weight of the physical pallet. 0.0 if goods arrive without one. |
|
||||||
|
| **TareWeight** | Weight of a single tray/crate. Used in NetWeight calculation. |
|
||||||
|
| **AverageWeight** | Per-pallet average: `NetWeight / TrayQuantity`. Validated against threshold. |
|
||||||
|
| **MeasuringStatus** | NotStarted(0) → Started(10) → **Finnished**(20) → Audited(30). Note: "Finnished" is intentional. |
|
||||||
|
| **RevisorId** | Quality auditor's Customer ID. OrderItemPallet becomes "Audited" when RevisorId > 0. |
|
||||||
|
|
||||||
|
## Three Measurement Hierarchies
|
||||||
|
|
||||||
|
All share `MeasuringItemPalletBase` with the same NetWeight formula:
|
||||||
|
|
||||||
|
| Flow | Parent | Pallet Record | Extra |
|
||||||
|
|---|---|---|---|
|
||||||
|
| **Inbound (Shipping)** | ShippingItem | ShippingItemPallet | Declared vs measured discrepancy |
|
||||||
|
| **Outbound (Order)** | OrderItemDto | OrderItemPallet | RevisorId for audit |
|
||||||
|
| **Inventory (StockTaking)** | StockTakingItem | StockTakingItemPallet | QuantityDiff for stock adjustment |
|
||||||
|
|
||||||
|
## nopCommerce Entities
|
||||||
|
|
||||||
|
These are **NOT custom FruitBank entities** — they come from nopCommerce:
|
||||||
|
- Customer, Order, OrderItem, OrderNote, Product, GenericAttribute
|
||||||
|
|
||||||
|
FruitBank extends them via:
|
||||||
|
- **DTOs** (OrderDto, OrderItemDto, ProductDto) that wrap nopCommerce entities with measurement properties
|
||||||
|
- **GenericAttributes** for storing custom values (IsMeasurable, Tare, AverageWeight, etc.)
|
||||||
|
|
||||||
|
## Common Traps
|
||||||
|
|
||||||
|
| Trap | Correct Behavior |
|
||||||
|
|---|---|
|
||||||
|
| "Pallet" = physical pallet | ❌ It's a measurement record. Always created. |
|
||||||
|
| Shipping = outgoing | ❌ Shipping = INBOUND. Order = OUTBOUND. |
|
||||||
|
| Fix "Finnished" spelling | ❌ Intentional legacy typo. Do NOT fix. |
|
||||||
|
| IsMeasurable=false means no Pallet | ❌ Pallet is always created, weights just = 0.0 |
|
||||||
|
| NetWeight is stored | ❌ It's calculated: GrossWeight − PalletWeight − (TrayQuantity × TareWeight) |
|
||||||
|
| GenericAttribute is simple | ❌ It's polymorphic: KeyGroup determines which entity type owns the record |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
> **LLM Maintenance:** If you modify domain terminology or introduce new business concepts, update this glossary.
|
||||||
|
|
@ -0,0 +1,635 @@
|
||||||
|
# Domain Model Schema (Toon Format)
|
||||||
|
|
||||||
|
> Full domain model in [Toon](../../AyCode.Core/AyCode.Core/Serializers/Toons/README.md) (Token-Oriented Object Notation) format.
|
||||||
|
> This is the authoritative schema for entities, DTOs, and enums in the FruitBank domain.
|
||||||
|
|
||||||
|
@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,28 @@
|
||||||
|
#!/usr/bin/env dotnet-script
|
||||||
|
|
||||||
|
#r "H:/Applications/Mango/Source/FruitBankHybridApp/FruitBank.Common/bin/Debug/net9.0/FruitBank.Common.dll"
|
||||||
|
#r "H:/Applications/Aycode/Source/AyCode.Core/AyCode.Core/bin/FruitBank/Debug/net9.0/AyCode.Core.dll"
|
||||||
|
|
||||||
|
using AyCode.Core.Serializers.Toons;
|
||||||
|
using FruitBank.Common.Dtos;
|
||||||
|
|
||||||
|
var toon = AcToonSerializer.SerializeTypeMetadata<OrderDto>();
|
||||||
|
Console.WriteLine(toon);
|
||||||
|
|
||||||
|
// Search for IsMeasurable property output
|
||||||
|
if (toon.Contains("business-logic:"))
|
||||||
|
{
|
||||||
|
Console.WriteLine("\n✓ SUCCESS: business-logic attribute found!");
|
||||||
|
var lines = toon.Split('\n');
|
||||||
|
foreach (var line in lines)
|
||||||
|
{
|
||||||
|
if (line.Contains("IsMeasurable") || line.Contains("business-logic:"))
|
||||||
|
{
|
||||||
|
Console.WriteLine(line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Console.WriteLine("\n✗ FAIL: business-logic attribute NOT found!");
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
/h/Applications/Mango/Source/FruitBankHybridApp
|
||||||
Loading…
Reference in New Issue