Compare commits

..

43 Commits

Author SHA1 Message Date
Loretta db9f7aa12f Add new DB table constants and domain description
Added constants for CustomerCredit, PreOrder, and PreOrderItem DB table names to FruitBankConstClient. Introduced a DomainDescription constant summarizing the plugin's business logic and purpose. No other changes made.
2026-05-27 12:30:46 +02:00
Loretta ea643d0855 Add preorder entities, enums, and UI/admin updates
- Added PreorderWindowStart/End constants for product preorder visibility.
- Introduced CustomerCredit, Preorder, and PreorderItem entities with ORM mapping.
- Added PreorderStatus and PreorderItemStatus enums.
- Updated FruitBankConstClient with new BaseUrl options.
- Changed order grid tabs to admin-only and improved summary logic.
- Displayed CargoCompany in shipping document grid.
- Enhanced measured quantity highlighting in shipping item grid.
- Bumped app version to 1.0.2.
2026-05-26 17:04:07 +02:00
Loretta 431ed8c037 Sync PackageVersion with ApplicationDisplayVersion
Added <PackageVersion> property to FruitBankHybrid.csproj to ensure the NuGet package version matches the application's display version, improving version consistency across metadata and user-facing information.
2026-05-26 16:28:55 +02:00
Loretta 352b3b2d21 Add XML doc standards; enable compiler-generated files
Added "XML Documentation" section to CONVENTIONS.md, specifying usage of <summary> tags for developer-facing comments and restricting implementation details to external docs. Enabled EmitCompilerGeneratedFiles in FruitBank.Common.csproj for improved debugging and analysis.
2026-05-23 09:27:29 +02:00
Loretta 5b0b080b5a [LOADED_DOCS: 3 files, no new loads]
Rename ShallowCopy to FlatCopy, add polymorph support

- Renamed all "ShallowCopy" serializer presets and references to "FlatCopy" for clarity and consistency.
- Expanded documentation to clarify flat serialization use cases, especially for delta-update and partial-write scenarios.
- Added EnablePolymorphDetectFeature to AcBinarySerializableAttribute and updated all constructor overloads.
- Set UsePolymorphType = true in AcBinarySourceGenerator to enable polymorphic type support by default.
- Updated all [AcBinarySerializable(...)] usages to include new feature flags, explicitly disabling property filter and polymorph detection for affected types.
- Improved comments and documentation for maintainability.
2026-05-15 08:40:53 +02:00
Loretta 40d6567f29 [LOADED_DOCS: 3 files, no new loads]
SIMD UTF-8 upgrades, i18n test data, MVC disabled

- Switch all test/benchmark data to Hungarian UTF-8 strings for i18n coverage
- Add AVX-512BW, Vector256, and Vector128 SIMD paths for UTF-8/UTF-16 encode/decode (ASCII and multi-byte) in binary serializer/deserializer
- Update WireMode docs for encoding guidance per workload/host
- Block-comment and disable MVC formatters and Microsoft.AspNetCore.App reference due to .NET 10 Hybrid client conflict; update docs to reflect temporary state
- Update appsettings: replace WaitForFlush with FlushPolicy
- Revise BINARY_TODO.md for SIMD transcoder progress and next steps
2026-05-05 15:06:11 +02:00
Loretta 9f8dbd29fc [LOADED_DOCS: 3 files, no new loads]
Update ID format to use per-repo prefixes and random suffix

Migrated all issue, TODO, and decision IDs to a new 4-part format: <PREFIX>-<TOPIC>-<TYPE>-<RAND>. Added per-repo prefix declarations in copilot-instructions.md and documented conventions in REPO_PREFIXES.md. Updated all topic registries, logs, cross-references, and documentation to use the new format. Introduced MIGRATION_ID_MAPPING.md for old-to-new ID mapping. Enhanced skills and protocol audit logic to validate and enforce per-repo prefixes and topic codes at runtime. Clarified Framework-First doctrine and ensured all references are unambiguous.
2026-04-26 19:12:51 +02:00
Loretta ab51b948ae [LOADED_DOCS: 3 files, no new loads]
Add Rule #6, AUTH topic, ADRs, config & doc updates

- Codified Rule #6 (authority, rule scope, skill invocation) in all primary copilot-instructions.md files
- Clarified skill pre-load/lazy-load rules and LOADED_DOCS prefix
- Forbid skill/template version labels in Decision Log governance
- Scaffolded new AUTH topic with README, ISSUES, and TODO files
- Added repo/project ADR folders and templates; new ADR for AcBinaryHubProtocol decorator stack
- Migrated cross-cutting issues/TODOs to Closed with detailed resolution
- Made FruitBankHybrid.Shared/appsettings.json the canonical config source; suppressed Razor SDK auto-publish to avoid file collisions
- Updated protocol/wire format docs for AcBinaryHubProtocol
- Minor config: updated ports, WaitForFlush, and csproj content rules
2026-04-26 13:44:12 +02:00
Loretta 25522f7c27 Add adr-author skill, ADR template, and log security issues
- Introduced the `adr-author` skill for structured ADR creation; updated session setup and shared skills to require pre-loading it.
- Added `SKILL.md` and `ADR_TEMPLATE.md` for ADR authoring workflow and documentation.
- Updated protocol decision log with entries for the new skill and its integration.
- Documented two critical JWT logging security issues in `LOGGING_ISSUES.md`.
- Minor: added a cleanup Bash command in `settings.local.json`.
2026-04-25 07:24:16 +02:00
Loretta aecd54ffdd [LOADED_DOCS: 4 files, no new loads]
Refactor docs: topic folders, TOON, XCUT, protocol sync

- Migrated all topic documentation into dedicated folders with canonical `README.md`, `ISSUES.md`, and `TODO.md` per topic (e.g., `LOGGING/`, `SIGNALR/`, `BINARY/`, `TOON/`).
- Added comprehensive TOON serializer documentation: design, format, options, attributes, inference, issues, and TODOs.
- Introduced `XCUT` folder for cross-cutting issues and TODOs, with canonical entries and topic cross-references.
- Updated all references and navigation to use new folder-based doc paths; fixed links and clarified doc structure.
- Enhanced AI agent protocol: enforce session skill preloading, `[LOADED_DOCS: ...]` short-name prefix, and mandatory `docs-check` skill for doc/code sync.
- Updated `.csproj` to include all `README.md` files for IDE visibility.
- Improved and clarified SignalR, grid, and project-level documentation.
- Minor code/test tweaks and doc content corrections for consistency.
2026-04-24 21:54:04 +02:00
Loretta ac7b4d58df [LOADED_DOCS: NONE]
Update protocol, docs-discovery skill, and doc layering

- Switched AI AGENT CORE PROTOCOL to new `[LOADED_DOCS: N files (+K this turn: ...)]` prefix format in all primary instruction files; updated examples and enforcement details.
- Added `docs-discovery` skill for proactive .md doc loading before code search; documented usage and integration as a shared agent skill.
- Introduced `## Protocol History` section and `LLM_PROTOCOL_DECISIONS.md` decision log for cross-repo protocol change tracking.
- Expanded protocol-audit skill and `REPOS.md` to support 8 instruction files (primary/inherit split), new invariants, and known issues.
- Added/updated structured TODO and issues docs for serialization, logging, and SignalR binary protocol.
- Improved cross-references, doc layering, and clarified documentation-first coding workflow.
- Various minor doc clarifications and formatting for protocol consistency.
2026-04-24 08:21:49 +02:00
Loretta 711c3c8ec0 Framework-first doctrine, DI logger factory, config refactor
Introduced framework-first design rules and updated documentation to clarify framework vs. consumer boundaries. Added AcLoggerOptions and DI-based logger factory extensions to AyCode.Core, enabling per-category logger instantiation from appsettings.json. Standardized SignalR connection setup with AddAcDefaults, replacing project-specific code. Enhanced protocol configuration for DI scope isolation. Refactored appsettings.json structure and added MSBuild targets for config propagation. Removed obsolete code and updated comments to match new patterns.
2026-04-23 16:11:22 +02:00
Loretta 33d84a8257 [LOADED_DOCS: .github\copilot-instructions.md, C:\Users\Fullepi\copilot-instructions.md]
Refactor SignalR client DI and config, add test factory

