Strategy Pattern¶
Every business decision point in Vectis is an abstract strategy interface. The core engine calls strategies; extensions and modules provide implementations.
Strategy ABCs¶
| Strategy | Purpose | Resolution |
|---|---|---|
PriceCalculation | Tiered / contract pricing | resolve |
TaxCalculation | Tax rates and remittance | resolve_all |
ShippingCalculator | Rate calculation | resolve_all |
ShippingEligibility | Method eligibility rules | resolve_all |
InventoryAllocation | Stock reservation | resolve |
PaymentProcess | Authorize, capture, charge, void, refund | resolve |
PaymentEligibility | Payment method filtering | resolve_all |
OrderProcess | Order state machine hooks | resolve |
SearchIndexing | Full-text index sync | resolve |
FraudScoring | Risk evaluation | resolve |
EmailDelivery | Transactional email | resolve |
SmsDelivery | Transactional SMS | resolve (via SmsDeliveryStrategy in notification/sms_strategies.py) |
CommissionCalculation | Sales rep commissions | resolve |
ReturnProcess | RMA / return workflow | resolve |
CatalogVisibility | Account-level product access | resolve_all |
PaymentProcessStrategy¶
The payment strategy is the most feature-rich strategy ABC. It defines the full payment lifecycle:
class PaymentProcessStrategy(ABC):
@property
@abstractmethod
def gateway_name(self) -> str: ...
@abstractmethod
async def authorize(self, amount, currency, payment_data) -> PaymentResult: ...
@abstractmethod
async def capture(self, transaction_id, amount) -> PaymentResult: ...
@abstractmethod
async def refund(self, transaction_id, amount) -> PaymentResult: ...
async def charge(self, amount, currency, payment_data) -> PaymentResult:
"""Auth + capture in one call. Default: authorize then capture."""
...
async def void(self, transaction_id) -> PaymentResult:
"""Void an authorized but uncaptured transaction."""
...
async def approve_held_transaction(self, transaction_id) -> PaymentResult:
"""Release a transaction held by fraud filters."""
...
The PaymentResult dataclass includes a held_for_review flag for fraud filter detection:
@dataclass
class PaymentResult:
success: bool
transaction_id: str | None = None
error_message: str | None = None
gateway_response: dict | None = None
held_for_review: bool = False
Creating a Strategy¶
Define an ABC, then register implementations with strategy_resolver.
from abc import ABC, abstractmethod
from decimal import Decimal
class TaxCalculationStrategy(ABC):
@abstractmethod
async def calculate(self, line_items: list[dict], address: dict) -> Decimal: ...
Register a default at startup:
from vectis.core.strategy import strategy_resolver
class SimpleSalesTax(TaxCalculationStrategy):
async def calculate(self, line_items, address):
subtotal = sum(i["price"] * i["qty"] for i in line_items)
return subtotal * Decimal("0.08")
strategy_resolver.register(TaxCalculationStrategy, SimpleSalesTax(), name="sales_tax")
Single vs Multi Resolution¶
resolve — returns the last-registered implementation (or a named one). Use for single-active-strategy decision points:
price_strategy = strategy_resolver.resolve(PriceCalculationStrategy)
price = await price_strategy.calculate(variant_id, account_id)
resolve_all — returns every implementation. Use when multiple strategies combine results (e.g., tax jurisdictions):
tax_strategies = strategy_resolver.resolve_all(TaxCalculationStrategy)
total_tax = sum([await s.calculate(items, addr) for s in tax_strategies])
Channel-Scoped Filtering¶
Extensions are activated per-channel via ChannelExtension. Pass channel_enabled to filter strategies at resolution time:
from vectis.core.strategy import _load_channel_extensions
enabled = await _load_channel_extensions(channel_id)
strategies = strategy_resolver.resolve_all(TaxCalculationStrategy, channel_enabled=enabled)
Core defaults (no extension_name) always pass the filter.
Note
The channel extension cache is invalidated when an admin toggles an extension. Call invalidate_channel_extension_cache(channel_id) if you modify ChannelExtension rows programmatically.
Overriding a Default¶
Extensions override by registering the same strategy type. resolve() returns the last-registered implementation, so extensions loaded after core win:
strategy_resolver.register(
TaxCalculationStrategy, ExciseIQTax(), name="exciseiq", extension_name="exciseiq",
)
Warning
Two extensions registering the same strategy type: last wins. Use conflicts_with in ExtensionManifest to declare mutual exclusivity.