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.
Packaging & MMOQ (Decided #25, #228, #236)¶
Packaging and MMOQ ship as columns and helpers inside the existing product module — there's no standalone packaging module.
Packaging¶
Product.packaging_enabled toggles the per-product ladder. The packages table holds global package definitions (slug, label, multiplier, active); the product_packages join carries per-product overrides (sku, price, eaches conversion). CartLineItem.unit_package_id records the selected package; CartLineItem.unit_changed flags when reorder fell back because the original ladder shifted. OrderLineItem.product_snapshot.unit captures the full {package_id, slug, label, multiplier, unit_price_in_unit} snapshot at order time.
MMOQ¶
The caps live on ProductVariant: max_per_customer_per_order, max_per_customer_30d, max_backorder_qty, mmoq_display_unit_id. The cart aggregate at finalize re-validates inside a serializable transaction (Decided #236) and stamps refunded_quantity_eaches on refunds so a customer's effective window credits restored quantities. The storefront previews violations via MmoqViolationDetail returned on cartBulkLookup rows. The 30-day window honours channels.timezone.
Product Label Module (Decided #228)¶
Predicate-driven product labels evaluated in batches:
- Predicate language — namespaced JSON (e.g.,
{ "all": [{"channel.country": "US"}, {"product.price": {"<=": 10}}] }). Predicates are versioned; resolved instances carry the predicate version that produced them. - Batch evaluation —
evaluate_many(products, channel_id)andsupports_batchflag let the service evaluate hundreds of labels against thousands of products in one pass. - Stale references —
clearProductLabelStaleReferencesmutation garbage-collects resolved instances whose predicate version no longer matches; surfaced in the admin via a banner when stale rows exist. - Rendering —
ProductType.labelsis batch-loaded on lists; the storefront renders SVG components on PDP, related-products, recently-viewed, and search-result cards.
Refund Approval Module (OQ #26 / #27)¶
Two paired modules implement async, durable refund execution:
refund_approval—RefundRequestmodel, the admin inbox UI, self-approval guard, and the GraphQL surface (submitRefundForApproval,decideRefundApproval,retryRefundExecution).payment(workflow extension) —RefundExecutionWorkflowTemporal workflow with per-tender progress, idempotent retry, and empty-transaction handling. Faults emit onvectis.workflow.fault.v1; success emitsvectis.audit.refund_execution_completed.v1.
B2B Workflow¶
The B2B cart-approval workflow is composed across cart, order, payment, and notification. Module-owned tables that ship the contract:
cart—CartApprovalRequest,CartRejectionEvent,Cart.cart_approved_grand_total,Cart.peer_view_count. The cart'sstatusenum includescart_approved_blocked_inventoryfor the post-approval inventory-revalidation gate (Decided C9).order—Order.inventory_risk_flag,Order.external_handoff_acknowledged_at/external_handoff_reference, the reissue chain (is_reissue,reissued_from_order_id,reissue_reason),OrderLineItem.tracking_enabled_at_checkoutpayment—PaymentTender.sourceprovenance column (cascadevsbuyer_prepended, Decided C12), cascade re-curation helpersnotification— templates for cart-submitted, payment-composed, refund-decided, fault-occurred
There is no standalone workflow module — workflow faults are an event domain with the vectis.workflow.fault.v1 schema and a workflow_faults table; queries surface through the existing extension/admin schema. Protocol details and the C-series gates live in vectis/MINIFORGE.md.