403 lines
16 KiB
Markdown
403 lines
16 KiB
Markdown
|
|
---
|
||
|
|
title: Architecture
|
||
|
|
description: Technical architecture of the core-commerce package
|
||
|
|
updated: 2026-01-29
|
||
|
|
---
|
||
|
|
|
||
|
|
# 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
|
||
|
|
|
||
|
|
```php
|
||
|
|
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 │
|
||
|
|
└──────────┘ └──────────┘
|
||
|
|
```
|
||
|
|
|
||
|
|
1. **pending**: Order created, awaiting checkout
|
||
|
|
2. **processing**: Customer redirected to payment gateway
|
||
|
|
3. **paid**: Payment confirmed, entitlements provisioned
|
||
|
|
4. **failed**: Payment declined or expired
|
||
|
|
5. **cancelled**: Customer abandoned checkout
|
||
|
|
6. **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.
|
||
|
|
|
||
|
|
```php
|
||
|
|
// 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.
|
||
|
|
|
||
|
|
```php
|
||
|
|
// 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 - creditAmount
|
||
|
|
```
|
||
|
|
|
||
|
|
### DunningService
|
||
|
|
|
||
|
|
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 cancelled
|
||
|
|
```
|
||
|
|
|
||
|
|
Configuration in `config.php`:
|
||
|
|
```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.
|
||
|
|
|
||
|
|
```php
|
||
|
|
$taxResult = $taxService->calculate($workspace, $amount);
|
||
|
|
// Returns: TaxResult with taxAmount, taxRate, jurisdiction, isExempt
|
||
|
|
```
|
||
|
|
|
||
|
|
## Payment Gateways
|
||
|
|
|
||
|
|
### PaymentGatewayContract
|
||
|
|
|
||
|
|
All gateways implement this interface ensuring consistent behavior:
|
||
|
|
|
||
|
|
```php
|
||
|
|
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 action
|
||
|
|
- `InvoiceReceivedPayment` → Order status: processing
|
||
|
|
- `InvoiceProcessing` → Waiting for confirmations
|
||
|
|
- `InvoiceSettled` → Fulfill order
|
||
|
|
- `InvoiceExpired` → 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 order
|
||
|
|
- `invoice.paid` → Renew subscription
|
||
|
|
- `invoice.payment_failed` → Trigger dunning
|
||
|
|
- `customer.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
|
||
|
|
|
||
|
|
```php
|
||
|
|
// Dispatched automatically on model changes
|
||
|
|
SubscriptionCreated::class → RewardAgentReferralOnSubscription
|
||
|
|
SubscriptionRenewed::class → ResetUsageOnRenewal
|
||
|
|
OrderPaid::class → CreateReferralCommission
|
||
|
|
```
|
||
|
|
|
||
|
|
### Listeners
|
||
|
|
|
||
|
|
- `ProvisionSocialHostSubscription`: Product-specific provisioning logic
|
||
|
|
- `RewardAgentReferralOnSubscription`: Attribute referral for new subscriptions
|
||
|
|
- `ResetUsageOnRenewal`: Clear usage counters on billing period reset
|
||
|
|
- `CreateReferralCommission`: 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`:
|
||
|
|
|
||
|
|
```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:
|
||
|
|
|
||
|
|
```bash
|
||
|
|
# 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 flow
|
||
|
|
- `SubscriptionServiceTest`: Subscription lifecycle, proration
|
||
|
|
- `DunningServiceTest`: Payment recovery flows
|
||
|
|
- `WebhookTest`: Gateway webhook handling
|
||
|
|
- `TaxServiceTest`: Tax calculation, VAT validation
|
||
|
|
- `CouponServiceTest`: Discount application
|
||
|
|
- `RefundServiceTest`: Refund processing
|