Refactored FruitBankSignalRClient construction to use DI and centralized configuration from appsettings.json across all platforms. Introduced TestSignalRClientFactory for consistent test setup. Added FruitBankHubConnectionExtensions for reusable SignalR connection and logging configuration. Updated Program.cs and MauiProgram.cs to register logger factories, log writers, and IHubConnectionBuilder via DI. Embedded appsettings.json in MAUI and updated .csproj references for build flexibility. No business logic changes; all updates are infrastructure and test setup.
2026-04-22 22:45:32 +02:00
Loretta 10f325cc26 Refactor SignalR binary protocol for extensibility
- Move SignalParams-aware deserialization logic from AcBinaryHubProtocol to new AyCodeBinaryHubProtocol, enabling project-specific customization.
- Make key deserialization and helper methods in AcBinaryHubProtocol protected and virtual for easier extension.
- Improve byte[] handling to distinguish between AcBinary-serialized and raw data.
- Remove diagnostic serialization verification from the base protocol.
- Update DI registration to use AyCodeBinaryHubProtocol with configurable options.
- Adjust client code to support object-based response data and raw byte handling.
- Comment out SignalResponseDataMessage diagnostic logger in Program.cs.
2026-04-09 08:12:50 +02:00
Loretta 26b40cf7a1 Refactor MessageReceived to use new SignalR param types
Updated the MessageReceived method signature to accept SignalParams and SignalData instead of SignalReceiveParams and byte[]. Adjusted internal usage to reflect these new types, improving consistency with updated SignalR client structures.
2026-04-06 22:45:26 +02:00
Loretta 27ac2d1843 Refactor SignalR message handling in FruitBankSignalRClient
Update MessageReceived to use SignalReceiveParams and construct
SignalResponseDataMessage with more context. Add AyCode.Core.Serializers
using directive for serializer type support.
2026-04-05 09:31:06 +02:00
Loretta 8f48838ded Clarify and enforce doc-first, no-redundant-read policy
Correct rule references for no re-reading of loaded `.md` files. Add explicit "STRICT NO-RE-READ POLICY" section defining context and re-read conditions. Expand context recovery rules with auto-detection triggers and specify directories for recovery. These changes make doc-first and anti-loop policies clearer and more robust, ensuring efficient and up-to-date documentation usage.
2026-04-04 20:50:51 +02:00
Loretta 322f38f1fa Strengthen doc-first, multi-repo protocol and clarify tools
Expanded copilot-instructions.md to enforce strict doc-first and cross-repo hard-gate rules, including per-question doc checks and a no re-read policy with explicit penalties. Broadened consent requirements for all file modifications. Updated context recovery paths. In CLAUDE.md, added sequential execution override and mapped Copilot tool names to Claude Code equivalents, ensuring protocol consistency across tools.
2026-04-04 09:27:49 +02:00
Loretta dbccbf487d Enforce strict AI agent protocol, doc sync, and glossary
- Added "AI AGENT CORE PROTOCOL" to all copilot-instructions.md files: mandates [LOADED_DOCS] prefix, hard-gates tool usage, enforces no-re-read of .md files, and requires user consent for doc/code changes.
- Updated CLAUDE.md to require reading copilot-instructions.md first.
- Added topic-based doc separation and folder navigation rules.
- Changed doc sync: agent now passively detects discrepancies and asks before updating docs.
- Every code-modifying response must end with a [DOCUMENTATION CHECK] section.
- Centralized measurement system and domain traps in new FruitBank.Common/docs/GLOSSARY.md; updated references in FruitBankHybridApp GLOSSARY.md.
- Clarified schema and doc locations in FruitBankHybridApp README.md.
- Added hybrid execution model section to AyCode.Core BINARY_FEATURES.md.
- Removed unnecessary BeginUpdate/EndUpdate calls in MgGridBase.cs for layout persistence.
- Removed full Toon schema from plugin SCHEMA.md to avoid duplication.
2026-04-02 09:02:55 +02:00
Loretta 3f49945bfb Update LLM instruction files for token efficiency and cross-repo navigation
- CLAUDE.md: reduced to single-line pointer to copilot-instructions.md (eliminates redundant auto-loaded content)
- copilot-instructions.md: added @repo name field, relative paths in own-dep-repos, SCHEMA.md first-use read, "do not re-read .md files" rule, and explicit permission to navigate external repos

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-31 16:15:04 +02:00
Loretta 5e4bb4c8e0 Clarify and update copilot instructions and domain docs
- Clarified location of nopCommerce library rules (Mango.Nop Libraries repo)
- Improved FruitBank domain description to specify server-side plugin context
- Refined redundancy guidance to check for similar methods in context
- Updated .md sync instructions: clarify when/how to update docs, code as source of truth, and reviewer guidance
- Simplified descriptions of code locations for core, UI, library, and plugin layers
- Minor formatting and wording improvements for clarity
2026-03-30 10:16:31 +02:00
Loretta 90419001ab Add structured metadata and update doc references
- Introduce @repo and @project metadata blocks in copilot-instructions.md and README.md for all projects, declaring type, dependencies, and layer.
- Update all documentation links to reference canonical AyCode.Core and AyCode.Blazor repo locations, removing hardcoded paths.
- Move MgGridBase and related doc references to AyCode.Blazor.Components/docs/ to reflect repo reorg.
- Expand project READMEs with purpose, key files, and dependency tables for clearer architecture.
- Update glossary, conventions, and architecture docs to match new doc structure and reference locations.
- Apply minor corrections to terminology and file references for consistency and accuracy.
- Standardize documentation and make project relationships explicit.
2026-03-30 08:54:33 +02:00
Loretta e393718c20 Document FruitBankGridBase and update grid conventions
Expanded and clarified grid documentation:
- Grids/README.md now details FruitBankGridBase<TEntity> usage, default settings, and legacy MgGridBase.
- Main README.md and ARCHITECTURE.md reference Grids/README.md for grid details.
- Subfolder table in Grids/README.md includes descriptive notes.
- CONVENTIONS.md: removed outdated layout key section, streamlined code reuse guidance.
- GLOSSARY.md links to canonical MgGrid docs and new grid base docs.
- copilot-instructions.md: clarified .md sync rules and documentation layering.
These changes ensure clear, layered, and non-redundant grid documentation.
2026-03-29 18:30:33 +02:00
Loretta 8c90a6ba51 Document SignalR architecture, grid, and ext deps
Added comprehensive docs for SignalR tag-based dispatch (docs/SIGNALR_ARCHITECTURE.md), including message flow, tag system, dynamic method registry, and tech debt (JSON-in-Binary). Updated all related READMEs, glossaries, and conventions to reference this architecture and clarify grid infrastructure (MgGridBase, FruitBankGridBase) and external dependency locations (AyCode.Core, AyCode.Blazor, Mango.Nop Libraries, FruitBank Plugin). Synchronized solution items and copilot-instructions. Improves discoverability, enforces conventions, and clarifies tech debt for all developers.
2026-03-29 10:43:07 +02:00
Loretta e2c49940c6 Update docs: enforce .md sync, clarify structure & TFMs
Expanded and clarified solution/project documentation:
- Added all top-level docs and docs/ folder as solution items in .sln files
- Inserted maintenance notices in all project and subfolder READMEs: require .md sync with code
- Main READMEs now include project tables with TFM, purpose, and README links
- ARCHITECTURE.md now details dependency graph and TFM rationale
- CONVENTIONS.md and copilot-instructions.md require code reuse, no redundancy
- Glossary and conventions updated to require terminology/rule updates with code changes
- Emphasized DLL-only AyCode.Core refs and nopCommerce .NET 9.0 requirement
- Clarified domain terms and intentional typos
- No code logic changes; documentation and guidance only
2026-03-29 09:26:16 +02:00
Loretta 45195b9cdf Add LLM onboarding docs and standardize project READMEs
- Introduced `.github/copilot-instructions.md` as the single source of truth for domain rules, conventions, and pitfalls in each solution.
- Added `CLAUDE.md` to guide Claude to read domain rules, glossary, and README before code generation.
- Updated all solution and project `README.md` files to document project purpose, structure, key files, and LLM context (Copilot/Claude/Cursor).
- Added or revised `docs/ARCHITECTURE.md`, `docs/CONVENTIONS.md`, and `docs/GLOSSARY.md` to clarify dependency graphs, naming, patterns, and terminology.
- For FruitBankHybridApp, added `docs/SCHEMA.md` (Toon format) and expanded the glossary with business/measurement terms and common traps.
- Updated all subfolder READMEs to list key files, conventions, and LLM maintenance notes.
- Ensured all documentation is cross-referenced, up-to-date, and includes explicit instructions for LLMs to keep docs in sync with code and avoid suggesting removal/rollback as a solution.
- Standardized documentation and onboarding for maintainability and LLM/code quality across all solutions.
2026-03-28 22:38:23 +01:00
Loretta ecd7275cee Update test to serialize type metadata instead of instance
Commented out serialization of FullProcessModel instance and replaced it with serialization of type metadata using SerializeTypeMetadata<FullProcessModel>. This change focuses the test on type metadata serialization rather than instance data.
2026-03-28 17:25:51 +01:00
Loretta c5e841f207 Update ToonTests to serialize type metadata only
Replaced AcToonSerializer.Serialize with SerializeTypeMetadata<FullProcessModel> in ToonTests. The test now serializes only the type metadata for FullProcessModel using the domain description, rather than serializing the actual data instance. Assertions and comments remain unchanged.
2026-03-28 17:25:12 +01:00
Loretta d1c254d5d1 Merge branch 'main' into FruitBank_v0.0.8.0 2026-03-28 16:23:29 +01:00
Loretta 5bd5e14953 Enable source-generated binary serialization & AOT
Added AcBinarySerializable and ToonDescription to DTOs/entities for source-generated serialization. Enabled AOT compilation for Blazor/WebAssembly projects. Integrated AyCode.Core.Serializers.SourceGenerator as analyzer. Updated solution and project files, improved entity metadata, and adjusted imports. Commented out InitializeComponent in WinUI App for startup handling.
2026-03-07 14:05:39 +01:00
Loretta 1b68599acc Enhance file, shipping, stock, and Android deployment
- Added new file metadata fields and improved shipping entities
- Refined weight validation logic in OrderItemDto
- Extended SignalR for stock taking refresh/close operations
- Improved SignalR error handling and UI feedback
- Enhanced grid UI: links, audit info, date formatting, loading panels
- Disabled AOT for WebAssembly/Android; updated Android signing config
- Updated app versioning and SQL schema comparison
- NavMenu: "Leltározás" now admin-only
2026-03-06 14:57:01 +01:00
Loretta 623a01e3e3 Add StockTakings to FullProcessModel and new meta test
Added List<StockTaking> StockTakings to FullProcessModel. Introduced GetMetaInfos test in ToonTests to serialize and output FullProcessModel type metadata for inspection.
2026-02-23 16:10:28 +01:00
Loretta 22bda45ade Add string interning analysis and recent data filtering
- Add using directives for binary and JSON serializers.
- Reformat and clarify Hungarian documentation comments.
- Add GetAnalyzeStringInternCandidatesLog test (DEBUG only) to analyze string interning opportunities in recent orders.
- Filter OrderDtoToToon test data to only include orders and shippings from the last 70 days.
- Improves test relevance and adds diagnostics for serialization efficiency.
2026-01-26 10:23:43 +01:00
Loretta f369491a1d temp 2026-01-20 09:54:28 +01:00
Loretta 9a3817dff0 Improve docs, naming, and reference handling for Toon/DTOs
- Use nameof(Product) for key group consistency and refactor safety
- Expand ToonDescription attributes with business context and legacy notes
- Add and update property-level documentation for measurement logic
- Add tests for AcToonSerializer reference handling (@ref markers)
- Introduce FullProcessModel and TestContainerWithSharedRefs for tests
- Add DomainDescription constant for plugin documentation
- Refine test output and assertions for metadata and navigation completeness
- Minor property description and naming improvements throughout
2026-01-16 09:28:17 +01:00
Loretta dd3c1c58c0 Add ToonDescription metadata to entities and DTOs
Expanded use of the ToonDescription attribute across core entities and DTOs to provide rich metadata, including business rules, purposes, and status flags. Added class-level and property-level annotations to improve self-documentation and support automated tooling. Introduced new computed properties with ToonDescription in ProductDto, StockQuantityHistoryDto, and StockTakingItem. Updated ToonTypeRelation with an Entity constant. Enhanced test coverage for Toon metadata. Cleaned up imports and removed obsolete test_debug.ps1 script. Updated settings.local.json to support additional Bash commands for tooling. These changes improve introspectability and support for serialization, UI, and API documentation.
2026-01-15 11:33:34 +01:00
Loretta 3700bfdb29 Add business-logic metadata to DTO serialization
Introduce "business-logic" field in AcToonSerializer type metadata output, sourced from ToonDescription attributes on DTO properties. Annotate relevant OrderDto and OrderItemDto properties with business rules and constraints. Expand allowed Bash commands in settings.local.json. Add test script to verify business-logic metadata presence. Temporarily disable HasToonIgnoreAttribute logic in JsonUtilities.
2026-01-14 22:16:49 +01:00
Loretta ca186c9e90 Refactor Toon serializer: modularize metadata & relations
Major refactor: split AcToonSerializer.MetaSection.cs into focused modules for meta writing, type/enum definitions, navigation, foreign key, validation, descriptions, placeholders, topological sort, and attribute detection. Extend ToonDescriptionAttribute with BusinessRule, TypeRelation, and RelatedTypes for richer metadata. Add ToonTypeRelation constants. Annotate all DTOs with ToonDescription for type relationships. Refactor TypeMetadataBase for customizable ignore filters. Update tests and settings. Improves maintainability, extensibility, and metadata accuracy.
2026-01-14 15:39:03 +01:00
Loretta 0bb0b06af4 Refactor Toon serializer: robust navigation/type metadata
- Introduce AcNavigationPropertyInfo for unified, cached relationship metadata per property (primary key, navigation type, FK, other key, inverse, target type)
- Refactor relationship detection to use AcNavigationPropertyInfo, replacing ad-hoc logic and old RelationshipMetadata
- Add AcSerializerCommon.GetCSharpTypeName for consistent, C#-style type name formatting (handles primitives, generics, nullables, enums, collections)
- Use topological sort for @types output to ensure dependency order
- Improve enum handling: avoid redundant constraints, use new type name formatter for underlying types
- Output navigation, foreign-key, other-key, and inverse-property metadata consistently in meta section
- Enhance convention-based detection for inverse properties and "other key", including unidirectional/polymorphic support
- Add comprehensive test for navigation metadata completeness and demo test entities
- Add "source-code-language: C#" to meta section
- Misc: code cleanup, remove unused cache, improve property filtering
2026-01-14 08:00:32 +01:00
Loretta 18b119c7a8 Refactor AcToonSerializer metadata extraction & DTO tables
- Remove redundant constraints (nullable, numeric, boolean) from metadata; only explicit [Required] is documented.
- Exclude enum values from constraints; add "readonly" for readonly/init-only properties.
- Filter out primitives from documented types; only complex types and enums are included.
- Detect and document enum backing fields with "enum-type" constraint.
- Only output descriptions if explicitly provided; no fallback/inferred text.
- Add "not-mapped" constraint for [NotMapped]/[NotColumn] properties.
- Switch FruitBankHybrid.Shared.Tests.csproj to direct AyCode.Core project reference.
- Add both LinqToDB and DataAnnotations [Table] attributes to DTOs for ORM compatibility.
2026-01-13 08:25:28 +01:00
Loretta 6d689d3632 Improve AcToonSerializer type metadata handling & tests
- Refined type metadata serialization: collections and dictionaries are now detected and described more accurately, avoiding generic type names (e.g., List`1) and redundant "object" element types.
- Added circular reference protection to type name generation to prevent stack overflows and duplicate type names.
- Updated AcToonSerializerOptions.Compact to use indentation for better readability.
- Introduced ToonTests with unit tests to ensure type metadata correctness, uniqueness, and clarity.
- Added AyCode.Core project to the solution and adjusted namespaces/usings for consistency.
2026-01-12 08:36:23 +01:00
Loretta 38f268ec1d Refactor SignalR hub registration and background tasks
- Use Forget() extension for fire-and-forget tasks in MgGridSignalRDataSource
- Remove legacy commented DevAdminSignalRHub code
- Switch to DynamicMethodRegistry for hub method registration, improving startup performance by deferring reflection
- Add clarifying comment for remaining legacy message handling code
2026-01-06 08:55:38 +01:00
Loretta 10eea9e70c Add LINQ Expression JSON serialization & SignalR grid source
- Introduce AcExpressionHelper and related classes for serializing/deserializing LINQ Expression trees and IQueryable queries to/from JSON, enabling remote transport and execution.
- Add MgGridSignalRDataSource<TDataItem, TId> for instant local filtering and background refresh in DevExpress grids using SignalR.
- Update bunit NuGet package to v2.4.2 in test projects.
- Minor: update FruitBank base URL comment, add new test in OrderClientTests.
2025-12-30 19:29:50 +01:00
98 changed files with 2338 additions and 291 deletions

View File

@ -3,7 +3,15 @@
"allow": [
"Bash(dir:*)",
"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)",
"Bash(rm \"C:/Users/Fullepi/.claude/projects/H--Applications-Mango-Source-FruitBankHybridApp/memory/feedback_framework_docs_no_consumer_types.md\")"
]
}
}

181
.github/copilot-instructions.md vendored Normal file

File diff suppressed because one or more lines are too long

17
CLAUDE.md Normal file
View File

@ -0,0 +1,17 @@
CRITICAL: Your FIRST action in every session MUST be reading `.github/copilot-instructions.md`. Execute ALL session-start instructions found there before responding to any user query. It is the single source of truth for this repo.
## SEQUENTIAL EXECUTION OVERRIDE
The AI AGENT CORE PROTOCOL in copilot-instructions.md requires STRICT SEQUENTIAL execution. This OVERRIDES your default parallelization behavior. Do NOT parallelize doc reads with code searches. The sequence is:
1. Read copilot-instructions.md → process its rules FULLY
2. Read ALL docs/ .md files listed in the protocol → wait for completion
3. Output [LOADED_DOCS: ...] prefix
4. ONLY THEN respond to the user's query or search code
## Tool mapping for AI AGENT CORE PROTOCOL
The copilot-instructions.md references Copilot tool names. Map them to Claude Code tools:
- `get_file` / `file_search``Read`, `Glob`, `Grep`
- `code_search` / `get_symbols_by_name` / `find_symbol``Grep`, `Glob`
- `replace_string_in_file` / `edit_file``Edit`
- `create_file``Write`
Follow the protocol using YOUR tools. The rules (LOADED_DOCS prefix, hard-gate, no-re-read, context recovery, explicit consent) apply equally to Claude Code.

View File

@ -0,0 +1,9 @@
# 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.

View File

@ -0,0 +1,28 @@
# FruitBank.Common.Server
@project {
type = "product"
own-dep-projects = [
"AyCode.Core, AyCode.Interfaces, AyCode.Models.Server, AyCode.Services, AyCode.Services.Server (in AyCode.Core repo)",
"Mango.Nop.Core (in Mango.Nop Libraries repo)"
]
}
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

View File

@ -0,0 +1,8 @@
# 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.

View File

@ -0,0 +1,10 @@
# 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 |

View File

@ -13,22 +13,6 @@ using Microsoft.Extensions.Configuration;
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 DevAdminSignalRHub(IConfiguration configuration, IFruitBankDataControllerServer fruitBankDataController/*, SessionService sessionService*/,
@ -37,11 +21,13 @@ public class DevAdminSignalRHub : AcWebSignalRHubWithSessionBase<SignalRTags, Lo
{
EnableBinaryDiagnostics = FruitBankConstClient.SignalRSerializerDiagnosticLog;
SerializerOptions = new AcBinarySerializerOptions();
//SerializerOptions = new AcJsonSerializerOptions();
DynamicMethodCallModels.Add(new AcDynamicMethodCallModel<SignalRAttribute>(fruitBankDataController));
DynamicMethodCallModels.Add(new AcDynamicMethodCallModel<SignalRAttribute>(customOrderSignalREndpoint));
DynamicMethodCallModels.Add(new AcDynamicMethodCallModel<SignalRAttribute>(stockSignalREndpointServer));
// Use the new lazy Registry - no reflection at construction time
DynamicMethodRegistry.CahcheSizeCapacity = 3;
DynamicMethodRegistry.Register(fruitBankDataController);
DynamicMethodRegistry.Register(customOrderSignalREndpoint);
DynamicMethodRegistry.Register(stockSignalREndpointServer);
}
protected override void LogContextUserNameAndId()
@ -49,83 +35,5 @@ public class DevAdminSignalRHub : AcWebSignalRHubWithSessionBase<SignalRTags, Lo
return;
base.LogContextUserNameAndId();
}
//public override Task OnReceiveMessage(int messageTag, byte[]? message, int? requestId)
//{
// 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;
// }
// });
//}
// ...existing commented code...
}

View File

@ -0,0 +1,10 @@
# 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.

View File

@ -0,0 +1,7 @@
# 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.

View File

@ -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;
[AcBinarySerializable(false, true, false, true, false, false)]
[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
{

View File

@ -1,4 +1,6 @@
using AyCode.Core.Extensions;
using AyCode.Core.Serializers.Attributes;
using AyCode.Core.Serializers.Toons;
using AyCode.Utils.Extensions;
using FruitBank.Common.Entities;
using FruitBank.Common.Enums;
@ -20,6 +22,10 @@ using System.Linq.Expressions;
namespace FruitBank.Common.Dtos;
[AcBinarySerializable(false, true, false, true, false, false)]
[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
{
[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; }
[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
{
get => IsMeasuredAndValid();
@ -38,6 +45,7 @@ public class OrderDto : MgOrderDto<OrderItemDto, ProductDto>, IOrderDto
}
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
[ToonDescription(BusinessRule = "get => OrderItemDtos.Any(oi => oi.IsMeasurable)", Constraints = "[#SmartTypeConstraints], readonly")]
public bool IsMeasurable
{
get => OrderItemDtos.Any(oi => oi.IsMeasurable);
@ -65,18 +73,23 @@ public class OrderDto : MgOrderDto<OrderItemDto, ProductDto>, IOrderDto
public DateTime DateOfReceiptOrCreated => DateOfReceipt ?? CreatedOnUtc;
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
[ToonDescription(BusinessRule = "get => GenericAttributes.GetValueOrNull<DateTime>('DateOfReceipt')")]
public DateTime? DateOfReceipt => GenericAttributes.GetValueOrNull<DateTime>(nameof(IOrderDto.DateOfReceipt));
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
[ToonDescription(BusinessRule = "get => GenericAttributes.GetValueOrDefault('RevisorId', 0)")]
public int RevisorId => GenericAttributes.GetValueOrDefault(nameof(IOrderDto.RevisorId), 0);
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
[ToonDescription(BusinessRule = "get => GenericAttributes.GetValueOrDefault('MeasurementOwnerId', 0)")]
public int MeasurementOwnerId => GenericAttributes.GetValueOrDefault(nameof(IOrderDto.MeasurementOwnerId), 0);
[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);
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
[ToonDescription(BusinessRule = "get => OrderItemDtos.All(oi => oi.AverageWeightIsValid)")]
public bool IsAllOrderItemAvgWeightValid => OrderItemDtos.All(oi => oi.AverageWeightIsValid);
[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]
[ToonDescription(BusinessRule = "get => OrderStatus == OrderStatus.Complete")]
public bool IsComplete => OrderStatus == OrderStatus.Complete;
public bool HasMeasuringAccess(int? customerId, bool isRevisorUser = false)

View File

@ -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.Interfaces;
using LinqToDB.Mapping;
@ -14,6 +16,10 @@ using System.Linq.Expressions;
namespace FruitBank.Common.Dtos;
[AcBinarySerializable(false, true, false, true, false, false)]
[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
{
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
@ -31,6 +37,7 @@ public class OrderItemDto : MgOrderItemDto<ProductDto>, IOrderItemDto
public OrderDto OrderDto { get; set; }
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
[ToonDescription(Purpose = "Status flag", BusinessRule = "get => IsMeasuredAndValid()")]
public bool IsMeasured
{
get => IsMeasuredAndValid();
@ -38,6 +45,7 @@ public class OrderItemDto : MgOrderItemDto<ProductDto>, IOrderItemDto
}
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
[ToonDescription(Purpose = "Status flag", BusinessRule = "get => ProductDto!.IsMeasurable")]
public bool IsMeasurable
{
get => ProductDto!.IsMeasurable;
@ -45,6 +53,7 @@ public class OrderItemDto : MgOrderItemDto<ProductDto>, IOrderItemDto
}
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
[ToonDescription(BusinessRule = "get => OrderItemPallets.Sum(x => x.TrayQuantity)")]
public int 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]
[ToonDescription(BusinessRule = "get => double.Round(OrderItemPallets.Sum(x => x.NetWeight), 1)")]
public double NetWeight
{
get
@ -69,6 +79,7 @@ public class OrderItemDto : MgOrderItemDto<ProductDto>, IOrderItemDto
}
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
[ToonDescription(BusinessRule = "get => double.Round(OrderItemPallets.Sum(x => x.NetWeight), 1)")]
public double GrossWeight
{
get
@ -86,18 +97,23 @@ public class OrderItemDto : MgOrderItemDto<ProductDto>, IOrderItemDto
}
[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;
[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;
[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);
[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);
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
[ToonDescription(BusinessRule = "get => complex conditional logic based on IsAudited, IsMeasured, and OrderItemPallets status")]
public MeasuringStatus MeasuringStatus
{
get

View File

@ -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 Mango.Nop.Core.Dtos;
using Mango.Nop.Core.Extensions;
using Mango.Nop.Core.Interfaces.ForeignKeys;
using Newtonsoft.Json;
using Nop.Core.Domain.Catalog;
//using Nop.Core.Domain.Catalog;
using Nop.Core.Domain.Common;
using Nop.Core.Domain.Orders;
using System.ComponentModel.DataAnnotations.Schema;
using System.Linq.Expressions;
namespace FruitBank.Common.Dtos;
[AcBinarySerializable(false, true, false, true, false, false)]
[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
{
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
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)]
@ -29,6 +38,7 @@ public class ProductDto : MgProductDto, IProductDto
//{ }
[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
{
get => GenericAttributes.GetValueOrDefault<bool>(nameof(IMeasurable.IsMeasurable));
@ -43,6 +53,7 @@ public class ProductDto : MgProductDto, IProductDto
}
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
[ToonDescription(BusinessRule = "get => GenericAttributes.GetValueOrDefault<double>('Tare')")]
public double 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]
[ToonDescription(BusinessRule = "get => GenericAttributes.GetValueOrDefault<double>('NetWeight')")]
public double 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]
[ToonDescription(BusinessRule = "get => GenericAttributes.GetValueOrDefault<int>('IncomingQuantity')")]
public int IncomingQuantity
{
get => GenericAttributes.GetValueOrDefault<int>(nameof(IIncomingQuantity.IncomingQuantity));
@ -65,19 +78,22 @@ public class ProductDto : MgProductDto, IProductDto
//set
//{
// 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();
//}
}
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
[ToonDescription(BusinessRule = "get => StockQuantity + IncomingQuantity")]
public int AvailableQuantity => StockQuantity + IncomingQuantity;
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
[ToonDescription(BusinessRule = "get => GenericAttributes.GetValueOrDefault<double>('AverageWeight')")]
public double AverageWeight => GenericAttributes.GetValueOrDefault<double>(nameof(IProductDto.AverageWeight));
[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 bool HasMeasuringValues() => Id > 0 && NetWeight != 0 && IsMeasurable;

View File

@ -0,0 +1,15 @@
# 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.

View File

@ -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 Mango.Nop.Core.Dtos;
using Mango.Nop.Core.Entities;
using Mango.Nop.Core.Interfaces;
using Newtonsoft.Json;
using Nop.Core.Domain.Catalog;
using Nop.Core.Domain.Common;
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using FruitBank.Common.Entities;
namespace FruitBank.Common.Dtos
{
[AcBinarySerializable(false, true, false, true, false, false)]
[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
{
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
[ToonDescription(BusinessRule = "get => StockQuantityHistoryExt?.StockQuantityHistoryId")]
public int? StockQuantityHistoryId
{
get => StockQuantityHistoryExt?.StockQuantityHistoryId;
@ -24,6 +33,7 @@ namespace FruitBank.Common.Dtos
}
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
[ToonDescription(BusinessRule = "get => StockQuantityHistoryExt?.NetWeightAdjustment")]
public double? NetWeightAdjustment
{
get => StockQuantityHistoryExt?.NetWeightAdjustment;
@ -31,6 +41,7 @@ namespace FruitBank.Common.Dtos
}
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
[ToonDescription(BusinessRule = "get => StockQuantityHistoryExt?.NetWeight")]
public double? NetWeight
{
get => StockQuantityHistoryExt?.NetWeight;
@ -38,6 +49,7 @@ namespace FruitBank.Common.Dtos
}
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
[ToonDescription(Purpose = "Status flag", BusinessRule = "get => StockQuantityHistoryExt?.IsInconsistent ?? false")]
public bool IsInconsistent
{
get => StockQuantityHistoryExt?.IsInconsistent ?? false;

View File

@ -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 Mango.Nop.Core.Entities;
namespace FruitBank.Common.Entities;
[AcBinarySerializable(false, true, false, true, false, false)]
[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)]
[System.ComponentModel.DataAnnotations.Schema.Table(FruitBankConstClient.FilesDbTableName)]
public class Files : MgEntityBase, IFiles

View File

@ -1,3 +1,5 @@
using AyCode.Core.Serializers.Attributes;
using AyCode.Core.Serializers.Toons;
using FruitBank.Common.Dtos;
using FruitBank.Common.Enums;
using FruitBank.Common.Interfaces;
@ -8,6 +10,8 @@ using Newtonsoft.Json;
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
{
private double _palletWeight;
@ -16,9 +20,12 @@ public abstract class MeasuringItemPalletBase : MgEntityBase, IMeasuringItemPall
[NotColumn]
protected int ForeignItemId;
[NotColumn]
[ToonDescription(BusinessRule = "get => ForeignItemId", Constraints = "[#SmartTypeConstraints]")]
public int ForeignKey => ForeignItemId;
[ToonDescription(Purpose = "Always recorded, regardless of measurability")]
public int TrayQuantity { get; set; }
[Column(DataType = DataType.DecFloat)]
@ -29,6 +36,7 @@ public abstract class MeasuringItemPalletBase : MgEntityBase, IMeasuringItemPall
}
[Column(DataType = DataType.DecFloat)]
[ToonDescription(Purpose = "Weight of the physical pallet if used; 0.0 if goods arrive without a pallet")]
public double 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]
[ToonDescription(BusinessRule = "get => GrossWeight - PalletWeight - (TrayQuantity * TareWeight)", Constraints = "[#SmartTypeConstraints], readonly")]
public double NetWeight
{
get => CalculateNetWeight();
@ -43,6 +52,7 @@ public abstract class MeasuringItemPalletBase : MgEntityBase, IMeasuringItemPall
}
[Column(DataType = DataType.DecFloat, CanBeNull = false)]
[ToonDescription(Purpose = "Measured gross weight; 0.0 if product is not measurable")]
public double GrossWeight
{
get => _grossWeight;
@ -60,6 +70,7 @@ public abstract class MeasuringItemPalletBase : MgEntityBase, IMeasuringItemPall
public DateTime Modified { get; set; }
[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;

View File

@ -1,3 +1,5 @@
using AyCode.Core.Serializers.Attributes;
using AyCode.Core.Serializers.Toons;
using FruitBank.Common.Dtos;
using FruitBank.Common.Enums;
using FruitBank.Common.Interfaces;
@ -10,6 +12,8 @@ using Table = LinqToDB.Mapping.TableAttribute;
namespace FruitBank.Common.Entities;
[AcBinarySerializable(false, true, false, true, false, false)]
[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)]
[System.ComponentModel.DataAnnotations.Schema.Table(FruitBankConstClient.OrderItemPalletDbTableName)]
public class OrderItemPallet : MeasuringItemPalletBase, IOrderItemPallet
@ -20,7 +24,11 @@ public class OrderItemPallet : MeasuringItemPalletBase, IOrderItemPallet
set => ForeignItemId = value;
}
[ToonDescription(Purpose = "User/Customer ID of the quality auditor")]
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;
//[JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
@ -28,6 +36,7 @@ public class OrderItemPallet : MeasuringItemPalletBase, IOrderItemPallet
public OrderItemDto? OrderItemDto { get; set; }
[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 double CalculateNetWeight() => base.CalculateNetWeight();
@ -37,6 +46,7 @@ public class OrderItemPallet : MeasuringItemPalletBase, IOrderItemPallet
}
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
[ToonDescription(BusinessRule = "get => double.Round(NetWeight / TrayQuantity, 1)")]
public double AverageWeight => double.Round(NetWeight / TrayQuantity, 1);
/// <summary>

View File

@ -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.Mapping;
using Mango.Nop.Core.Entities;
namespace FruitBank.Common.Entities;
[AcBinarySerializable(false, true, false, true, false, false)]
[ToonDescription("Pallet type definition with size and weight")]
[Table(Name = FruitBankConstClient.PalletDbTableName)]
[System.ComponentModel.DataAnnotations.Schema.Table(FruitBankConstClient.PalletDbTableName)]
public class Pallet : MgEntityBase, IPallet

View File

@ -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 Mango.Nop.Core.Entities;
namespace FruitBank.Common.Entities;
[AcBinarySerializable(false, true, false, true, false, false)]
[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)]
[System.ComponentModel.DataAnnotations.Schema.Table(FruitBankConstClient.PartnerDbTableName)]
public class Partner : MgEntityBase, IPartner

View File

@ -0,0 +1,33 @@
# 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.

View File

@ -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 LinqToDB.Mapping;
using Mango.Nop.Core.Entities;
namespace FruitBank.Common.Entities;
[AcBinarySerializable(false, true, false, true, false, false)]
[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)]
[System.ComponentModel.DataAnnotations.Schema.Table(FruitBankConstClient.ShippingDbTableName)]
public class Shipping : MgEntityBase, IShipping, IEntityComment

View File

@ -1,10 +1,14 @@
using System.Collections.ObjectModel;
using AyCode.Core.Serializers.Attributes;
using AyCode.Core.Serializers.Toons;
using FruitBank.Common.Interfaces;
using LinqToDB.Mapping;
using Mango.Nop.Core.Entities;
using System.Collections.ObjectModel;
namespace FruitBank.Common.Entities;
[AcBinarySerializable(false, true, false, true, false, false)]
[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)]
[System.ComponentModel.DataAnnotations.Schema.Table(FruitBankConstClient.ShippingDocumentDbTableName)]
public class ShippingDocument : MgEntityBase, IShippingDocument

View File

@ -1,21 +1,28 @@
using System.ComponentModel.DataAnnotations.Schema;
using AyCode.Core.Serializers.Attributes;
using AyCode.Core.Serializers.Toons;
using FruitBank.Common.Interfaces;
using LinqToDB.Mapping;
using Mango.Nop.Core.Entities;
using Newtonsoft.Json;
using System.ComponentModel.DataAnnotations.Schema;
namespace FruitBank.Common.Entities;
[AcBinarySerializable(false, true, false, true, false, false)]
[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)]
[System.ComponentModel.DataAnnotations.Schema.Table(FruitBankConstClient.ShippingDocumentToFilesDbTableName)]
public class ShippingDocumentToFiles : MgEntityBase, IShippingDocumentToFiles
{
public int FilesId { get; set; }
public int ShippingDocumentId { get; set; }
[ToonDescription(Constraints = "enum-reference: DocumentType")]
public int DocumentTypeId { get; set; }
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
[ToonDescription(Purpose = "Enum wrapper", BusinessRule = "get, set => DocumentTypeId")]
public DocumentType DocumentType
{
get => (DocumentType)DocumentTypeId;

View File

@ -1,4 +1,6 @@
using AyCode.Core.Interfaces;
using AyCode.Core.Serializers.Attributes;
using AyCode.Core.Serializers.Toons;
using FruitBank.Common.Dtos;
using FruitBank.Common.Enums;
using FruitBank.Common.Interfaces;
@ -10,13 +12,15 @@ using Nop.Core.Domain.Customers;
using Nop.Core.Domain.Orders;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using Column = LinqToDB.Mapping.ColumnAttribute;
//using Nop.Core.Domain.Catalog;
using DataType = LinqToDB.DataType;
using Column = LinqToDB.Mapping.ColumnAttribute;
using Table = LinqToDB.Mapping.TableAttribute;
namespace FruitBank.Common.Entities;
[AcBinarySerializable(false, true, false, true, false, false)]
[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)]
[System.ComponentModel.DataAnnotations.Schema.Table(FruitBankConstClient.ShippingItemDbTableName)]
public class ShippingItem : MgEntityBase, IShippingItem
@ -29,10 +33,8 @@ public class ShippingItem : MgEntityBase, IShippingItem
public string NameOnDocument { get; set; }
public string HungarianName { get; set; }
/// <summary>
/// get => ProductDto?.Name ?? Name
/// </summary>
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
[ToonDescription(BusinessRule = "get => ProductDto?.Name ?? Name")]
public string ProductName => ProductDto?.Name ?? Name;
public int PalletsOnDocument { get; set; }
@ -84,6 +86,7 @@ public class ShippingItem : MgEntityBase, IShippingItem
public DateTime Modified { get; set; }
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
[ToonDescription(BusinessRule = "get => complex conditional logic based on IsMeasured and ShippingItemPallets status")]
public MeasuringStatus MeasuringStatus
{
get

View File

@ -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 System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
@ -6,6 +8,8 @@ using System.Security.Cryptography.X509Certificates;
namespace FruitBank.Common.Entities;
[AcBinarySerializable(false, true, false, true, false, false)]
[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)]
[System.ComponentModel.DataAnnotations.Schema.Table(FruitBankConstClient.ShippingItemPalletDbTableName)]
public class ShippingItemPallet : MeasuringItemPalletBase, IShippingItemPallet

View File

@ -1,17 +1,11 @@
using AyCode.Interfaces.Entities;
using AyCode.Interfaces.TimeStampInfo;
using FruitBank.Common;
using FruitBank.Common.Interfaces;
using AyCode.Core.Serializers.Attributes;
using AyCode.Core.Serializers.Toons;
using AyCode.Interfaces.Entities;
using LinqToDB;
using LinqToDB.Mapping;
using Nop.Core.Domain.Catalog;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Mango.Nop.Core.Entities;
namespace Mango.Nop.Core.Entities
namespace FruitBank.Common.Entities
{
public interface IStockQuantityHistoryExt : IEntityInt
{
@ -21,8 +15,10 @@ namespace Mango.Nop.Core.Entities
public bool IsInconsistent { get; set; }
}
[AcBinarySerializable(false, true, false, true, false, false)]
[Table(Name = 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 int StockQuantityHistoryId { get; set; }

View File

@ -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;
namespace FruitBank.Common.Entities;
[AcBinarySerializable(false, true, false, true, false, false)]
[ToonDescription("Inventory session record", Purpose = "Orchestrates inventory sessions by freezing logical stock states")]
[Table(Name = FruitBankConstClient.StockTakingDbTableName)]
[System.ComponentModel.DataAnnotations.Schema.Table(FruitBankConstClient.StockTakingDbTableName)]
public class StockTaking : MgStockTaking<StockTakingItem>

View File

@ -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.Mapping;
using Mango.Nop.Core.Entities;
using System.ComponentModel.DataAnnotations.Schema;
using Newtonsoft.Json;
using System.ComponentModel.DataAnnotations.Schema;
using Column = LinqToDB.Mapping.ColumnAttribute;
using Table = LinqToDB.Mapping.TableAttribute;
namespace FruitBank.Common.Entities;
[AcBinarySerializable(false, true, false, true, false, false)]
[ToonDescription("Line item for product reconciliation", Purpose = "Reconciles snapshot quantity with physical count to calculate final stock delta")]
[Table(Name = FruitBankConstClient.StockTakingItemDbTableName)]
[System.ComponentModel.DataAnnotations.Schema.Table(FruitBankConstClient.StockTakingItemDbTableName)]
public class StockTakingItem : MgStockTakingItem<StockTaking, ProductDto>
@ -21,27 +25,34 @@ public class StockTakingItem : MgStockTakingItem<StockTaking, ProductDto>
[Column(DataType = DataType.DecFloat, CanBeNull = false)]
public double MeasuredNetWeight { get; set; }
[ToonDescription(Purpose = "Reserved stock buffer (not yet shipped) to prevent double-deduction during closing")]
public int InProcessOrdersQuantity { get; set; }
[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;
[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;
[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;
[Association(ThisKey = nameof(Id), OtherKey = nameof(StockTakingItemPallet.StockTakingItemId), CanBeNull = true)]
public List<StockTakingItemPallet>? StockTakingItemPallets { get; set; }
[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);
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
[ToonDescription(Purpose = "Status flag", BusinessRule = "get => TotalOriginalQuantity < 0")]
public bool IsInvalid => TotalOriginalQuantity < 0;
[NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore]
[ToonDescription(BusinessRule = "get => conditional string based on IsInvalid, IsMeasured, IsRequiredForMeasuring")]
public string DisplayText
{
get

View File

@ -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 LinqToDB.Mapping;
using Mango.Nop.Core.Entities;
@ -12,6 +14,8 @@ public interface IStockTakingItemPallet : IMeasuringItemPalletBase
public StockTakingItem? StockTakingItem{ get; set; }
}
[AcBinarySerializable(false, true, false, true, false, false)]
[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)]
[System.ComponentModel.DataAnnotations.Schema.Table(FruitBankConstClient.StockTakingItemPalletDbTableName)]
public class StockTakingItemPallet : MeasuringItemPalletBase, IStockTakingItemPallet

View File

@ -0,0 +1,7 @@
# 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.

View File

@ -5,6 +5,7 @@
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
</PropertyGroup>
<ItemGroup>
@ -39,4 +40,10 @@
</Reference>
</ItemGroup>
</Project>
<ItemGroup>
<ProjectReference Include="..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Core.Serializers.SourceGenerator\AyCode.Core.Serializers.SourceGenerator.csproj"
OutputItemType="Analyzer"
ReferenceOutputAssembly="false" />
</ItemGroup>
</Project>

View File

@ -44,11 +44,12 @@ public static class FruitBankConstClient
public const string StockTakingDbTableName = "fbStockTaking";
public const string StockTakingItemDbTableName = "fbStockTakingItem";
public const string StockTakingItemPalletDbTableName = "fbStockTakingItemPallet";
public const string CustomerCreditDbTableName = "fbCustomerCredit";
public const string PreOrderDbTableName = "fbPreorder";
public const string PreOrderItemDbTableName = "fbPreorderItem";
public const string DomainDescription = "This is a nopCommerce plugin developed for FruitBank, a fruit and vegetable wholesaler. The plugin manages supplier inbound delivery (receiving), warehouse weighing (net/gross/pallet/tare weights), and inventory stocktaking. The business logic is centered around FruitBank's requirement for precise physical measurement and quantity tracking.";
//public static Guid[] DevAdminIds = new Guid[2] { Guid.Parse("dcf451d2-cc4c-4ac2-8c1f-da00041be1fd"), Guid.Parse("4cbaed43-2465-4d99-84f1-c8bc6b7025f7") };
//public static Guid[] SysAdmins = new Guid[3]

View File

@ -0,0 +1,9 @@
# 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.

View File

@ -7,6 +7,7 @@ using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using FruitBank.Common.Entities;
namespace FruitBank.Common.Interfaces
{

View File

@ -0,0 +1,29 @@
# 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

View File

@ -0,0 +1,7 @@
# Loggers
SignalR client-to-server log writer.
## Key Files
- **`SignaRClientLogItemWriter.cs`** — Routes client logs to `{BaseUrl}/loggerHub` via SignalR. Configurable by AppType and LogLevel.

View File

@ -0,0 +1,13 @@
# 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.

View File

@ -0,0 +1,38 @@
# FruitBank.Common
@project {
type = "product"
own-dep-projects = [
"AyCode.Core, AyCode.Entities, AyCode.Interfaces, AyCode.Models, AyCode.Services, AyCode.Utils (in AyCode.Core repo)",
"Mango.Nop.Core (in Mango.Nop Libraries repo)"
]
}
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` for full terminology

View File

@ -0,0 +1,10 @@
# 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.

View File

@ -0,0 +1,19 @@
# 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

View File

@ -0,0 +1,40 @@
# FruitBank.Common Domain Rules & Glossary
> This file acts as the single source of truth for the core Measurement System and Common Traps shared across all FruitBank applications (Hybrid App, Blazor, and nopCommerce server plugin).
## 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** | ShippingItem | ShippingItemPallet | Declared vs measured discrepancy |
| **Outbound** | OrderItemDto | OrderItemPallet | RevisorId for audit |
| **Inventory**| StockTakingItem | StockTakingItemPallet | QuantityDiff for stock adjustment |
## 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/settable | ❌ It is calculated. The setter throws an Exception! It only exists to satisfy the `IMeasuringItemPalletBase` interface boundary. Set `GrossWeight`, `PalletWeight`, `TareWeight` instead. |
| Setting MeasuringStatus | ❌ It's a calculated property (evaluates `IsMeasured`, `Id`, or child pallets). Do not try to set it. |
| Setting ForeignKey | ❌ `ForeignKey` is read-only. Use `SetForeignKey(id)` method instead. |
| GenericAttribute is simple | ❌ It's polymorphic: KeyGroup determines which entity type owns the record |

View File

@ -0,0 +1,15 @@
# FruitBank.Common documentation
Topic documentation for the `FruitBank.Common` project (shared types across Hybrid client).
## Reference docs (flat)
- [`GLOSSARY.md`](GLOSSARY.md) — Common domain terms for the Hybrid client side
## Navigation
Per the AI Agent Core Protocol (folder navigation rule), start from this README when browsing `docs/`. Currently only single-file reference.
## See also
- **Repo-level glossary**: `../../docs/GLOSSARY.md`

View File

@ -0,0 +1,7 @@
# FruitBankHybrid.Shared.Common
@project {
type = "product"
}
Shared common library. Currently a placeholder — no source files yet. .NET 10.0 with AOT enabled.

View File

@ -31,11 +31,7 @@ namespace FruitBankHybrid.Shared.Tests
{
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))
});
_signalRClient = TestSignalRClientFactory.Create(nameof(FruitBankClientTests));
}
#region Partner

