Skip to content

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:

@strawberry.type
class Query(ProductQuery, CartQuery, OrderQuery, ...):
    ...

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, and priority for allocation ordering
  • StockLevel — per-variant per-warehouse stock: quantity_on_hand, quantity_reserved, quantity_available
  • StockReservation — temporary holds for carts/orders with expiration
  • InventoryLot — batch/lot tracking with cost and FIFO ordering
  • StockAdjustmentLog — immutable audit record: before/after qty, delta, reason, source, user_id

Service: InventoryService

  • adjust_stock / set_stock — single-variant updates, always writes to StockAdjustmentLog
  • bulk_adjust_stock — batch updates for multiple variants at once
  • reserve / release_reservation — stock reservation lifecycle
  • list_stock_levels — paginated, enriched with product/variant info via joins
  • stock_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, and maps_to targeting (account, address, meta, or document).
  • RegistrationRule — geo-conditional rule with rule_type (require/block/exempt), geo_conditions JSONB (countries, states, counties, cities, postal codes), and requirements JSONB 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.