server/AGENTS.md
David Kong 96b6bc1e78 docs: harmonize AGENTS.md, DOMAIN.md, and README.md — fix inconsistencies across all three documents
- Expand BDP section in AGENTS.md from 4 entities to 18+ (Invoices, Inventory, Employees, Business Units, Locations, Suppliers, Manufacturers, Returns/RMA, Templates, Tasks & Task Lists, Service Jobs, Status Definitions, Sequences, Financial Accounts)
- Add Rust edition 2024 and missing crates to AGENTS.md Technical Implementation (serde, config, dotenvy, thiserror, tracing)
- Add Coding Conventions table for BDP section in AGENTS.md
- Update Database Configuration with specific variables (DATABASE_URL, server__host, server__port)
- Add Domain Concepts section referencing DOMAIN.md in AGENTS.md (_underscore fields, reference resolution pattern, AUD default currency, BU allocation)
- Add Domain Concepts section to README.md filling gaps
- Peer harmonization approach: no single source of truth; all three docs are peers with cross-references

See HARMONIZATION_DIFF.md for before/after diff summary.
2026-06-26 21:11:17 +10:00

322 lines
No EOL
16 KiB
Markdown

# Agents
This repository is a Business Data Platform (BDP) that provides a GraphQL API. The API allows AI agents to manage business entities including orders, customers, sales quotes, and products through GraphQL operations.
For comprehensive domain terminology, entity relationships, enums, and business concepts, see [`DOMAIN.md`](./DOMAIN.md).
## Available Operations
AI agents can perform the following operations via GraphQL:
### Orders
- Create new orders (supports MASTER/SUBORDER aggregation)
- Update existing orders
- Delete orders
- Retrieve order information
### Customers
- Create new customer records
- Update customer information
- Delete customer records
- Retrieve customer details
### Sales Quotes
- Create sales quotes
- Update quote information
- Delete quotes
- Retrieve quote data
### Products
- Create product listings
- Update product information
- Delete products
- Retrieve product details
### Invoices
- Create billing documents (sales invoices, RMA returns, employee invoices)
- Update invoice information
- Delete invoices
- Retrieve invoice data with payment tracking and tax breakdowns
### Inventory
- Manage individual stock units tracked by serial number or batch
- Support scan-in/out operations and manufacturing tracking
- Retrieve inventory records linked to products and supplier invoices
### Employees
- Create staff records with payroll, schedule, entitlements, PAYG tax settings
- Update employee information including authentication links
- Delete employee records
- Retrieve employee details
### Business Units
- Create top-level organizational entities grouping employees, locations, customers, and financial accounts
- Define currency, locale (en-AU default), and tax settings (GST or No GST)
- Update business unit configuration
- Delete business units
- Retrieve business unit records
### Locations
- Create physical sites associated with business units (warehouses, offices) with operating hours and addresses
- Update location information
- Delete locations
- Retrieve location data
### Suppliers
- Create external vendor/service provider records with credit terms and registration
- Update supplier category profiles and credit details
- Delete suppliers
- Retrieve supplier information
### Manufacturers
- Create entities that produce goods; referenced by products for identification
- Update manufacturer information
- Delete manufacturers
- Retrieve manufacturer records
### Returns (RMA)
- Create return merchandise authorization tracking: fault reporting, replacements, resolution actions
- Manage customer and supplier returns with completion date tracking
- Delete RMA records
- Retrieve RMA data including inventory references
### Templates
- Create reusable blueprints for orders, quotes, emails, leads, tickets, and articles
- Update template content (can be scoped: global or personal/owned by a specific Customer)
- Delete templates
- Retrieve template records
### Tasks & Task Lists
- Create work items with scheduling, assignment, progress tracking, status history
- Manage work done entries and task list organization
- Delete tasks
- Retrieve task records
### Service Jobs
- Create paid or warranty service performed for customers; includes work tracking and scheduling
- Update service job details
- Delete service jobs
- Retrieve service job data
### Status Definitions
- Create central registry of reusable status states with visual styling and allowed actions
- Update status definitions (styling, permitted transitions)
- Delete status definitions
- Retrieve status records
### Sequences
- Manage shared counters for auto-generating unique identifiers (order numbers, invoice numbers)
- Each sequence belongs to a model (e.g., `Order`, `Invoice`) and increments the corresponding field
- Default starting count is 200000
- Update sequence configuration
- Delete sequences
- Retrieve sequence records
### Financial Accounts
- Create bank accounts or payment methods used by the business for transactions
- Update account information
- Delete financial accounts
- Retrieve financial account data
## Technical Implementation
The API is built using:
- Rust programming language (**edition 2024**)
- **Juniper** GraphQL framework (`juniper`, `juniper_actix`)
- **Actix-web** HTTP server (`actix-web`, `actix-files`, `actix-cors`)
- Tokio async runtime
- SQLx for database operations (via `sqlx-postgres`) connecting to PostgreSQL
- Serde / serde_json for serialization
- Config crate for configuration (`.env` + environment variables with `__` separator)
- Dotenvy for `.env` file loading
- Thiserror for error types
- Tracing, tracing-subscriber, tracing-actix-web for logging and request tracing
## Database Configuration
Database connection parameters are configured via environment variables using the `__` separator convention:
| Variable | Purpose |
|---|---|
| `DATABASE_URL` | Full PostgreSQL connection string (e.g. `postgres://user:pass@host:port/dbname`) |
| `server__host` | Server bind host address |
| `server__port` | Server bind port number |
Create a `.env` file with your configuration values before running the server. The config crate loads defaults → `.env` → environment variables in that priority order.
## Coding Conventions
| Convention | Detail |
|---|---|
| **Indentation** | 4 spaces for Rust; 2 spaces for YAML/JSON/TOML |
| **File naming** | One struct/type per file. File name matches the type (e.g. `product_card.rs` for `ProductCard`) |
| **Module visibility** | `pub(crate)` for internal-only modules; `pub` for public-facing ones (`graphql/models`, `canonical`) |
| **Struct naming** | PascalCase (e.g. `ProductCard`, `MeilisearchRepository`, `AppConfig`) |
| **Enum naming** | PascalCase struct names, uppercase variants (e.g. `ProductTagStyle::STANDARD`) |
| **Field naming** | snake_case (e.g. `product_id`, `price_as_cents`, `invoice_date`) |
| **Derive macros** | GraphQL objects: `#[derive(Debug, Deserialize, Serialize, GraphQLObject)]`; Enums: `GraphQLObject``GraphQLEnum` |
| **Error handling** | `Box<dyn Error>` in repository layer; context errors propagated via `FieldResult` |
| **Async traits** | Repository interfaces use `#[async_trait]` for async method definitions |
| **Configuration loading** | Use `config` crate builder: defaults → `.env` (via dotenvy) → environment variables (`__` separator). E.g. `server__host` maps to `server.host` |
| **Repository pattern** | Abstract interface (`trait`) in `infra/repositories/traits/`, concrete impl in `infra/repositories/`. Client wrapper (e.g. `MeilisearchClient`) is a separate struct that wraps `reqwest::Client` with auth headers |
| **Context object** | Single `AppContext` struct passed to all GraphQL resolvers; holds `AppConfig` + repository clones |
## Domain Concepts
When working with the BDP, agents should be aware of key domain concepts defined in [`DOMAIN.md`](./DOMAIN.md):
- **_underscore fields** — System-managed metadata prefixed with underscore (`_id`, `_entered.by`, `_modified.ts`, `_total`, `_active`). Never set by agents directly.
- **Reference resolution pattern** — All entity references follow `reference._id` → query the referenced entity. See DOMAIN.md for full reference resolution guide.
- **Default currency** — AUD (Australian Dollar) across the platform.
- **Business Unit & Location allocation** — Every participating entity must be assigned to a Business Unit and often a Location (required on save).
- **Master/Suborder aggregation** — Orders can be grouped hierarchically; Master orders aggregate totals from linked Suborders.
- **Classification/Profile pattern** — Customers and Products use extendable schema-driven classification via `classification.name`, `classification.profile._id`, `classification.profile.fields`.
## Authentication and Authorization
This API does not specify authentication or authorization mechanisms in the requirements, but AI agents would typically require appropriate access tokens or credentials to perform operations.
---
# Shoppy BFF (`~/Projects/shoppy/bff`)
This repository is a **Backend For Frontend (BFF)** GraphQL service for the Shoppy mobile app. It aggregates data from external search services and exposes it via a single GraphQL endpoint, serving as an API gateway between the frontend apps (iOS/Android) and backend infrastructure.
## Available Operations
AI agents can perform the following operations via its `/graphql` endpoint:
### Queries
- `productList(listType, argument, page_number?, facets?, sort_by?)` — Search or browse products with faceted filtering and sorting
- `app()` — Retrieve app configuration including store selection pages, product pages, and account menu
- `querySuggestions(query_term)` — Get search autocomplete suggestions
- `cartProducts(input)` — Resolve cart items by product IDs (returns product cards or not-found placeholders)
- `smartSwaps(input)` — Fetch "swap and save" alternative products for given cart items
### Mutations
- `submitFeedback(feedback)` — Submit user feedback (stub implementation, always returns true)
## Technical Implementation
The service is built using:
- Rust programming language (**edition 2024**)
- **Juniper** GraphQL framework (`juniper`, `juniper_actix`)
- **Actix-web** HTTP server (`actix-web`, `actix-files`, `actix-cors`)
- Tokio async runtime
- `config` crate for configuration (`.env` + environment variables with `__` separator)
- `dotenvy` for `.env` file loading
- `thiserror` for error types
- `tracing` / `tracing-subscriber` / `tracing-actix-web` for logging and request tracing
- `async_trait` for async trait implementations
- `serde` / `serde_json` for serialization
## Folder Structure
```
src/
├── main.rs # Entry point: server boot, schema registration, middleware chain
├── mod.rs # Root module declarations (pub(crate) vs pub)
├── app_config.rs # Configuration struct + loading logic (config crate builder pattern)
├── graphql/ # GraphQL layer — public-facing schema & resolvers
│ ├── schema.rs # Schema type alias + factory function
│ ├── mod.rs
│ ├── models/ # GraphQL output types (PascalCase structs/enums, all #[derive(GraphQLObject)])
│ │ └── mod.rs # One file per model struct/type
│ ├── queries/
│ │ ├── query_root.rs # QueryRoot struct + #[juniper::graphql_object] impl
│ │ ├── helpers.rs # Shared resolver helpers (create_product_list, facet utilities)
│ │ └── resolvers/ # Per-domain resolver modules (app_content_resolvers, etc.)
│ └── mutations/
│ └── mutation_root.rs # MutationRoot struct + #[juniper::graphql_object] impl
├── infra/ # Infrastructure layer — external service integrations
│ ├── mod.rs
│ ├── models/ # Internal domain types (infrastructure response structs, helpers)
│ │ └── helpers/ # Helper modules (category_helpers, image_helpers, pricing_helpers, stores_helpers)
│ └── repositories/
│ ├── meilisearch_client.rs # reqwest Client wrapper with auth headers
│ ├── meilisearch_repository.rs # Main repository implementation
│ ├── meilisearch_helpers.rs # Query-building helpers (apply_facets, create_filters)
│ ├── traits/ # Abstract repository interface
│ └── mod.rs
├── canonical/ # Canonical / shared domain models (clean types without infra coupling)
│ └── models/
│ ├── search.rs # SortOption, SearchFacet — pure domain structs
│ └── mod.rs
└── middleware/ # HTTP middleware
├── shoppy_api_key.rs # API-key validation (checks shoppy-api-key header against config)
└── mod.rs
```
## Coding Conventions
| Convention | Detail |
|---|---|
| **Indentation** | 4 spaces for Rust; 2 spaces for YAML/JSON/TOML; tabs for Makefiles |
| **File naming** | One struct/type per file. File name matches the type (e.g. `product_card.rs` for `ProductCard`) |
| **Module visibility** | `pub(crate)` for internal-only modules; `pub` for public-facing ones (`graphql/models`, `canonical`) |
| **Struct naming** | PascalCase (e.g. `ProductCard`, `MeilisearchRepository`, `AppConfig`) |
| **Enum naming** | PascalCase struct names, uppercase variants (e.g. `ProductTagStyle::STANDARD`) |
| **Field naming** | snake_case (e.g. `product_id`, `price_as_cents`, `meilisearch_url`) |
| **Derive macros** | GraphQL objects: `#[derive(Debug, Deserialize, Serialize, GraphQLObject)]`; Enums: `GraphQLObject``GraphQLEnum` |
| **Error handling** | `Box<dyn Error>` in repository layer; context errors propagated via `FieldResult` |
| **Async traits** | Repository interfaces use `#[async_trait]` for async method definitions |
| **Configuration loading** | Use `config` crate builder: defaults → `.env` (via dotenvy) → environment variables (`__` separator). E.g. `server__host` maps to `server.host` |
| **Logging (current)** | Unstructured `println!("[INFO] -- ...")` across ~40 call sites; `tracing-actix-web::TracingLogger` wired as middleware but silent due to missing subscriber init. Requirement: migrate to structured `tracing` with `EnvFilter`, JSON output in production, compact output locally, and activate request-level tracing |
| **Repository pattern** | Abstract interface (`trait`) in `infra/repositories/traits/`, concrete impl in `infra/repositories/`. Client wrapper (e.g. `MeilisearchClient`) is a separate struct that wraps `reqwest::Client` with auth headers |
| **Context object** | Single `AppContext` struct passed to all GraphQL resolvers; holds `AppConfig` + repository clones |
| **Docker build** | Multi-stage using `cargo-chef` for dependency caching: chef → planner (recipe) → builder (cook + build) → runtime (debian slim, binary only) |
## Configuration
Environment variables use the `__` separator convention:
- `server__host`, `server__port` — server bind settings
- `proxy__http`, `proxy__https` — proxy settings (optional)
- `auth__sx_ios_api_key`, `auth__android_sx_api_key` — API keys for iOS/Android app authentication
## Authentication
The BFF validates a `shoppy-api-key` header against the configured `auth.sx_ios_api_key` or `auth.android_sx_api_key`. The `/graphql` endpoint and `/healthz` are exempt from this check. GraphQL itself does not require auth — only non-GraphQL routes do.
## Logging Requirement
### Current state
All logging uses raw `println!("[INFO] -- ...")` with no timestamps, log levels, or field context (~40 call sites). This makes logs unparseable and hard to filter in production. The project already has the right crates (`tracing`, `tracing-subscriber`, `tracing-actix-web`) but they are unused.
### Required approach: layered structured tracing
| Layer | Purpose |
|---|---|
| **EnvFilter** | Runtime log level control via `RUST_LOG` env var (e.g. `shoppy_bff=debug`, `reqwest=warn`) |
| **Fmt layer — JSON in prod, Compact in dev** | Structured logs for production aggregation; readable compact output locally |
| **tracing-actix-web::TracingLogger** | Middleware auto-logs every HTTP request (method, path, status, latency) |
### Implementation notes
- Replace `println!("[INFO] -- ...")``tracing::info!(...)` with structured fields (e.g., `tracing::error!(status = ?response_status, "Error getting products by search")`)
- Initialize subscriber in `main.rs`:
```rust
let fmt_layer = tracing_subscriber::fmt::layer().with_target(false);
let filter = EnvFilter::try_from_default_env()
.or_else(|_| EnvFilter::try_new("info")).unwrap();
#[cfg(debug_assertions)]
let fmt_layer = fmt_layer.pretty();
#[cfg(not(debug_assertions))]
let fmt_layer = fmt_layer.json();
tracing_subscriber::registry()
.with(filter)
.with(fmt_layer)
.init();
```
- The `TracingLogger` middleware would then actually emit request traces (currently silent)
### Scope
- **In scope**: replace all `println!` logging, init subscriber in `main.rs`, activate TracingLogger output
- **Out of scope**: OpenTelemetry integration, log crate bridge, multi-layer stdout/JSON split — these can be added later if log aggregation becomes a requirement