View File

@ -15,20 +15,18 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="bunit" Version="2.2.2" />
<PackageReference Include="bunit" Version="2.4.2" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Core\AyCode.Core.csproj" />
<ProjectReference Include="..\FruitBank.Common\FruitBank.Common.csproj" />
<ProjectReference Include="..\FruitBankHybrid.Shared.Common\FruitBankHybrid.Shared.Common.csproj" />
<ProjectReference Include="..\FruitBankHybrid.Shared\FruitBankHybrid.Shared.csproj" />
</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">
<HintPath>..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Services.Server\bin\FruitBank\Debug\net9.0\AyCode.Entities.dll</HintPath>
</Reference>

View File

@ -190,10 +190,7 @@ public sealed class JsonExtensionTests
{
if (!FruitBankConstClient.BaseUrl.Contains("localhost:")) throw new Exception("NEM LOCALHOST-ON TESZTELÜNK!");
_signalRClient = new FruitBankSignalRClient(new List<IAcLogWriterClientBase>
{
new SignaRClientLogItemWriter(AppType.TestUnit, LogLevel.Detail, nameof(FruitBankClientTests))
});
_signalRClient = TestSignalRClientFactory.Create(nameof(FruitBankClientTests));
}
[TestMethod]

View File

@ -3,14 +3,16 @@ using AyCode.Core.Extensions;
using AyCode.Core.Loggers;
using FruitBank.Common;
using FruitBank.Common.Dtos;
using FruitBank.Common.Entities;
using FruitBank.Common.Loggers;
using FruitBankHybrid.Shared.Services.SignalRs;
using Newtonsoft.Json;
using Nop.Core.Domain.Common;
using Nop.Core.Domain.Orders;
using Nop.Core.Domain.Payments;
using System.Linq.Expressions;
using System.Runtime.Serialization;
using FruitBank.Common.Entities;
using Nop.Core.Domain.Common;
using AyCode.Core.Serializers.Toons;
namespace FruitBankHybrid.Shared.Tests;
@ -26,11 +28,7 @@ public sealed class OrderClientTests
{
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))
});
_signalRClient = TestSignalRClientFactory.Create(nameof(OrderClientTests));
}
@ -77,6 +75,18 @@ public sealed class OrderClientTests
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]
[DataRow(1)]
[DataRow(2)]

