php-commerce/tests/Feature/CheckoutFlowTest.php
2026-01-27 00:24:22 +00:00

340 lines
11 KiB
PHP

<?php
use Illuminate\Support\Facades\Cache;
use Core\Commerce\Models\Invoice;
use Core\Commerce\Models\Order;
use Core\Commerce\Models\Payment;
use Core\Commerce\Services\CommerceService;
use Core\Commerce\Services\PaymentGateway\PaymentGatewayContract;
use Core\Mod\Tenant\Models\Package;
use Core\Mod\Tenant\Models\User;
use Core\Mod\Tenant\Models\Workspace;
use Core\Mod\Tenant\Models\WorkspacePackage;
uses(\Illuminate\Foundation\Testing\RefreshDatabase::class);
beforeEach(function () {
Cache::flush();
$this->user = User::factory()->create([
'email' => 'test@example.com',
]);
$this->workspace = Workspace::factory()->create([
'billing_email' => 'billing@example.com',
'billing_name' => 'Test Company',
'billing_country' => 'GB',
]);
$this->workspace->users()->attach($this->user->id, [
'role' => 'owner',
'is_default' => true,
]);
// Use existing seeded packages or create test package
$this->package = Package::where('code', 'creator')->first();
if (! $this->package) {
$this->package = Package::create([
'name' => 'Creator',
'code' => 'creator',
'description' => 'For creators',
'monthly_price' => 19.00,
'yearly_price' => 190.00,
'is_active' => true,
]);
}
$this->service = app(CommerceService::class);
});
describe('Order Creation', function () {
it('creates an order for a package purchase', function () {
$order = $this->service->createOrder(
$this->workspace,
$this->package,
'monthly'
);
expect($order)->toBeInstanceOf(Order::class)
->and($order->orderable_type)->toBe(Workspace::class)
->and($order->orderable_id)->toBe($this->workspace->id)
->and($order->status)->toBe('pending')
->and($order->billing_cycle)->toBe('monthly')
->and($order->billing_email)->toBe('billing@example.com')
->and($order->billing_name)->toBe('Test Company')
->and($order->order_number)->toStartWith('ORD-');
});
it('creates order items for package', function () {
$order = $this->service->createOrder(
$this->workspace,
$this->package,
'monthly'
);
expect($order->items)->toHaveCount(1);
$item = $order->items->first();
expect($item->item_type)->toBe('package')
->and($item->item_id)->toBe($this->package->id)
->and($item->item_code)->toBe($this->package->code)
->and($item->billing_cycle)->toBe('monthly');
});
it('calculates tax correctly for UK customer', function () {
$order = $this->service->createOrder(
$this->workspace,
$this->package,
'monthly'
);
// UK VAT is 20%
expect($order->tax_country)->toBe('GB')
->and($order->tax_rate)->toBe(20.00)
->and((float) $order->tax_amount)->toBe(round(19.00 * 0.20, 2));
});
it('calculates total correctly', function () {
$order = $this->service->createOrder(
$this->workspace,
$this->package,
'monthly'
);
$expectedTotal = 19.00 + (19.00 * 0.20); // subtotal + tax
expect((float) $order->total)->toBe(round($expectedTotal, 2));
});
it('creates order with yearly billing cycle', function () {
$order = $this->service->createOrder(
$this->workspace,
$this->package,
'yearly'
);
expect($order->billing_cycle)->toBe('yearly')
->and((float) $order->subtotal)->toBe(190.00);
});
});
describe('Checkout Session Creation', function () {
it('creates checkout session with mocked gateway', function () {
// Create a mock gateway
$mockGateway = Mockery::mock(PaymentGatewayContract::class);
$mockGateway->shouldReceive('createCustomer')
->andReturn('cust_mock_123');
$mockGateway->shouldReceive('createCheckoutSession')
->andReturn([
'session_id' => 'cs_mock_session_123',
'checkout_url' => 'https://checkout.example.com/session/123',
]);
app()->instance('commerce.gateway.btcpay', $mockGateway);
$order = $this->service->createOrder(
$this->workspace,
$this->package,
'monthly'
);
$result = $this->service->createCheckout(
$order,
'btcpay',
'https://example.com/success',
'https://example.com/cancel'
);
expect($result)->toHaveKeys(['order', 'session_id', 'checkout_url'])
->and($result['session_id'])->toBe('cs_mock_session_123')
->and($result['checkout_url'])->toContain('checkout.example.com')
->and($result['order']->status)->toBe('processing');
});
});
describe('Order Fulfilment', function () {
it('fulfils order and provisions entitlements', function () {
$order = $this->service->createOrder(
$this->workspace,
$this->package,
'monthly'
);
// Create a payment
$payment = Payment::create([
'workspace_id' => $this->workspace->id,
'order_id' => $order->id,
'gateway' => 'btcpay',
'gateway_payment_id' => 'pay_mock_123',
'amount' => $order->total,
'currency' => 'GBP',
'status' => 'succeeded',
'paid_at' => now(),
]);
// Fulfil the order
$this->service->fulfillOrder($order, $payment);
$order->refresh();
expect($order->status)->toBe('paid')
->and($order->paid_at)->not->toBeNull();
// Check that workspace package was provisioned
$workspacePackage = WorkspacePackage::where('workspace_id', $this->workspace->id)
->where('package_id', $this->package->id)
->first();
expect($workspacePackage)->not->toBeNull()
->and($workspacePackage->status)->toBe('active');
});
it('creates invoice on fulfilment', function () {
$order = $this->service->createOrder(
$this->workspace,
$this->package,
'monthly'
);
$payment = Payment::create([
'workspace_id' => $this->workspace->id,
'order_id' => $order->id,
'gateway' => 'btcpay',
'gateway_payment_id' => 'pay_mock_456',
'amount' => $order->total,
'currency' => 'GBP',
'status' => 'succeeded',
'paid_at' => now(),
]);
$this->service->fulfillOrder($order, $payment);
$invoice = Invoice::where('order_id', $order->id)->first();
expect($invoice)->not->toBeNull()
->and($invoice->invoice_number)->toStartWith('INV-')
->and((float) $invoice->total)->toBe((float) $order->total)
->and($invoice->status)->toBe('paid');
});
it('fails order with reason', function () {
$order = $this->service->createOrder(
$this->workspace,
$this->package,
'monthly'
);
$this->service->failOrder($order, 'Payment declined');
$order->refresh();
expect($order->status)->toBe('failed')
->and($order->metadata['failure_reason'])->toBe('Payment declined');
});
});
describe('End-to-End Checkout Flow', function () {
it('completes full checkout flow: cart to paid order', function () {
// Step 1: Create order (simulates adding to cart and proceeding)
$order = $this->service->createOrder(
$this->workspace,
$this->package,
'monthly'
);
expect($order->status)->toBe('pending');
// Step 2: Simulate checkout session creation (mocked gateway)
$mockGateway = Mockery::mock(PaymentGatewayContract::class);
$mockGateway->shouldReceive('createCustomer')
->andReturn('cust_e2e_123');
$mockGateway->shouldReceive('createCheckoutSession')
->andReturn([
'session_id' => 'cs_e2e_session',
'checkout_url' => 'https://pay.example.com/checkout',
]);
app()->instance('commerce.gateway.btcpay', $mockGateway);
$checkout = $this->service->createCheckout($order, 'btcpay');
$order->refresh();
expect($order->status)->toBe('processing')
->and($order->gateway_session_id)->toBe('cs_e2e_session');
// Step 3: Simulate payment completion (webhook would call this)
$payment = Payment::create([
'workspace_id' => $this->workspace->id,
'order_id' => $order->id,
'gateway' => 'btcpay',
'gateway_payment_id' => 'pay_e2e_completed',
'amount' => $order->total,
'currency' => 'GBP',
'status' => 'succeeded',
'paid_at' => now(),
]);
$this->service->fulfillOrder($order, $payment);
// Step 4: Verify final state
$order->refresh();
$this->workspace->refresh();
expect($order->status)->toBe('paid')
->and($order->paid_at)->not->toBeNull();
// Verify invoice created
$invoice = Invoice::where('order_id', $order->id)->first();
expect($invoice)->not->toBeNull()
->and($invoice->status)->toBe('paid');
// Verify entitlements provisioned
$workspacePackage = $this->workspace->workspacePackages()
->where('package_id', $this->package->id)
->where('status', 'active')
->first();
expect($workspacePackage)->not->toBeNull();
});
it('handles checkout cancellation', function () {
$order = $this->service->createOrder(
$this->workspace,
$this->package,
'monthly'
);
// Simulate user cancelling checkout
$order->cancel();
expect($order->status)->toBe('cancelled');
// No entitlements should be provisioned
$workspacePackage = $this->workspace->workspacePackages()
->where('package_id', $this->package->id)
->first();
expect($workspacePackage)->toBeNull();
});
it('handles checkout expiry', function () {
$order = $this->service->createOrder(
$this->workspace,
$this->package,
'monthly'
);
// Simulate payment expiry (BTCPay invoice expired)
$order->markAsFailed('Payment expired');
expect($order->status)->toBe('failed')
->and($order->metadata['failure_reason'])->toBe('Payment expired');
// No entitlements should be provisioned
$workspacePackage = $this->workspace->workspacePackages()
->where('package_id', $this->package->id)
->first();
expect($workspacePackage)->toBeNull();
});
});
afterEach(function () {
Mockery::close();
});