Backend Modules¶
Vectis organizes business logic into self-contained modules under vectis/backend/vectis/modules/. Each module owns its tables, services, and GraphQL resolvers. Modules communicate through service interfaces — never direct cross-module DB access.
Module Structure¶
vectis/modules/product/
├── models.py # SQLAlchemy ORM models
├── services.py # Business logic (session-scoped)
├── resolvers.py # Strawberry GraphQL queries & mutations
├── types.py # Strawberry input/output types
├── dataloaders.py # DataLoader batching (optional)
└── strategies.py # Strategy implementations (optional)
Defining Models¶
All models inherit from Base and TimestampMixin. PKs must be BigInteger. Money columns use the shared MoneyColumn type (never Float).
from decimal import Decimal
from typing import Optional
from sqlalchemy import BigInteger, ForeignKey, String, Text
from sqlalchemy.dialects.postgresql import JSONB
from sqlalchemy.orm import Mapped, mapped_column, relationship
from vectis.core.database import Base, MoneyColumn, TimestampMixin
class Product(TimestampMixin, Base):
__tablename__ = "products"
id: Mapped[int] = mapped_column(BigInteger, primary_key=True)
name: Mapped[str] = mapped_column(String(500))
slug: Mapped[str] = mapped_column(String(500), unique=True)
custom_fields: Mapped[Optional[dict]] = mapped_column(JSONB, default=dict)
variants: Mapped[list["ProductVariant"]] = relationship(back_populates="product")
class ProductVariant(TimestampMixin, Base):
__tablename__ = "product_variants"
id: Mapped[int] = mapped_column(BigInteger, primary_key=True)
product_id: Mapped[int] = mapped_column(ForeignKey("products.id"), index=True)
sku: Mapped[str] = mapped_column(String(100), unique=True, index=True)
price: Mapped[Decimal] = mapped_column(MoneyColumn)
Note
pk_column() and money_column() helpers exist for terser definitions.
Writing a Service¶
Services accept an AsyncSession and encapsulate all DB access. Other modules call your service — they never import your models directly.
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from vectis.core.events import Event, event_bus
from vectis.core.exceptions import NotFoundError
from vectis.modules.product.models import Product
class ProductService:
def __init__(self, session: AsyncSession):
self.session = session
async def get_by_id(self, product_id: int) -> Product:
result = await self.session.execute(
select(Product).where(Product.id == product_id)
)
product = result.scalar_one_or_none()
if product is None:
raise NotFoundError("Product", product_id)
return product
Adding GraphQL Resolvers¶
Define Query / Mutation classes with Strawberry decorators, then register them in vectis/core/graphql.py via multiple inheritance.
import strawberry
from strawberry.types import Info
from vectis.core.deps import get_session
@strawberry.type
class ProductQuery:
@strawberry.field
async def product(self, info: Info, id: strawberry.ID) -> ProductType | None:
async with get_session(info) as session:
svc = ProductService(session)
p = await svc.get_by_id(int(id))
return ProductType(id=strawberry.ID(str(p.id)), name=p.name)
Register in vectis/core/graphql.py:
Cross-Module Communication¶
Warning
Never import another module's models.py or query its tables. Always call through the owning module's service.
# order/services.py — correct cross-module call
from vectis.modules.product.services import VariantService
class OrderService:
async def validate_line(self, session, variant_id: int):
svc = VariantService(session)
variant = await svc.get_by_id(variant_id)
Inventory module (Decided OQ-7, #113)¶
The inventory module manages multi-warehouse stock tracking with full audit trails.
Models¶
Warehouse— physical or logical stock locations with address, contact info, andpriorityfor allocation orderingStockLevel— per-variant per-warehouse stock:quantity_on_hand,quantity_reserved,quantity_availableStockReservation— temporary holds for carts/orders with expirationInventoryLot— batch/lot tracking with cost and FIFO orderingStockAdjustmentLog— immutable audit record: before/after qty, delta, reason, source, user_id
Service: InventoryService¶
adjust_stock/set_stock— single-variant updates, always writes toStockAdjustmentLogbulk_adjust_stock— batch updates for multiple variants at oncereserve/release_reservation— stock reservation lifecyclelist_stock_levels— paginated, enriched with product/variant info via joinsstock_levels_for_product— all variants for a single product (bulk editor)list_adjustments— paginated audit log with user email- Warehouse CRUD:
create_warehouse,update_warehouse,deactivate_warehouse,list_warehouses
GraphQL¶
Queries: warehouses, stockLevels (paginated), stockLevelsForProduct, stockAdjustmentLog
Mutations: createWarehouse, updateWarehouse, deactivateWarehouse, adjustStock, bulkAdjustStock, importInventory
All mutations require inventory.manage permission. All queries require inventory.view.
Registration module (Decided #114)¶
The registration module owns all B2B/B2C account onboarding: form builder, geo-conditional rules engine, document verification, e-signatures, invitation codes, and the registration submission lifecycle.
Models¶
RegistrationForm— named form definition with steps, sections, and fields. Supports channel scoping, commerce mode filtering, customer group assignment, and invitation code gating.RegistrationFormStep/RegistrationFormSection/RegistrationFormField— nested form structure for multi-step wizard rendering. Fields support 18+ types, conditional visibility, validation rules, andmaps_totargeting (account, address, meta, or document).RegistrationRule— geo-conditional rule withrule_type(require/block/exempt),geo_conditionsJSONB (countries, states, counties, cities, postal codes), andrequirementsJSONB with AND/OR logic groups.DocumentType— configurable document categories (e.g., business license, EIN). Controls allowed file types, size, expiration/number/authority fields.RegistrationDocumentUpload— uploaded document with type FK, AI confidence score, OCR extracted data, and review status.RegistrationVerificationRecord— AI/OCR verification result with strategy name, confidence, extracted fields, flags.AgreementTemplate— versioned agreement content with signature requirement toggle.SignatureRecord— captured e-signature (drawn/typed/checkbox/external) with IP, user agent, content hash (SHA-256).InvitationCode— access code with form/group assignment, max uses, expiration.RegistrationSubmission— full lifecycle (submitted → documents_pending → in_review → approved/denied/expired) with nested documents, signatures, verifications.
Strategies¶
DocumentVerificationStrategy(ABC) — AI document analysis. Default:ManualReviewStrategy(no-op).OCRExtractionStrategy(ABC) — extract fields from document images.ESignatureStrategy(ABC) — create/check/retrieve e-signatures. Default:BuiltInSignatureStrategy. Extensions:ext_docusign,ext_hellosign.
Temporal Workflows¶
RegistrationWorkflow— validate → create account → notify → wait for admin decision → activate/deny/expire.DocumentVerificationWorkflow— OCR extraction → AI verification → store results.
Permissions¶
registration.manage (form/rule/code CRUD), registration.review (approve/deny submissions).
Reporting module (Decided #82)¶
The reporting module owns SavedReport and ScheduledReport. ReportExecutionService maps report_type + JSON config to SQL aggregates (orders, line items, stock levels, tax). ReportingService handles CRUD for saved and scheduled definitions. GraphQL exposes runReport, exportReport, and scheduled-report mutations; run_scheduled_reports is a Temporal activity (daily schedule) that executes enabled schedules and logs results—email/S3 delivery can be added later.