View File

@ -0,0 +1,29 @@
# FruitBankHybrid.Shared.Tests
@project {
type = "test"
own-dep-projects = [
"AyCode.Entities, AyCode.Services, AyCode.Utils (in AyCode.Core repo)",
"Mango.Nop.Core, Mango.Nop.Services (in Mango.Nop Libraries repo)"
]
}
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).

View File

@ -15,8 +15,8 @@ namespace FruitBankHybrid.Shared.Tests;
/// <summary>
/// Teszt a TestSignalREndpoint-hoz.
/// FONTOS: A SANDBOX-ot manuálisan kell elindítani a tesztek futtatása előtt!
/// Indítás: dotnet run --project Mango.Sandbox.EndPoints --urls http://localhost:59579
/// FONTOS: A SANDBOX-ot manu<EFBFBD>lisan kell elind<6E>tani a tesztek futtat<61>sa el<65>tt!
/// Ind<EFBFBD>t<EFBFBD>s: dotnet run --project Mango.Sandbox.EndPoints --urls http://localhost:59579
/// </summary>
[TestClass]
public class SandboxEndpointSimpleTests
@ -24,7 +24,7 @@ public class SandboxEndpointSimpleTests
private static readonly string SandboxUrl = FruitBankConstClient.BaseUrl; //"http://localhost:59579";
private static readonly string HubUrl = $"{SandboxUrl}/fbHub";
// Teszt SignalR Tags (TestSignalRTags-ből)
// Teszt SignalR Tags (TestSignalRTags-b<EFBFBD>l)
private const int PingTag = SignalRTags.PingTag;
private const int EchoTag = SignalRTags.EchoTag;
private const int GetTestItemsTag = 9003;
@ -34,13 +34,9 @@ public class SandboxEndpointSimpleTests
[TestInitialize]
public void TestInit()
{
if (!SandboxUrl.Contains("localhost:")) throw new Exception("NEM LOCALHOST-ON TESZTELÜNK!");
if (!SandboxUrl.Contains("localhost:")) throw new Exception("NEM LOCALHOST-ON TESZTEL<EFBFBD>NK!");
_signalRClient = new FruitBankSignalRClient(new List<IAcLogWriterClientBase>
{
//new ConsoleLogWriter(AppType.TestUnit, LogLevel.Detail, nameof(FruitBankClientTests)),
new SignaRClientLogItemWriter(AppType.TestUnit, LogLevel.Detail, nameof(SandboxEndpointSimpleTests))
});
_signalRClient = TestSignalRClientFactory.Create(nameof(SandboxEndpointSimpleTests));
}
#region HTTP Endpoint Tests
@ -121,7 +117,7 @@ public class SandboxEndpointSimpleTests
// using var jsonDoc = JsonDocument.Parse(response);
// var root = jsonDoc.RootElement;
// // Ellenőrizzük, hogy van Message property
// // Ellen<EFBFBD>rizz<EFBFBD>k, hogy van Message property
// Assert.IsTrue(root.TryGetProperty("Message", out var messageElement) ||
// root.TryGetProperty("message", out messageElement),
// "Response should contain 'Message' property");
@ -141,13 +137,13 @@ public class SandboxEndpointSimpleTests
// using var jsonDoc = JsonDocument.Parse(response);
// var root = jsonDoc.RootElement;
// // Ellenőrizzük az Id-t
// // Ellen<EFBFBD>rizz<EFBFBD>k az Id-t
// Assert.IsTrue(root.TryGetProperty("Id", out var idElement) ||
// root.TryGetProperty("id", out idElement),
// "Response should contain 'Id' property");
// Assert.AreEqual(42, idElement.GetInt32(), "Id should be 42");
// // Ellenőrizzük a Name-et
// // Ellen<EFBFBD>rizz<EFBFBD>k a Name-et
// Assert.IsTrue(root.TryGetProperty("Name", out var nameElement) ||
// root.TryGetProperty("name", out nameElement),
// "Response should contain 'Name' property");
@ -167,13 +163,13 @@ public class SandboxEndpointSimpleTests
// using var jsonDoc = JsonDocument.Parse(response);
// var root = jsonDoc.RootElement;
// // Ellenőrizzük, hogy tömb-e
// // Ellen<EFBFBD>rizz<EFBFBD>k, hogy t<>mb-e
// Assert.AreEqual(JsonValueKind.Array, root.ValueKind, "Response should be an array");
// Assert.IsTrue(root.GetArrayLength() > 0, "Array should have items");
// Console.WriteLine($"[GetTestItems] Received {root.GetArrayLength()} items");
// // Ellenőrizzük az első elemet
// // Ellen<EFBFBD>rizz<EFBFBD>k az els<6C> elemet
// var firstItem = root[0];
// Assert.IsTrue(firstItem.TryGetProperty("Id", out _) || firstItem.TryGetProperty("id", out _),
// "Item should have 'Id' property");
@ -187,8 +183,8 @@ public class SandboxEndpointSimpleTests
//#region EREDETI BUSINESS ENDPOINT TESZTEK - KIKOMMENTEZVE
//// ===========================================
//// === Az alábbi tesztek az eredeti 3 endpoint-ot tesztelik ===
//// === Visszaállításhoz: töröld a kommenteket és regisztráld az endpoint-okat a Program.cs-ben ===
//// === Az al<EFBFBD>bbi tesztek az eredeti 3 endpoint-ot tesztelik ===
//// === Vissza<EFBFBD>ll<EFBFBD>t<EFBFBD>shoz: t<>r<EFBFBD>ld a kommenteket <20>s regisztr<74>ld az endpoint-okat a Program.cs-ben ===
//// ===========================================
//// [TestMethod]
@ -260,13 +256,13 @@ public class SandboxEndpointSimpleTests
// await connection.StartAsync();
// Assert.AreEqual(HubConnectionState.Connected, connection.State, $"Failed to connect to SignalR hub for {endpointName}");
// // Készítsük el a request data-t
// // Ha nincs paraméter, null-t küldünk (nem üres byte tömböt!)
// // K<EFBFBD>sz<EFBFBD>ts<EFBFBD>k el a request data-t
// // Ha nincs param<EFBFBD>ter, null-t k<>ld<6C>nk (nem <20>res byte t<>mb<6D>t!)
// byte[]? requestData = parameter != null
// ? Encoding.UTF8.GetBytes(JsonSerializer.Serialize(parameter))
// : null;
// // A Hub metódus neve: OnReceiveMessage (3 paraméter: messageTag, messageBytes, requestId)
// // A Hub met<EFBFBD>dus neve: OnReceiveMessage (3 param<61>ter: messageTag, messageBytes, requestId)
// await connection.InvokeAsync("OnReceiveMessage", tag, requestData, (int?)null);
// var completed = await Task.WhenAny(responseReceived.Task, Task.Delay(15000));
@ -276,7 +272,7 @@ public class SandboxEndpointSimpleTests
// Console.WriteLine($"[{endpointName}] Response tag: {receivedTag}");
// Console.WriteLine($"[{endpointName}] Response JSON: {receivedJson?.Substring(0, Math.Min(500, receivedJson?.Length ?? 0))}...");
// // Ellenőrizzük, hogy valid JSON-e (ha van adat)
// // Ellen<EFBFBD>rizz<EFBFBD>k, hogy valid JSON-e (ha van adat)
// if (!string.IsNullOrEmpty(receivedJson))
// {
// try

