- 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.
322 lines
No EOL
16 KiB
Markdown
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 |