Changelog¶
Phase 15 — B2B Workflow, Inventory Lifecycle, Refund Approvals (2026-05, Current)¶
A 236-commit wave covering the B2B four-phase approval workflow, packaging + MMOQ, the inventory state machine + external-fulfillment handoff, refund approvals with durable Temporal execution, product labels with predicates, AI parse-to-cart, and the extension self-registration completeness story.
B2B Cart / Order Approval (Rounds 1–3 + C-series, Decided #200, C1–C16)¶
- Multi-phase workflow (
cart+paymentapproval phases) with rejections written toCartRejectionEventand faults emitted onvectis.workflow.fault.v1(Decided #200) - Cart-level split-tender composer with
PaymentTender.sourceprovenance column (cascadevsbuyer_prepended, Decided C12) - Stale-approval pricing-drift gate —
Cart.cart_approved_grand_totalis compared against the live grand total at place-order; mismatch rejects placement (Decided C8) - Inventory revalidation gate at placeOrder — re-checks inventory under a serializable transaction; on drift the cart transitions to
cart_approved_blocked_inventorystatus (Decided C9) recurateCascade(orderId, newCascade: JSON!, reason?)lets an approver re-curate the tender cascade per-order after a fault (Decided C4)- Multi-approver archival quorum preservation so removing one approver doesn't disable an in-flight approval (Decided C1)
- Refund cap invariant + recurring auto-pause (Decided C6, C14)
- Card-auth expiry void daily sweep —
VoidExpiringCardAuthsWorkflowvoids authorized card legs that aged past their gateway expiry, including ACH-paired tenders that hadn't settled in time (Decided C10)
Inventory, Packaging, MMOQ (Decided #25, #160, #161, #172, #234, #235, #236)¶
- Packaging UoM ladder —
packages+product_packagestables with per-product slug/multiplier; cart and order snapshots store the chosen package; reorder respects the original slug and flagsunit_changedwhen the ladder shifts (#235) - MMOQ caps on
ProductVariant—max_per_customer_per_order,max_per_customer_30d,max_backorder_qty,mmoq_display_unit_id; cart-aggregate enforcement; channel-timezone 30-day window; serializable re-validation at finalize;MmoqViolationDetailreturned oncartBulkLookuprows (#236) - Reservation state machine — HELD → CONFIRMED → RELEASED / EXPIRED / FULFILLED with TTL expiry via
ExpireStaleReservationsWorkflow(#160) - External stock push —
pushExternalStockAPI-key-scoped mutation; tracked products update reservable inventory, untracked products only setVariant.external_stock_snapshotfor audit (#161, #234) - Inventory state version —
Product.inventory_state_versionbumps on policy change and gates place-order races (#234) - External fulfillment handoff —
acknowledgeOrderExternalHandoff(orderId, externalReference?)releases internal holds so the ERP becomes the system of record (#172) Order.inventory_risk_flagauto-fires on oversold;orders(inventoryRiskOnly:)filter for admin triageclearProductInventoryState(productId): ClearProductInventoryStateResult!releases reservations;clearProductBackorders(productId)unblocks order resumptionOrderLineItem.tracking_enabled_at_checkoutcaptures the tracking flag at place-order time so fulfillment isn't surprised by a live toggle
Refund Approval (OQ #26 / #27)¶
- Refund Approvals Inbox in admin (
/refund-approvals) — staff submits viasubmitRefundForApproval(orderId, input), approver decides viadecideRefundApproval(refundRequestId, approved, notes?) - Self-approval blocked at the API; per-tender progress tracking in the inbox
- Refund execution is a durable Temporal
RefundExecutionWorkflow(backend/vectis/modules/refund_approval/workflows.py); faults emit onvectis.workflow.fault.v1;retryRefundExecution(refundRequestId)is idempotent against empty-transaction states
Product Labels (Decided #228)¶
- Predicate-driven labels rendered on PDP, related products, recently-viewed, search results, and catalog cards
- Batch evaluation (
evaluate_many+supports_batchflag) so hundreds of labels evaluate against thousands of products in one pass - Stale reference GC via
clearProductLabelStaleReferences(extensionName: String!): Int!mutation — scoped per extension; predicate namespacing keeps cross-extension predicates from colliding
Cart Bulk Ops + AI Parse-to-Cart¶
- Bulk select + delete + save-for-later with
bulkRemoveCartLines(lineIds),bulkSaveCartLinesForLater(lineIds),restoreSavedCartLine(Decided #230 —unit_package_idpersists across save→restore) - Quick Order CSV/paste with
cartBulkLookuppreview (MMOQ violations, packaging hints,unitChanged) and partial-successbulkAddToCart(Decided #231) - AI parse-to-cart from text or photo —
cartAiParseToLookupItems(input: CartAIParseInput!); result feedscartBulkLookup→bulkAddToCart;cart_ai_provider_prioritySetting controls provider order (Decided #232)
Extensions Plug-In Completeness (Decided #222–#231 + #233)¶
- Self-registration for goshippo, omnisend, shipstation; new slots for CarrierSeed, SavedPayment, AI providers, notification templates, Cmd-K Quick Actions, federated search, page tabs, nav, models, GraphQL queries / mutations
- ExtensionRegistry._activate consults
InstallStateService(Decided #148) — install-state persistence, version detection,on_upgradehook - Zero-extension-imports invariant enforced via AST guard (Decided #233) — core never imports
ext_* - Worker readiness gate — refuses to start if an enabled payment method has no registered strategy (strict mode default)
ext_excise_engineswap — decommissionedext_exciseiq(Decided #225) with a one-click config migration- TaxJar real integration — calculation, filing workflow, order sync (replaces stub)
- J'AI →
ext_jai_chat—support_chatreduced to strategy ABCs only; the extension contributes models, GraphQL, admin tab, and a session-cleanup schedule - Cmd-K Quick Actions + federated search (Decided #226, #227) — extensions contribute palette entries and async search indexes; merged into the global picker
Observability + DR (Decided #153, #154, #157, #158)¶
/metrics(Prometheus viaprometheus-fastapi-instrumentator) and/ready(parallel DB/Redis/Redpanda probes);/healthis the cheap liveness check- Token-bucket rate limiter — burst is the ceiling, per-endpoint policies in
rate_limit_policies, attributes byX-Forwarded-For(Decided #157, #165) - JSON Schema event registry under
backend/vectis/events/schemas/(Decided #153); seevectis/docs/REDPANDA_TOPICS.md - DR runbook + reference
backend/scripts/backup_postgres.sh+verify_backup_restore.pyweekly verify (Decided #158)
Other Platform Polish¶
- Fernet-encrypted secrets with
SECRETS_MASTER_KEY+SECRETS_MASTER_KEY_ROTATING_FROMrotation (Decided #155) - Per-gateway settlement webhook dispatcher with multi-secret rotation window (Decided #198, OQ #28)
- Payment gateway capability introspection —
paymentMethodCapabilitiesquery with orphaned + disabled filters (Decided #233) - Money precision validation —
money.decimal_placessave-time + cache invalidation (Decided #104) - Automated promotion override invalidation (Decided #168) — stale rules flag automatically
- Phase-based promotion engine —
stack_group,$0floor, order-discount distribution (Decided #162, #170, #171); enforcement-vs-observability usage writes (#152) - Stage-sorted tax engine with order-subtotal redistribution (Decided #151)
- CMS block templates + ProductTranslation overlay with cache contract (Decided #145, #146, #156)
- Split CSP for CMS embeds (Decided #177)
- Multi-factor affiliate fraud guard + suspicious review (Decided #179)
- Location archive + 3-way employee reassignment (Decided #166); storefront active-location switcher (Decided #167); channel context via
X-Channel-Slugheader forwarded by the BFF (Decided #164 — header-based; admin/storefront routes stay flat) - BFF
X-Forwarded-For+ Uvicorn--proxy-headersfor correct audit/rate-limit attribution (Decided #165) - Order reissue —
reissueOrderandreissueShipmentcreate new linked orders so ERP pollers pick them up; original order untouched (Order columns:is_reissue,reissued_from_order_id,reissue_reason) - Background shipping rate precompute with explicit cache-hash composition, cubing dedup, untracked-item filter, asyncio yield (Round 6 shipping)
- Allow store-credit overdraft (Decided #173) —
GcExpiredOverdraftDraftsWorkflowreaps unredeemed drafts - Cookie consent rewrite — close 7 audit gaps + always-show granular banner
- A11y warning sweep — 6 critical errors fixed, 50 warnings cleared
- Tests at 1,670 across backend + storefront/admin Playwright;
make checkis the local gate (Phase D regression guard + schema-drift guard) - CI currently disabled in favor of local
make check(2026-05-18); push directly to main when the local gate passes
Platform — Houdini Frontend Migration & CI Pipeline¶
- Migrated both admin and storefront apps to Houdini (
houdini1.5 +houdini-svelte2.1) as the sole GraphQL client; every page's ops live insrc/lib/houdini/<PageName>.tsand consume generated*Storeclasses from$houdini(Decided #119) - Deleted the hand-rolled
$lib/api.ts(admin) and server-sidegql()helper in$lib/server/api.ts(storefront); the storefront's$lib/server/api.tsnow exposes only anapiHeaders()builder for the BFF forwarder - Page loaders standardized on
+page.ts+load_<QueryName>(no more+page.server.tsfor data loading); mutations usenew <Op>Store().mutate(...); refetches use<QueryStore>.fetch()instead ofinvalidateAll() - BFF
/api/graphqlis a CSRF-guarded forwarder that attaches the JWT from the session cookie server-side — JWT never reaches the browser (Decided #88) - Added GitHub Actions CI (
.github/workflows/check.yml) with five jobs —backend-lint(ruff),backend-resolver-check(AST guard against duplicate Strawberry resolvers),backend-tests(pytest against real PG 16 + Redis),admin-check(houdini generate+svelte-check, 0 errors),storefront-check(same) - Backend cleanup landed during the ruff sweep: 10 F821 fixes, 3 B904
raise ... from, 2 B905zip(strict=True), a real B023 bug inschedules.py(closure captured last iteration), and 44 E741 renames
Phase 14 — Payment Lifecycle Rework¶
- Reworked payment processing to support configurable capture modes (authorize-only vs auth+capture)
- Added full payment lifecycle: authorize → capture → void → refund with per-transaction status and type tracking
- Automatic void/refund on order cancellation and refund transitions via state machine side effects
- Authorize.net FDS fraud filter detection: orders flagged as
HeldForReview, admin fraud hold release - Added
modifyOrdermutation for editing authorized orders with automatic reauthorization - Added
adminCreateOrdermutation and admin UI for staff to create orders with saved card charging - Saved payment methods now support B2B location scoping (
location_id) - Admin payments page now includes capture mode configuration (Authorize Only / Authorize + Capture)
- Added
PaymentTransaction.typefield (authorization,capture,charge,void,refund) - Added
Order.fraud_statusandOrder.fraud_detailsfields - Updated order detail page with fraud hold banner, transaction actions (capture, void), and release button
- Added admin Create Order page with account/channel selection, line items, saved cards, and capture mode
Phase 13 — Core Geocoding Service¶
- Added geographic data model (GeoUnion, Country, Region, County, City) with ISO standards
- Implemented IP geolocation, address geocoding, and address validation strategies
- Enriched RequestContext with IP and geo data
- Added admin UI for managing geographic data and geocoding settings
Phase 12 — Tax Architecture Rework¶
- Rebuilt tax calculation engine using geographic data models
- Added support for granular sales tax, excise tax, and compound tax
- Tax jurisdiction lookup by ISO region, county, and city
- Added admin Tax management page
Phase 11 — Documentation System¶
- Added MkDocs Material documentation site with developer, usage, and operations guides
- Added in-admin contextual help system (HelpDrawer + HelpTooltip components)
- Added structured, translatable help JSON files for all admin routes
- Added Cursor rule to enforce documentation updates alongside code changes
- Added GraphQL schema introspection script for auto-generated API reference
Phase 10 — Multi-Currency & Checkout Pipeline¶
- Implemented multi-currency support: channel-level supported currencies, exchange rates, currency-aware price resolution
- Added dual-amount orders (transacted + base currency)
- Wired end-to-end checkout pipeline: promotions → shipping → tax → payment → order creation
- Integrated promotion engine with cart-level coupon application and per-line discount allocation
- Added cart pessimistic locking via
SELECT FOR UPDATEduring checkout - Added Redpanda event publishing for
order.created,order.status_changed,inventory.adjusted - Added dedicated event consumer service in Docker Compose
Phase 9 — Internationalization & Channel Commerce Mode¶
- Implemented channel-based commerce modes: B2B, B2C, hybrid
- Added multi-language support with JSONB translations on products, categories, brands, and CMS pages
- Added locale resolution middleware (X-Locale header, cookie, Accept-Language)
- Added LanguageSwitcher and CurrencySwitcher storefront components
- Implemented channel-scoped extension activation via ChannelExtension model
Phase 8 — Payment & Shipping Extensions¶
- Built Authorize.Net CIM extension (PaymentProcessStrategy)
- Built gift card extension with virtual/physical cards, partial redemption, bulk generation
- Added UPS shipping rate extension
- Replaced inline SVG/emoji icons with Lucide icons across storefront and admin
- Added shipping carrier icon components (UPS, USPS, FedEx, OnTrac, freight)
- Added payment card brand icons (Visa, Mastercard, Amex, Discover)
Phase 7 — Testing & Production Prep¶
- Set up pytest with real PostgreSQL test database
- Added test fixtures: event_bus, strategy_resolver, request contexts
- Wrote test suites for strategy resolver, pricing, cart/order flow, GraphQL API, RBAC, events, extensions
- Created production-ready multi-stage Dockerfiles
- Added CMS policy pages: contact, shipping, return, privacy, terms
- Fixed accessibility warnings in storefront and admin layouts
Phase 6 — Admin Panel & Dashboard¶
- Built 20+ admin routes: orders, products, customers, pricing, promotions, inventory, shipping, tax, CMS, settings
- Implemented command palette (Cmd+K) with fuzzy search
- Added Customer 360 view
- Built collapsible sidebar navigation with Lucide icons
Phase 5 — Core Commerce Modules¶
- Implemented all 14 strategy ABCs with default implementations
- Built promotion engine with condition/action pattern and CartIndex
- Built fulfillment module with by_order and by_box modes
- Built RMA module with line-level approval
- Built store credit, gift card, and net terms modules
- Built reporting and audit modules
Phase 4 — Pricing & Inventory¶
- Implemented five-level pricing hierarchy
- Built price list management with scheduling and volume tiers
- Built inventory module with warehouses, stock levels, and reservations
- Implemented backorder support
Phase 3 — Orders & Cart¶
- Built configurable order state machine
- Implemented cart with per-location support and session-based guest carts
- Built checkout mutation with initial pipeline
- Fixed SQLAlchemy identity map caching issues
Phase 2 — Auth & Accounts¶
- Implemented BFF authentication pattern with httpOnly session cookies
- Built B2B account hierarchy: Account → Location → Employee → Address
- Implemented RBAC with role-based permissions
- Built customer group system
Phase 1 — Products & Catalog¶
- Built product module with variants, categories, and brands
- Implemented Meilisearch integration for full-text search
- Built catalog visibility rules engine
Phase 0 — Foundation¶
- Scaffolded monorepo: backend, admin, storefront, migrator
- Set up Docker Compose with PostgreSQL, Redis, Redpanda, Temporal, Meilisearch, MinIO
- Created FastAPI application factory with Strawberry GraphQL
- Created SvelteKit admin and storefront shells
- Set up Alembic migrations with initial schema
- Established Forge methodology artifacts: constitution, conventions, reconciliation