View File

@ -0,0 +1,7 @@
# 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.

View File

@ -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();
}

View File

@ -0,0 +1,53 @@
using AyCode.Core.Enums;
using AyCode.Core.Loggers;
using AyCode.Core.Serializers.Binaries;
using AyCode.Services.SignalRs;
using FruitBank.Common;
using FruitBank.Common.Loggers;
using FruitBankHybrid.Shared.Services.Loggers;
using FruitBankHybrid.Shared.Services.SignalRs;
using Microsoft.AspNetCore.Http.Connections;
using Microsoft.AspNetCore.SignalR.Client;
namespace FruitBankHybrid.Shared.Tests;
/// <summary>
/// Test-only factory for <see cref="FruitBankSignalRClient"/>. Builds a <c>HubConnectionBuilder</c>
/// with the same connection settings a production <c>Program.cs</c> would use, wires a logger factory
/// backed by a single <c>SignaRClientLogItemWriter</c> (test-unit AppType, Detail level),
/// and uses <see cref="BinaryProtocolMode.AsyncSegment"/> for the protocol.
/// </summary>
internal static class TestSignalRClientFactory
{
public static FruitBankSignalRClient Create(string testCategoryName)
{
var logWriters = new List<IAcLogWriterClientBase>
{
new SignaRClientLogItemWriter(AppType.TestUnit, LogLevel.Detail, testCategoryName)
};
Func<string, LoggerClient> loggerFactory =
categoryName => new LoggerClient(categoryName, logWriters.ToArray());
var connectionOptions = new AcHubConnectionOptions
{
Url = $"{FruitBankConstClient.BaseUrl}/{FruitBankConstClient.DefaultHubName}",
TransportMaxBufferSize = 30_000_000,
ApplicationMaxBufferSize = 30_000_000,
CloseTimeout = TimeSpan.FromSeconds(10),
KeepAliveInterval = TimeSpan.FromSeconds(60),
ServerTimeout = TimeSpan.FromSeconds(180),
SkipNegotiation = true,
Transports = HttpTransportType.WebSockets,
UseAutomaticReconnect = true,
UseStatefulReconnect = true
};
var logger = loggerFactory(nameof(FruitBankSignalRClient));
var hubBuilder = new HubConnectionBuilder().AddAcDefaults(logger, connectionOptions);
hubBuilder.AddAcBinaryProtocol(opts => opts.ProtocolMode = BinaryProtocolMode.AsyncSegment);
return new FruitBankSignalRClient(hubBuilder, loggerFactory);
}
}

View File

@ -0,0 +1,461 @@
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 = TestSignalRClientFactory.Create(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; }
}

View File

@ -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

View File

