Commerce Architecture
This document describes the technical architecture of the core-commerce package, which provides billing, subscriptions, and payment processing for the Host UK platform.
Overview
The commerce module implements a multi-gateway payment system supporting cryptocurrency (BTCPay) and traditional card payments (Stripe). It handles the complete commerce lifecycle from checkout to recurring billing, dunning, and refunds.
┌─────────────────────────────────────────────────────────────────┐
│ Commerce Module │
├─────────────────────────────────────────────────────────────────┤
│ Services Layer │
│ ┌─────────────┐ ┌──────────────┐ ┌───────────────┐ │
│ │ Commerce │ │ Subscription │ │ Dunning │ │
│ │ Service │ │ Service │ │ Service │ │
│ └─────────────┘ └──────────────┘ └───────────────┘ │
│ ┌─────────────┐ ┌──────────────┐ ┌───────────────┐ │
│ │ Invoice │ │ Coupon │ │ Tax │ │
│ │ Service │ │ Service │ │ Service │ │
│ └─────────────┘ └──────────────┘ └───────────────┘ │
├─────────────────────────────────────────────────────────────────┤
│ Gateway Layer │
│ ┌──────────────────────┐ ┌──────────────────────┐ │
│ │ BTCPayGateway │ │ StripeGateway │ │
│ │ (Primary) │ │ (Secondary) │ │
│ └──────────────────────┘ └──────────────────────┘ │
│ │ │ │
│ └────────────┬─────────────┘ │
│ │ │
│ ┌────────────▼─────────────┐ │
│ │ PaymentGatewayContract │ │
│ └──────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘Core Concepts
Orderable Interface
The commerce system uses polymorphic relationships via the Orderable contract. Both Workspace and User models can place orders, enabling:
- Workspace orders: Subscription packages, team features
- User orders: Individual boosts, one-time purchases
interface Orderable
{
public function getBillingName(): string;
public function getBillingEmail(): string;
public function getBillingAddress(): array;
public function getTaxCountry(): ?string;
}Order Lifecycle
┌──────────┐ ┌────────────┐ ┌──────────┐ ┌────────┐
│ pending │───▶│ processing │───▶│ paid │───▶│refunded│
└──────────┘ └────────────┘ └──────────┘ └────────┘
│ │
│ │
▼ ▼
┌──────────┐ ┌──────────┐
│cancelled │ │ failed │
└──────────┘ └──────────┘- pending: Order created, awaiting checkout
- processing: Customer redirected to payment gateway
- paid: Payment confirmed, entitlements provisioned
- failed: Payment declined or expired
- cancelled: Customer abandoned checkout
- refunded: Full refund processed
Subscription States
┌────────┐ ┌──────────┐ ┌────────┐ ┌───────────┐
│ active │───▶│ past_due │───▶│ paused │───▶│ cancelled │
└────────┘ └──────────┘ └────────┘ └───────────┘
│ │ │
▼ │ │
┌──────────┐ │ │
│ trialing │────────┘ │
└──────────┘ │
│ │
└─────────────────────────────┘- active: Subscription in good standing
- trialing: Within trial period (no payment required)
- past_due: Payment failed, within retry window
- paused: Billing paused (dunning or user-initiated)
- cancelled: Subscription ended
Service Layer
CommerceService
Main orchestration service. Coordinates order creation, checkout, and fulfillment.
// Create an order
$order = $commerce->createOrder($workspace, $package, 'monthly', $coupon);
// Create checkout session (redirects to gateway)
$checkout = $commerce->createCheckout($order, 'btcpay', $successUrl, $cancelUrl);
// Fulfill order after payment (called by webhook)
$commerce->fulfillOrder($order, $payment);Key responsibilities:
- Gateway selection and initialization
- Customer management across gateways
- Order-to-entitlement provisioning
- Currency formatting and conversion
SubscriptionService
Manages subscription lifecycle without gateway interaction.
// Create local subscription record
$subscription = $subscriptions->create($workspacePackage, 'monthly');
// Handle plan changes with proration
$result = $subscriptions->changePlan($subscription, $newPackage, prorate: true);
// Pause/unpause with limits
$subscriptions->pause($subscription);
$subscriptions->unpause($subscription);Proration calculation:
creditAmount = currentPrice * (daysRemaining / totalPeriodDays)
proratedNewCost = newPrice * (daysRemaining / totalPeriodDays)
netAmount = proratedNewCost - creditAmountDunningService
Handles failed payment recovery with exponential backoff.
Day 0: Payment fails → subscription marked past_due
Day 1: First retry
Day 3: Second retry
Day 7: Third retry → subscription paused
Day 14: Workspace suspended (features restricted)
Day 30: Subscription cancelledConfiguration in config.php:
'dunning' => [
'retry_days' => [1, 3, 7],
'suspend_after_days' => 14,
'cancel_after_days' => 30,
'initial_grace_hours' => 24,
],TaxService
Jurisdiction-based tax calculation supporting:
- UK VAT (20%)
- EU VAT via VIES validation
- US state sales tax (nexus-based)
- Australian GST (10%)
B2B reverse charge is applied automatically when a valid VAT number is provided for EU customers.
$taxResult = $taxService->calculate($workspace, $amount);
// Returns: TaxResult with taxAmount, taxRate, jurisdiction, isExemptPayment Gateways
PaymentGatewayContract
All gateways implement this interface ensuring consistent behavior:
interface PaymentGatewayContract
{
// Identity
public function getIdentifier(): string;
public function isEnabled(): bool;
// Customer management
public function createCustomer(Workspace $workspace): string;
// Checkout
public function createCheckoutSession(Order $order, ...): array;
public function getCheckoutSession(string $sessionId): array;
// Payments
public function charge(Workspace $workspace, int $amountCents, ...): Payment;
public function chargePaymentMethod(PaymentMethod $pm, ...): Payment;
// Subscriptions
public function createSubscription(Workspace $workspace, ...): Subscription;
public function cancelSubscription(Subscription $sub, bool $immediately): void;
// Webhooks
public function verifyWebhookSignature(string $payload, string $sig): bool;
public function parseWebhookEvent(string $payload): array;
}BTCPayGateway (Primary)
Cryptocurrency payment gateway supporting BTC, LTC, XMR.
Characteristics:
- No saved payment methods (each payment is unique)
- No automatic recurring billing (requires customer action)
- Invoice-based workflow with expiry
- HMAC signature verification for webhooks
Webhook Events:
InvoiceCreated→ No actionInvoiceReceivedPayment→ Order status: processingInvoiceProcessing→ Waiting for confirmationsInvoiceSettled→ Fulfill orderInvoiceExpired→ Mark order failed
StripeGateway (Secondary)
Traditional card payment gateway.
Characteristics:
- Saved payment methods for recurring
- Automatic subscription billing
- Setup intents for card-on-file
- Stripe Customer Portal integration
Webhook Events:
checkout.session.completed→ Fulfill orderinvoice.paid→ Renew subscriptioninvoice.payment_failed→ Trigger dunningcustomer.subscription.deleted→ Revoke entitlements
Data Models
Entity Relationship
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Workspace │────▶│ Order │────▶│ OrderItem │
└─────────────┘ └─────────────┘ └─────────────┘
│ │
│ ▼
│ ┌─────────────┐ ┌─────────────┐
│ │ Invoice │────▶│InvoiceItem │
│ └─────────────┘ └─────────────┘
│ │
│ ▼
│ ┌─────────────┐ ┌─────────────┐
└───────────▶│ Payment │────▶│ Refund │
└─────────────┘ └─────────────┘
│
▼
┌─────────────┐ ┌─────────────┐
│ Coupon │────▶│ CouponUsage │
└─────────────┘ └─────────────┘Multi-Entity Commerce (M1/M2/M3)
The commerce module supports a hierarchical entity structure:
- M1 (Master Company): Source of truth, owns product catalog
- M2 (Facade/Storefront): Selects from M1 catalog, can override content
- M3 (Dropshipper): Full inheritance, no management responsibility
┌─────────┐
│ M1 │ ← Product catalog owner
└────┬────┘
│
┌────────┴────────┐
│ │
┌─────▼─────┐ ┌─────▼─────┐
│ M2 │ │ M2 │ ← Storefronts
└─────┬─────┘ └───────────┘
│
┌─────▼─────┐
│ M3 │ ← Dropshipper
└───────────┘Permission matrix controls which operations each entity type can perform, with a "training mode" for undefined permissions.
Event System
Domain Events
// Dispatched automatically on model changes
SubscriptionCreated::class → RewardAgentReferralOnSubscription
SubscriptionRenewed::class → ResetUsageOnRenewal
OrderPaid::class → CreateReferralCommissionListeners
ProvisionSocialHostSubscription: Product-specific provisioning logicRewardAgentReferralOnSubscription: Attribute referral for new subscriptionsResetUsageOnRenewal: Clear usage counters on billing period resetCreateReferralCommission: Calculate affiliate commission on paid orders
Directory Structure
core-commerce/
├── Boot.php # ServiceProvider, event registration
├── config.php # All configuration (currencies, gateways, tax)
├── Concerns/ # Traits for models
├── Console/ # Artisan commands (dunning, reminders)
├── Contracts/ # Interfaces (Orderable)
├── Controllers/ # HTTP controllers
│ ├── Api/ # REST API endpoints
│ └── Webhooks/ # Gateway webhook handlers
├── Data/ # DTOs and value objects
├── Events/ # Domain events
├── Exceptions/ # Custom exceptions
├── Jobs/ # Queue jobs
├── Lang/ # Translations
├── Listeners/ # Event listeners
├── Mail/ # Mailable classes
├── Mcp/ # MCP tool handlers
├── Middleware/ # HTTP middleware
├── Migrations/ # Database migrations
├── Models/ # Eloquent models
├── Notifications/ # Laravel notifications
├── routes/ # Route definitions
├── Services/ # Business logic layer
│ └── PaymentGateway/ # Gateway implementations
├── tests/ # Pest tests
└── View/ # Blade templates and Livewire components
├── Blade/ # Blade templates
└── Modal/ # Livewire components (Admin/Web)Configuration
All commerce configuration lives in config.php:
return [
'currency' => 'GBP', // Default currency
'currencies' => [...], // Supported currencies, exchange rates
'gateways' => [
'btcpay' => [...], // Primary gateway
'stripe' => [...], // Secondary gateway
],
'billing' => [...], // Invoice prefixes, due days
'dunning' => [...], // Retry schedule, suspension timing
'tax' => [...], // Tax rates, VAT validation
'subscriptions' => [...], // Proration, pause limits
'checkout' => [...], // Session TTL, country restrictions
'features' => [...], // Toggle coupons, refunds, trials
'usage_billing' => [...], // Metered billing settings
'matrix' => [...], // M1/M2/M3 permission matrix
];Testing
Tests use Pest with RefreshDatabase trait:
# Run all tests
composer test
# Run specific test file
vendor/bin/pest tests/Feature/CheckoutFlowTest.php
# Run tests matching pattern
vendor/bin/pest --filter="proration"Test categories:
CheckoutFlowTest: End-to-end order flowSubscriptionServiceTest: Subscription lifecycle, prorationDunningServiceTest: Payment recovery flowsWebhookTest: Gateway webhook handlingTaxServiceTest: Tax calculation, VAT validationCouponServiceTest: Discount applicationRefundServiceTest: Refund processing