@ -7,6 +7,7 @@ using FruitBank.Common.Interfaces;
using FruitBank.Common.SignalRs;
using FruitBankHybrid.Shared.Pages;
using Microsoft.AspNetCore.Components;
using Nop.Core.Domain.Catalog;
using Nop.Core.Domain.Common;
using Nop.Core.Domain.Orders;
@ -51,7 +52,7 @@ public class GridGenericAttributeBase: FruitBankGridBase<GenericAttributeDto>, I
switch (ParentDataItem)
{
case IProductDto:
if (!hasContextIdParameter) ContextIds![ContextKeyGroupIndex] = "Product";
if (!hasContextIdParameter) ContextIds![ContextKeyGroupIndex] = nameof(Product);
break;
case IOrderDto:

View File

@ -0,0 +1,52 @@
# Grids
Domain-specific grid components, one per entity type. All inherit `FruitBankGridBase<TEntity>`.
> For the MgGrid framework reference see: `AyCode.Blazor/AyCode.Blazor.Components/docs/MGGRID/README.md`
## FruitBankGridBase
`FruitBankGridBase<TDataItem>` is the project-specific adapter that fixes the generic parameters:
```
MgGridBase<SignalRDataSourceObservable<TDataItem>, TDataItem, int, LoggerClient>
```
Adds these defaults in `OnParametersSet` (based on `IsMasterGrid`):
| Setting | Master | Detail |
|---|---|---|
| `SizeMode` | `Small` | `Small` |
| `ShowGroupPanel` | `true` | `false` |
| `ShowSearchBox` | `true` | `false` |
| `ShowFilterRow` | `true` | `false` |
| `FilterMenuButtonDisplayMode` | `Never` | `Always` |
| `DetailRowDisplayMode` | `Auto` | `Never` |
| `DetailExpandButtonDisplayMode` | `Auto` | `Never` |
| `PagerVisible` | `true` | `true` |
| `PageSize` | 20 (Small) / 15 | 10 |
| `AllowColumnReorder` | `true` | `true` |
| `AllowGroup` | `true` | `false` |
| `EditMode` | `EditRow` | `EditRow` |
| `FocusedRowEnabled` | `true` | `true` |
| `ColumnResizeMode` | `NextColumn` | `NextColumn` |
| `PageSizeSelectorVisible` | `true` | `true` |
Also adds `OnCustomizeElement`: alternating row colors (`.alt-item`), header background (`#E6E6E6`), `hideDetailButton` for non-admin users.
## Legacy MgGridBase
`Components/MgGridBase.cs` — a non-generic legacy class that directly extends `DxGrid` and implements `IMgGridBase`. Used by older pages that predate the generic `MgGridBase<…>`. New grids should use `FruitBankGridBase<TEntity>` instead.
## Subfolders
| Folder | Entity | Notes |
|---|---|---|
| `GenericAttributes/` | `GridGenericAttributeBase` | Context-based (ContextIds: EntityId, KeyGroup, StoreId). Parent type switching: Product, Order, OrderItem |
| `OrderItems/` | `GridOrderItem` | Commented out — placeholder |
| `Partners/` | `GridPartnerBase` | Simple master grid with CRUD tags |
| `Products/` | `GridStockQuantityHistoryDtoBase` | Detail grid under ProductDto |
| `ShippingDocuments/` | `GridShippingDocumentBase` | Parent type switching: Shipping, Product, Partner. Sets ContextIds/KeyFieldNameToParentId per parent type |
| `ShippingItems/` | `GridShippingItemBase` | Parent type switching: ShippingDocument, Shipping, Partner |
| `Shippings/` | `GridShippingBase` | Simple master grid with CRUD tags |
| `StockTakingItems/` | `GridStockTakingItemBase` | Simple master grid, GetAll only |

View File

@ -0,0 +1,18 @@
# Components
DevExpress Blazor grid wrappers, pallet measurement components, and toast notifications.
## Key Files (Root)
- **`MgGridBase.cs`** — Legacy non-generic grid base (directly extends `DxGrid`). Used by older pages. New grids should use `FruitBankGridBase<TEntity>` — see [`Grids/README.md`](Grids/README.md).
- **`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 |

View File

@ -0,0 +1,7 @@
# 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.

View File

@ -0,0 +1,7 @@
# Extensions
DevExpress dialog helper extensions.
## Key Files
- **`DevexpressComponentExtensions.cs`** — ShowMessageBoxAsync() and ShowConfirmBoxAsync() for DevExpress dialogs.

View File

@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk.Razor">
<Project Sdk="Microsoft.NET.Sdk.Razor">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
@ -72,4 +72,21 @@
<ItemGroup>
<Folder Include="Components\Toolbars\" />
</ItemGroup>
<ItemGroup>
<None Include="**\README.md" Exclude="$(DefaultItemExcludes)" />
</ItemGroup>
<ItemGroup>
<!-- appsettings.json is the canonical config source for Web / Web.Client / MAUI hosts.
They each pull it directly from disk (Web/Web.Client via <Target Copy>, MAUI via <EmbeddedResource>).
Suppress the Razor SDK's auto-publish content behavior so the file does NOT flow into
dependent projects' publish output — that would collide with each host's own copy
(NETSDK1152 "multiple publish output files with the same relative path"). -->
<Content Update="appsettings.json">
<CopyToOutputDirectory>Never</CopyToOutputDirectory>
<CopyToPublishDirectory>Never</CopyToPublishDirectory>
<Pack>false</Pack>
</Content>
</ItemGroup>
</Project>

View File

@ -0,0 +1,9 @@
# 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.

View File

@ -0,0 +1,7 @@
# Models
View models for measuring pages.
## Key Files
- **`MeasuringDateSelectorModel.cs`** — Date picker model: ShippingId, DateTime, IsMeasured flag.

View File

@ -0,0 +1,13 @@
# 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.

View File

@ -0,0 +1,34 @@
# FruitBankHybrid.Shared
@project {
type = "product"
own-dep-projects = [
"AyCode.Core, AyCode.Entities, AyCode.Interfaces, AyCode.Models, AyCode.Services, AyCode.Services.Server, AyCode.Utils (in AyCode.Core repo)",
"AyCode.Blazor.Components (in AyCode.Blazor repo)",
"Mango.Nop.Core (in Mango.Nop Libraries repo)"
]
}
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.
- **`appsettings.json`** — Canonical configuration source for all three hosts (Web, Web.Client, MAUI). Edit ONLY here. Pull mechanism per host: see `docs/ARCHITECTURE.md` (in repo root) → "Shared Configuration".
## Target Framework
.NET 10.0 with AOT compilation and WASM IL stripping enabled.

View File

@ -0,0 +1,7 @@
# Loggers
Custom logger implementations for the FruitBank client.
## Key Files
- **`LoggerClient.cs`** — Non-generic and generic `LoggerClient<T>` extending AyCode logger base.

View File

@ -0,0 +1,16 @@
# 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 |

View File

@ -19,20 +19,16 @@ using Microsoft.AspNetCore.SignalR.Client;
using Nop.Core.Domain.Customers;
using System.Collections.ObjectModel;
using System.ServiceModel.Channels;
using AyCode.Core.Serializers;
using Mango.Nop.Core.Entities;
namespace FruitBankHybrid.Shared.Services.SignalRs
{
public class FruitBankSignalRClient : AcSignalRClientBase, IFruitBankDataControllerClient, ICustomOrderSignalREndpointClient, IStockSignalREndpointClient
{
public FruitBankSignalRClient( /*IServiceProvider serviceProvider, */ IEnumerable<IAcLogWriterClientBase> logWriters) : base($"{FruitBankConstClient.BaseUrl}/{FruitBankConstClient.DefaultHubName}", new LoggerClient(nameof(FruitBankSignalRClient), logWriters.ToArray()))
public FruitBankSignalRClient(IHubConnectionBuilder hubBuilder, Func<string, LoggerClient> loggerFactory)
: base(hubBuilder, loggerFactory(nameof(FruitBankSignalRClient)))
{
//var hubConnection = new HubConnectionBuilder()
// .WithUrl("fullHubName")
// .WithAutomaticReconnect()
// .WithStatefulReconnect()
// .WithKeepAliveInterval(TimeSpan.FromSeconds(60))
// .WithServerTimeout(TimeSpan.FromSeconds(120))
EnableBinaryDiagnostics = FruitBankConstClient.SignalRSerializerDiagnosticLog;
ConstHelper.NameByValue<SignalRTags>(0);
}
@ -42,10 +38,14 @@ namespace FruitBankHybrid.Shared.Services.SignalRs
/// </summary>
public event Func<int, SignalResponseDataMessage?, Task> OnMessageReceived = null!;
protected override async Task MessageReceived(int messageTag, byte[] messageBytes)
protected override async Task MessageReceived(int messageTag, SignalParams signalParams, object data)
{
var responseDataMessage = messageBytes.BinaryTo<SignalResponseDataMessage>();
var responseDataMessage = new SignalResponseDataMessage
{
Status = signalParams.Status,
DataSerializerType = AcSerializerType.Binary,
RawResponseData = data
};
await OnMessageReceived(messageTag, responseDataMessage);
}

View File

@ -0,0 +1,8 @@
# 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.

View File

@ -0,0 +1,44 @@
{
"Logging": {
"LogLevel": {
"Default": "Debug",
"Microsoft.AspNetCore": "Warning"
}
},
"AyCode": {
"ProjectId": "aad53443-2ee2-4650-8a99-97e907265e4e",
"Urls": {
"BaseUrl": "https://localhost:59579",
"ApiBaseUrl": "https://localhost:59579"
},
"Logger": {
"AppType": "Server",
"LogLevel": "Detail",
"LogWriters": [
{
"LogLevel": "Detail",
"LogWriterType": "FruitBank.Common.Server.Services.Loggers.ConsoleLogWriter, FruitBank.Common.Server, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"
}
]
}
},
"AcHubConnection": {
"Url": "https://localhost:59579/fbHub",
"TransportMaxBufferSize": 30000000,
"ApplicationMaxBufferSize": 30000000,
"CloseTimeout": "00:00:10",
"KeepAliveInterval": "00:01:00",
"ServerTimeout": "00:03:00",
"SkipNegotiation": true,
"Transports": "WebSockets",
"UseAutomaticReconnect": true,
"UseStatefulReconnect": true
},
"AcBinaryHubProtocol": {
"ProtocolMode": "AsyncSegment",
"BufferSize": 4096,
"FlushPolicy": "DoubleBuffered",
"FlushTimeout": "00:00:10"
}
}

View File

@ -26,6 +26,27 @@
<ProjectReference Include="..\FruitBankHybrid.Shared\FruitBankHybrid.Shared.csproj" />
</ItemGroup>
<!-- Shared appsettings.json synced into wwwroot at build time.
Approach: pre-build Copy Target (not <Content Include Link=...>), because:
- <Content Include="..\..." Link="wwwroot\..."/> triggers StaticWebAssets.Normalize() "Illegal characters"
- pre-normalizing via [System.IO.Path]::GetFullPath(...) still fails (the absolute path also trips the validator)
- a pre-build Copy creates a physical wwwroot/appsettings.json which the StaticWebAssets auto-discovery
picks up naturally, same as any other wwwroot file
BeforeTargets lists multiple early targets to ensure the Copy runs before static-asset discovery,
regardless of which one triggers first in a given SDK version.
NOTE: a clean build (delete obj/) is required the first time, because the static-asset manifest
is cached in obj/ and stale entries persist across incremental builds.
The physical wwwroot/appsettings.json is a build artifact — commit or gitignore per team policy;
edits should always be made in FruitBankHybrid.Shared/appsettings.json (the canonical source). -->
<Target Name="CopySharedAppSettings"
BeforeTargets="CollectPackageReferences;AssignTargetPaths;ResolveStaticWebAssetsInputs;BeforeBuild"
Inputs="..\FruitBankHybrid.Shared\appsettings.json"
Outputs="wwwroot\appsettings.json">
<Copy SourceFiles="..\FruitBankHybrid.Shared\appsettings.json"
DestinationFiles="wwwroot\appsettings.json"
SkipUnchangedFiles="true" />
</Target>
<ItemGroup>
<Reference Include="AyCode.Core">
<HintPath>..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Services.Server\bin\FruitBank\$(Configuration)\net9.0\AyCode.Core.dll</HintPath>

View File

@ -1,4 +1,5 @@
using AyCode.Core.Loggers;
using AyCode.Core.Serializers.Binaries;
using AyCode.Services.SignalRs;
using FruitBank.Common;
using FruitBank.Common.Loggers;
@ -6,11 +7,13 @@ using FruitBank.Common.Models;
using FruitBank.Common.Services;
using FruitBankHybrid.Shared.Databases;
using FruitBankHybrid.Shared.Services;
using FruitBankHybrid.Shared.Services.Loggers;
using FruitBankHybrid.Shared.Services.SignalRs;
using FruitBankHybrid.Web.Client.Services;
using FruitBankHybrid.Web.Client.Services.Loggers;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using Microsoft.AspNetCore.SignalR.Client;
using Microsoft.Extensions.Options;
var builder = WebAssemblyHostBuilder.CreateDefault(args);
@ -20,21 +23,55 @@ builder.Services.AddDevExpressBlazor(configure => configure.SizeMode = DevExpres
builder.Services.AddSingleton<IFormFactor, FormFactor>();
builder.Services.AddSingleton<ISecureCredentialService, WebSecureCredentialService>();
//#if DEBUG
#if DEBUG
builder.Services.AddSingleton<IAcLogWriterClientBase, BrowserConsoleLogWriter>();
//#endif
#endif
builder.Services.AddSingleton<IAcLogWriterClientBase, SignaRClientLogItemWriter>();
builder.Services.AddSingleton<LoggedInModel>(sp => new LoggedInModel(sp.GetRequiredService<ISecureCredentialService>()));
// Bind SignalR options from wwwroot/appsettings.json (loaded automatically by WebAssemblyHostBuilder) —
// single Configure call per options type, combining section Bind with runtime overrides.
builder.Services.Configure<AcHubConnectionOptions>(opts => builder.Configuration.GetSection("AcHubConnection").Bind(opts));
builder.Services.Configure<AcBinaryHubProtocolOptions>(opts =>
{
builder.Configuration.GetSection("AcBinaryHubProtocol").Bind(opts);
// WASM safety net: AsyncSegment send-path is unsupported here — Validate() would throw.
// Downgrade if appsettings.json accidentally specifies it.
if (opts.ProtocolMode == BinaryProtocolMode.AsyncSegment) opts.ProtocolMode = BinaryProtocolMode.Segment;
});
// Logger options + framework factory. LoggerClient instances are created per caller category,
// with AppType+LogLevel from appsettings and writers resolved from DI via IAcLogWriterClientBase.
builder.Services.Configure<AcLoggerOptions>(builder.Configuration.GetSection("AyCode:Logger"));
builder.Services.AddAcLoggerFactory<LoggerClient, IAcLogWriterClientBase>();
// HubConnectionBuilder — transient so each consumer gets a fresh builder to Build().
// All connection and protocol configuration flows from appsettings.json via IOptions<T>;
// AddAcDefaults (framework) applies AcHubConnectionOptions and bridges the provided logger into SignalR's internal pipeline.
// NOTE: AcBinaryHubProtocolOptions is resolved from the OUTER service provider and passed
// explicitly — HubConnectionBuilder's inner DI cannot see outer services.Configure<T>() registrations.
builder.Services.AddTransient<IHubConnectionBuilder>(sp =>
{
var loggerFactory = sp.GetRequiredService<Func<string, LoggerClient>>();
var connectionOpts = sp.GetRequiredService<IOptions<AcHubConnectionOptions>>().Value;
var protocolOpts = sp.GetRequiredService<IOptions<AcBinaryHubProtocolOptions>>().Value;
var logger = loggerFactory(nameof(FruitBankSignalRClient));
var hubBuilder = new HubConnectionBuilder().AddAcDefaults(logger, connectionOpts);
hubBuilder.AddAcBinaryProtocol(protocolOpts);
return hubBuilder;
});
builder.Services.AddSingleton<LoggedInModel>(sp =>
new LoggedInModel(sp.GetRequiredService<ISecureCredentialService>()));
builder.Services.AddSingleton<FruitBankSignalRClient>();
builder.Services.AddSingleton<DatabaseClient>();
builder.Services.AddSingleton<IAcLogWriterClientBase, SignaRClientLogItemWriter>();
#if DEBUG
if (FruitBankConstClient.SignalRSerializerDiagnosticLog)
{
SignalResponseDataMessage.DiagnosticLogger = message => { Console.WriteLine(message); };
//SignalResponseDataMessage.DiagnosticLogger = message => { Console.WriteLine(message); };
}
#endif

View File

@ -0,0 +1,22 @@
# FruitBankHybrid.Web.Client
@project {
type = "product"
own-dep-projects = [
"AyCode.Core, AyCode.Core.Server, AyCode.Entities, AyCode.Entities.Server, AyCode.Interfaces, AyCode.Interfaces.Server, AyCode.Models, AyCode.Models.Server, AyCode.Services, AyCode.Services.Server, AyCode.Utils (in AyCode.Core repo)",
"Mango.Nop.Core (in Mango.Nop Libraries repo)"
]
}
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.

View File

@ -0,0 +1,12 @@
# 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.

View File

@ -1,8 +0,0 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}

View File

@ -1,8 +1,44 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Default": "Debug",
"Microsoft.AspNetCore": "Warning"
}
},
"AyCode": {
"ProjectId": "aad53443-2ee2-4650-8a99-97e907265e4e",
"Urls": {
"BaseUrl": "https://localhost:59579",
"ApiBaseUrl": "https://localhost:59579"
},
"Logger": {
"AppType": "Server",
"LogLevel": "Detail",
"LogWriters": [
{
"LogLevel": "Detail",
"LogWriterType": "FruitBank.Common.Server.Services.Loggers.ConsoleLogWriter, FruitBank.Common.Server, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"
}
]
}
},
"AcHubConnection": {
"Url": "https://localhost:59579/fbHub",
"TransportMaxBufferSize": 30000000,
"ApplicationMaxBufferSize": 30000000,
"CloseTimeout": "00:00:10",
"KeepAliveInterval": "00:01:00",
"ServerTimeout": "00:03:00",
"SkipNegotiation": true,
"Transports": "WebSockets",
"UseAutomaticReconnect": true,
"UseStatefulReconnect": true
},
"AcBinaryHubProtocol": {
"ProtocolMode": "AsyncSegment",
"BufferSize": 4096,
"FlushPolicy": "DoubleBuffered",
"FlushTimeout": "00:00:10"
}
}

View File

@ -0,0 +1,9 @@
# 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.

View File

@ -6,7 +6,7 @@
<ImplicitUsings>enable</ImplicitUsings>
<!--<PublishTrimmed>true</PublishTrimmed>-->
<RunAOTCompilation>false</RunAOTCompilation>
<RunAOTCompilation>true</RunAOTCompilation>
<WasmStripILAfterAOT>true</WasmStripILAfterAOT>
<OverrideHtmlAssetPlaceholders>true</OverrideHtmlAssetPlaceholders>
</PropertyGroup>
@ -85,6 +85,19 @@
<Folder Include="Services\SignalRs\" />
</ItemGroup>
<!-- Shared appsettings.json copied into project root at build time.
Why a Copy target instead of <Content Include="..\..." Link="appsettings.json" CopyToOutputDirectory="PreserveNewest"/>:
the Link approach only copies the file to bin/Debug output, but ASP.NET Core's WebApplicationBuilder
in development reads appsettings.json from the ContentRoot (= project directory), not from the output folder.
Materializing the file physically in the project root makes it discoverable by the default configuration
loader in every run mode (F5 / dotnet run / published), and the SDK auto-include for appsettings*.json
takes care of copy-to-output from there. -->
<Target Name="CopySharedAppSettings" BeforeTargets="BeforeBuild" Inputs="..\FruitBankHybrid.Shared\appsettings.json" Outputs="appsettings.json">
<Copy SourceFiles="..\FruitBankHybrid.Shared\appsettings.json"
DestinationFiles="appsettings.json"
SkipUnchangedFiles="true" />
</Target>
</Project>

View File

@ -1,14 +1,18 @@
using AyCode.Core.Loggers;
using AyCode.Core.Serializers.Binaries;
using AyCode.Services.SignalRs;
using FruitBank.Common;
using FruitBank.Common.Models;
using FruitBank.Common.Services;
using FruitBank.Common.Server.Services.Loggers;
using FruitBank.Common.Server.Services.SignalRs;
using FruitBankHybrid.Shared.Databases;
using FruitBankHybrid.Shared.Services;
using FruitBankHybrid.Shared.Services.Loggers;
using FruitBankHybrid.Shared.Services.SignalRs;
using FruitBankHybrid.Web.Components;
using FruitBankHybrid.Web.Services;
using Microsoft.AspNetCore.SignalR.Client;
using Microsoft.Extensions.Options;
var builder = WebApplication.CreateBuilder(args);
@ -16,13 +20,49 @@ builder.Services.AddRazorComponents().AddInteractiveWebAssemblyComponents();
builder.Services.AddDevExpressBlazor(configure => configure.SizeMode = DevExpress.Blazor.SizeMode.Medium);
builder.Services.AddMvc();
builder.Services.AddSignalR(options => options.MaximumReceiveMessageSize = 256 * 1024);
builder.Services.AddSingleton<IFormFactor, FormFactor>();
builder.Services.AddSingleton<ISecureCredentialService, ServerSecureCredentialService>();
builder.Services.AddSingleton<IAcLogWriterBase, ConsoleLogWriter>();
builder.Services.AddSingleton<LoggedInModel>(sp =>
new LoggedInModel(sp.GetRequiredService<ISecureCredentialService>()));
// Logger options + framework factory. LoggerClient instances are created per caller category,
// with AppType+LogLevel from appsettings and writers resolved from DI via IAcLogWriterBase.
builder.Services.Configure<AcLoggerOptions>(builder.Configuration.GetSection("AyCode:Logger"));
builder.Services.AddAcLoggerFactory<LoggerClient>();
builder.Services.AddSingleton<LoggedInModel>(sp => new LoggedInModel(sp.GetRequiredService<ISecureCredentialService>()));
// Bind SignalR options from appsettings.json — single Configure call per options type.
// The lambda runs the appsettings Bind first, then any runtime overrides (e.g. the WASM safety net).
builder.Services.Configure<AcHubConnectionOptions>(opts => builder.Configuration.GetSection("AcHubConnection").Bind(opts));
builder.Services.Configure<AcBinaryHubProtocolOptions>(opts =>
{
builder.Configuration.GetSection("AcBinaryHubProtocol").Bind(opts);
// Platform safety net: on WebAssembly the AsyncSegment send-path is unsupported
// (Validate() would throw). No-op on this server host, but matches the contract.
if (OperatingSystem.IsBrowser() && opts.ProtocolMode == BinaryProtocolMode.AsyncSegment)
opts.ProtocolMode = BinaryProtocolMode.Segment;
});
// HubConnectionBuilder — transient so each consumer gets a fresh builder to Build().
// All connection and protocol configuration flows from appsettings.json via IOptions<T>;
// AddAcDefaults (framework) applies AcHubConnectionOptions and bridges the provided logger into SignalR's internal pipeline.
// NOTE: AcBinaryHubProtocolOptions is resolved from the OUTER service provider and passed
// explicitly — HubConnectionBuilder's inner DI cannot see outer services.Configure<T>() registrations.
builder.Services.AddTransient<IHubConnectionBuilder>(sp =>
{
var loggerFactory = sp.GetRequiredService<Func<string, LoggerClient>>();
var connectionOpts = sp.GetRequiredService<IOptions<AcHubConnectionOptions>>().Value;
var protocolOpts = sp.GetRequiredService<IOptions<AcBinaryHubProtocolOptions>>().Value;
var logger = loggerFactory(nameof(FruitBankSignalRClient));
var hubBuilder = new HubConnectionBuilder().AddAcDefaults(logger, connectionOpts);
hubBuilder.AddAcBinaryProtocol(protocolOpts);
return hubBuilder;
});
builder.Services.AddSingleton<FruitBankSignalRClient>();
builder.Services.AddSingleton<DatabaseClient>();
@ -45,9 +85,6 @@ else
app.UseHsts();
}
app.MapHub<LoggerSignalRHub>($"/{FruitBankConstClient.LoggerHubName}");
app.MapHub<DevAdminSignalRHub>($"/{FruitBankConstClient.DefaultHubName}");
app.UseHttpsRedirection();
app.UseStaticFiles();
@ -57,8 +94,6 @@ app.MapStaticAssets();
app.MapRazorComponents<App>()
//.AddInteractiveServerRenderMode()
.AddInteractiveWebAssemblyRenderMode()
.AddAdditionalAssemblies(
typeof(FruitBankHybrid.Shared._Imports).Assembly,
typeof(FruitBankHybrid.Web.Client._Imports).Assembly);
.AddAdditionalAssemblies(typeof(FruitBankHybrid.Shared._Imports).Assembly, typeof(FruitBankHybrid.Web.Client._Imports).Assembly);
app.Run();

View File

@ -0,0 +1,23 @@
# FruitBankHybrid.Web
@project {
type = "product"
own-dep-projects = [
"AyCode.Core, AyCode.Core.Server, AyCode.Entities, AyCode.Entities.Server, AyCode.Interfaces, AyCode.Interfaces.Server, AyCode.Models, AyCode.Models.Server, AyCode.Services, AyCode.Services.Server, AyCode.Utils (in AyCode.Core repo)",
"Mango.Nop.Core (in Mango.Nop Libraries repo)"
]
}
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.

View File

@ -0,0 +1,8 @@
# Services
Server-side service implementations.
## Key Files
- **`FormFactor.cs`** — Returns "Web" as form factor.
- **`ServerSecureCredentialService.cs`** — No-op implementation (clients handle credential storage).

View File

@ -1,8 +0,0 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}

View File

@ -1,17 +1,15 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Default": "Debug",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"AyCode": {
"ProjectId": "aad53443-2ee2-4650-8a99-97e907265e4e",
"Urls": {
"BaseUrl": "https://localhost:7144",
"ApiBaseUrl": "https://localhost:7144"
"BaseUrl": "https://localhost:59579",
"ApiBaseUrl": "https://localhost:59579"
},
"Logger": {
"AppType": "Server",
@ -19,10 +17,28 @@
"LogWriters": [
{
"LogLevel": "Detail",
"LogWriterType": "FruitBank.Common.Server.Services.Loggers.ConsoleLogWriter, FruitBank.Common.Server, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null",
"LogWriterType": "FruitBank.Common.Server.Services.Loggers.ConsoleLogWriter, FruitBank.Common.Server, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"
}
]
}
},
"AcHubConnection": {
"Url": "https://localhost:59579/fbHub",
"TransportMaxBufferSize": 30000000,
"ApplicationMaxBufferSize": 30000000,
"CloseTimeout": "00:00:10",
"KeepAliveInterval": "00:01:00",
"ServerTimeout": "00:03:00",
"SkipNegotiation": true,
"Transports": "WebSockets",
"UseAutomaticReconnect": true,
"UseStatefulReconnect": true
},
"AcBinaryHubProtocol": {
"ProtocolMode": "AsyncSegment",
"BufferSize": 4096,
"FlushPolicy": "DoubleBuffered",
"FlushTimeout": "00:00:10"
}
}

View File

@ -1,4 +1,4 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 18
VisualStudioVersion = 18.0.11222.15
@ -33,10 +33,25 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
ProjectSection(SolutionItems) = preProject
..\..\..\Aycode\Source\AyCode.Blazor\AyCode.Blazor.Components\Components\Grids\MgGridSignalRDataSource.txt = ..\..\..\Aycode\Source\AyCode.Blazor\AyCode.Blazor.Components\Components\Grids\MgGridSignalRDataSource.txt
SqlSchemaCompare_Dev_to_Prod.scmp = SqlSchemaCompare_Dev_to_Prod.scmp
.github\copilot-instructions.md = .github\copilot-instructions.md
CLAUDE.md = CLAUDE.md
README.md = README.md
EndProjectSection
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}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "docs", "docs", "{B7D3E8A1-F4C2-4E9D-A6B5-1C3D5E7F9A2B}"
ProjectSection(SolutionItems) = preProject
docs\ARCHITECTURE.md = docs\ARCHITECTURE.md
docs\CONVENTIONS.md = docs\CONVENTIONS.md
docs\GLOSSARY.md = docs\GLOSSARY.md
docs\SCHEMA.md = docs\SCHEMA.md
EndProjectSection
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
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -101,6 +116,14 @@ Global
{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}.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
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE

View File

@ -21,6 +21,7 @@
<!-- Versions -->
<ApplicationDisplayVersion>1.0.2</ApplicationDisplayVersion>
<ApplicationVersion>1</ApplicationVersion>
<PackageVersion>$(ApplicationDisplayVersion)</PackageVersion>
<RunAOTCompilation>false</RunAOTCompilation>
<WasmStripILAfterAOT>true</WasmStripILAfterAOT>
@ -78,6 +79,12 @@
<AndroidKeyStore>True</AndroidKeyStore>
</PropertyGroup>
<ItemGroup>
<!-- Shared appsettings.json linked from FruitBankHybrid.Shared, embedded so MauiAppBuilder can load via GetManifestResourceStream.
LogicalName preserves the original manifest resource name so the loader in MauiProgram.cs doesn't need changes. -->
<EmbeddedResource Include="..\FruitBankHybrid.Shared\appsettings.json" Link="appsettings.json" LogicalName="FruitBankHybrid.appsettings.json" />
</ItemGroup>
<ItemGroup>
<!-- App Icon -->
<MauiIcon Include="Resources\AppIcon\appicon.svg" ForegroundFile="Resources\AppIcon\appiconfg.svg" Color="#512BD4" />
@ -131,11 +138,11 @@
<Reference Include="AyCode.Entities">
<HintPath>..\..\..\..\Aycode\Source\AyCode.Core\AyCode.Services.Server\bin\FruitBank\$(Configuration)\net9.0\AyCode.Entities.dll</HintPath>
</Reference>
<!--<Reference Include="Mango.Nop.Core">
<HintPath>..\..\NopCommerce.Common\4.70\Libraries\Mango.Nop.Core\bin\FruitBank\Debug\net9.0\Mango.Nop.Core.dll</HintPath>
<Reference Include="Mango.Nop.Core">
<HintPath>..\..\NopCommerce.Common\4.70\Libraries\Mango.Nop.Core\bin\FruitBank\$(Configuration)\net9.0\Mango.Nop.Core.dll</HintPath>
</Reference>
<Reference Include="Nop.Core">
<HintPath>..\..\NopCommerce.Common\4.70\Libraries\Mango.Nop.Core\bin\FruitBank\Debug\net9.0\Nop.Core.dll</HintPath>
<!--<Reference Include="Nop.Core">
<HintPath>..\..\NopCommerce.Common\4.70\Libraries\Mango.Nop.Core\bin\FruitBank\$(Configuration)\net9.0\Nop.Core.dll</HintPath>
</Reference>-->
</ItemGroup>

View File

@ -1,4 +1,5 @@
using AyCode.Core.Loggers;
using AyCode.Services.SignalRs;
using FruitBank.Common.Loggers;
using FruitBank.Common.Models;
using FruitBank.Common.Services;
@ -6,9 +7,15 @@ using FruitBankHybrid.Services;
using FruitBankHybrid.Services.Loggers;
using FruitBankHybrid.Shared.Databases;
using FruitBankHybrid.Shared.Services;
using FruitBankHybrid.Shared.Services.Loggers;
using FruitBankHybrid.Shared.Services.SignalRs;
//using DevExpress.Maui;
using Microsoft.AspNetCore.SignalR.Client;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using System.Reflection;
namespace FruitBankHybrid
{
@ -27,22 +34,59 @@ namespace FruitBankHybrid
fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
});
// Load embedded appsettings.json — MAUI has no automatic config file discovery,
// so the JSON is shipped as an EmbeddedResource (see FruitBankHybrid.csproj).
using (var appsettingsStream = Assembly.GetExecutingAssembly().GetManifestResourceStream("FruitBankHybrid.appsettings.json"))
{
if (appsettingsStream is not null)
{
var jsonConfig = new ConfigurationBuilder().AddJsonStream(appsettingsStream).Build();
builder.Configuration.AddConfiguration(jsonConfig);
}
}
#if DEBUG
builder.Services.AddSingleton<IAcLogWriterClientBase, BrowserConsoleLogWriter>();
#endif
builder.Services.AddSingleton<IAcLogWriterClientBase, SignaRClientLogItemWriter>();
// Logger options + framework factory. LoggerClient instances are created per caller category,
// with AppType+LogLevel from appsettings and writers resolved from DI via IAcLogWriterClientBase.
builder.Services.Configure<AcLoggerOptions>(builder.Configuration.GetSection("AyCode:Logger"));
builder.Services.AddAcLoggerFactory<LoggerClient, IAcLogWriterClientBase>();
// Bind SignalR options from configuration.
// Precedence: code default → appsettings.json (this line) → any later Configure<T> action.
builder.Services.Configure<AcHubConnectionOptions>(builder.Configuration.GetSection("AcHubConnection"));
builder.Services.Configure<AcBinaryHubProtocolOptions>(builder.Configuration.GetSection("AcBinaryHubProtocol"));
// Add device-specific services used by the FruitBankHybrid.Shared project
builder.Services.AddSingleton<IFormFactor, FormFactor>();
builder.Services.AddSingleton<ISecureCredentialService, MauiSecureCredentialService>();
#if DEBUG
builder.Services.AddSingleton<IAcLogWriterClientBase, BrowserConsoleLogWriter>();
#endif
builder.Services.AddSingleton<LoggedInModel>(sp => new LoggedInModel(sp.GetRequiredService<ISecureCredentialService>()));
// SignalR HubConnectionBuilder — transient so each consumer gets a fresh builder to Build().
// All connection and protocol configuration flows from appsettings.json via IOptions<T>;
// AddAcDefaults (framework) applies AcHubConnectionOptions and bridges the provided logger into SignalR's internal pipeline.
// NOTE: AcBinaryHubProtocolOptions is resolved from the OUTER service provider and passed
// explicitly — HubConnectionBuilder's inner DI cannot see outer services.Configure<T>() registrations.
builder.Services.AddTransient<IHubConnectionBuilder>(sp =>
{
var loggerFactory = sp.GetRequiredService<Func<string, LoggerClient>>();
var connectionOpts = sp.GetRequiredService<IOptions<AcHubConnectionOptions>>().Value;
var protocolOpts = sp.GetRequiredService<IOptions<AcBinaryHubProtocolOptions>>().Value;
var logger = loggerFactory(nameof(FruitBankSignalRClient));
var hubBuilder = new HubConnectionBuilder().AddAcDefaults(logger, connectionOpts);
hubBuilder.AddAcBinaryProtocol(protocolOpts);
return hubBuilder;
});
builder.Services.AddSingleton<LoggedInModel>(sp =>
new LoggedInModel(sp.GetRequiredService<ISecureCredentialService>()));
builder.Services.AddSingleton<FruitBankSignalRClient>();
builder.Services.AddSingleton<DatabaseClient>();
builder.Services.AddSingleton<IAcLogWriterClientBase, SignaRClientLogItemWriter>();
builder.Services.AddMauiBlazorWebView();
builder.Services.AddDevExpressBlazor(configure => configure.SizeMode = DevExpress.Blazor.SizeMode.Medium);

View File

@ -0,0 +1,10 @@
# 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).

View File

@ -16,7 +16,7 @@ namespace FruitBankHybrid.WinUI
/// </summary>
public App()
{
this.InitializeComponent();
//this.InitializeComponent();
}
protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();

26
FruitBankHybrid/README.md Normal file
View File

@ -0,0 +1,26 @@
# FruitBankHybrid
@project {
type = "product"
own-dep-projects = [
"AyCode.Core, AyCode.Services, AyCode.Entities (in AyCode.Core repo)",
"Mango.Nop.Core (in Mango.Nop Libraries repo)"
]
}
.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.

View File

@ -0,0 +1,12 @@
# 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.

46
README.md Normal file
View File

@ -0,0 +1,46 @@
# 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).
nopCommerce 4.80.9 requires it
## 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/` — GLOSSARY.md, ARCHITECTURE.md, CONVENTIONS.md. Domain model schema (TOON) lives in the plugin: `NopCommerce.Common/4.70/Plugins/Nop.Plugin.Misc.AIPlugin/docs/SCHEMA.md`
## Solution Structure
| Project | TFM | Purpose | README |
|---|---|---|---|
| [`FruitBank.Common`](FruitBank.Common/README.md) | net9.0 | Shared domain: entities, DTOs, interfaces, SignalR tags, measurement helpers | [README](FruitBank.Common/README.md) |
| [`FruitBank.Common.Server`](FruitBank.Common.Server/README.md) | net9.0 | Server-side: SignalR hubs, broadcast service, logging, nopCommerce integration | [README](FruitBank.Common.Server/README.md) |
| [`FruitBankHybrid.Shared`](FruitBankHybrid.Shared/README.md) | net10.0 | Blazor UI: pages, grids, SignalR client, measurement service, layout | [README](FruitBankHybrid.Shared/README.md) |
| [`FruitBankHybrid.Shared.Common`](FruitBankHybrid.Shared.Common/README.md) | net10.0 | Shared common library (placeholder) | [README](FruitBankHybrid.Shared.Common/README.md) |
| [`FruitBankHybrid`](FruitBankHybrid/README.md) | net10.0android/ios/win | MAUI Hybrid app: Android, iOS, Windows | [README](FruitBankHybrid/README.md) |
| [`FruitBankHybrid.Web`](FruitBankHybrid.Web/README.md) | net10.0 | Blazor Server host with SignalR hubs | [README](FruitBankHybrid.Web/README.md) |
| [`FruitBankHybrid.Web.Client`](FruitBankHybrid.Web.Client/README.md) | net10.0 | Blazor WebAssembly client | [README](FruitBankHybrid.Web.Client/README.md) |
### Test Projects
| Project | TFM | Purpose | README |
|---|---|---|---|
| [`FruitBankHybrid.Shared.Tests`](FruitBankHybrid.Shared.Tests/README.md) | net10.0 | Integration + serialization tests (SignalR, JSON, Toon, bunit) | [README](FruitBankHybrid.Shared.Tests/README.md) |
### External Dependencies
All projects reference these via **DLL** (not ProjectReference). Full source is available in sibling directories:
| Repo | Path | Key Docs |
|---|---|---|
| **AyCode.Core** (net9.0) | `../../../Aycode/Source/AyCode.Core/` | [copilot-instructions](../../../Aycode/Source/AyCode.Core/.github/copilot-instructions.md), [ARCHITECTURE](../../../Aycode/Source/AyCode.Core/docs/ARCHITECTURE.md) |
| **AyCode.Blazor** (net10.0) | `../../../Aycode/Source/AyCode.Blazor/` | [copilot-instructions](../../../Aycode/Source/AyCode.Blazor/.github/copilot-instructions.md), [MGGRID](../../../Aycode/Source/AyCode.Blazor/AyCode.Blazor.Components/docs/MGGRID/README.md) |
| **Mango.Nop Libraries** (net9.0) | `../NopCommerce.Common/4.70/Libraries/` | [copilot-instructions](../NopCommerce.Common/4.70/Libraries/.github/copilot-instructions.md), [ARCHITECTURE](../NopCommerce.Common/4.70/Libraries/docs/ARCHITECTURE.md) |
| **FruitBank Plugin** (net9.0) | `../NopCommerce.Common/4.70/Plugins/Nop.Plugin.Misc.AIPlugin/` | [README](../NopCommerce.Common/4.70/Plugins/Nop.Plugin.Misc.AIPlugin/README.md) |

155
docs/ARCHITECTURE.md Normal file

File diff suppressed because one or more lines are too long

44
docs/CONVENTIONS.md Normal file
View File

@ -0,0 +1,44 @@
# Conventions
> For core framework conventions see: `CONVENTIONS.md` (in AyCode.Core repo)
> For UI framework conventions see: `CONVENTIONS.md` (in AyCode.Blazor repo)
## 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.
- **GridXxxBase** = C# code-behind class inheriting `FruitBankGridBase<TDataItem>`.
- **GridXxx.razor** = Razor markup using `<GridXxxBase>` with `<Columns>` and `<DetailRowTemplate>`.
- **OnGrid prefix** for MgGridBase event parameters: `OnGridItemDeleting`, `OnGridEditModelSaving`, `OnGridFocusedRowChanged`, etc. (avoids collision with DxGrid base events).
- **SignalRTags** constants use numeric ranges by domain (see `FruitBank.Common/SignalRs/`).
## XML Documentation
`<summary>` — brief, developer-facing, readable in VS IntelliSense tooltip. NO implementation details, NO wire-format / byte-level / perf specifics — those live in `docs/TOPIC/*.md`. Add `<example>` only when usage is non-obvious; otherwise omit.
## 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** — canonical grid base from AyCode.Blazor (see `AyCode.Blazor.Components/docs/MGGRID/README.md` (in AyCode.Blazor repo)). Provides SignalR CRUD, layout persistence, master-detail, InfoPanel, fullscreen.
- **FruitBankGridBase** — project adapter that fixes `TId=int`, `TLoggerClient=LoggerClient`, adds per-user layout and master/detail defaults.
- **FruitBankSignalRClient** as single hub client for all server communication.
- **DatabaseClient** for client-side caching with ConcurrentDictionary tables.
### Grid Creation Checklist
1. Create `GridXxxBase.cs` inheriting `FruitBankGridBase<TEntity>`.
2. Set CRUD tags in constructor: `GetAllMessageTag`, `AddMessageTag`, `UpdateMessageTag`, `RemoveMessageTag`.
3. Create `GridXxx.razor` with `<GridXxxBase>`, `<Columns>`, optional `<DetailRowTemplate>`.
4. Wrap in `<MgGridWithInfoPanel>` if InfoPanel is needed.
5. For detail grids: set `ParentDataItem`, `KeyFieldNameToParentId`, `ContextIds`.
## 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.

37
docs/GLOSSARY.md Normal file
View File

@ -0,0 +1,37 @@
# Glossary / Fogalomtár
> For core framework glossary see: `GLOSSARY.md` (in AyCode.Core repo)
> For UI framework glossary see: `GLOSSARY.md` (in AyCode.Blazor repo)
> For core measurement system rules and common domain traps, see: `../FruitBank.Common/docs/GLOSSARY.md`
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. |
## 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.)
## UI / Grid Components
For MgGrid framework terms (MgGridBase, MgGridWithInfoPanel, MgGridToolbarBase, MgGridDataColumn, MgGridInfoPanel, IMgGridBase, etc.) see `GLOSSARY.md` (in AyCode.Blazor repo) and `AyCode.Blazor.Components/docs/MGGRID/README.md` (in AyCode.Blazor repo).
| Term | Definition |
|---|---|
| **FruitBankGridBase** | Project-level adapter: fixes `TSignalRDataSource=SignalRDataSourceObservable`, `TId=int`, `TLoggerClient=LoggerClient`. Adds per-user layout, master/detail defaults. See `FruitBankHybrid.Shared/Components/Grids/README.md`. |

22
docs/README.md Normal file
View File

@ -0,0 +1,22 @@
# FruitBankHybridApp documentation
Top-level documentation for the `FruitBankHybridApp` repo (Layer 3 — FruitBank MAUI/Blazor Hybrid client).
## Reference docs (flat)
- [`ARCHITECTURE.md`](ARCHITECTURE.md) — Repo architecture overview
- [`CONVENTIONS.md`](CONVENTIONS.md) — Coding conventions
- [`GLOSSARY.md`](GLOSSARY.md) — Domain glossary (FruitBank terms: Shipping, Order, StockTaking, MeasuringStatus, etc.)
## Sub-projects with docs
- `FruitBank.Common/docs/` — Common glossary (shared across Hybrid client-side)
## Navigation
Per the AI Agent Core Protocol (folder navigation rule), start from this README when browsing `docs/`. Single-file reference docs remain flat; multi-file topics would live in named subfolders (none currently at this level).
## See also
- **Server-side plugin**: `../../NopCommerce.Common/4.70/Plugins/Nop.Plugin.Misc.AIPlugin/docs/README.md`
- **Base framework** (AyCode.Core, AyCode.Blazor): see those repos' `docs/` folders.

View File

@ -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!");
}

1
tmpclaude-bf6e-cwd Normal file
View File

@ -0,0 +1 @@
/h/Applications/Mango/Source/FruitBankHybridApp