Paddle Billing
Testing And Operations
Commands, sandbox setup, and operational runbooks for the Vortos Paddle billing package.
Testing And Operations
Sandbox Setup
Set PADDLE_MODE=sandbox in your environment. Sandbox IPs are automatically allowed for webhook delivery — no additional config required.
PADDLE_MODE=sandbox
PADDLE_API_KEY=your_sandbox_api_key
PADDLE_NOTIFICATION_SECRET=your_sandbox_webhook_secretCommands
Outbox relay
# Long-running worker (recommended — handles SIGTERM gracefully)
php bin/console vortos:paddle:outbox:relay
# Single batch (for cron)
php bin/console vortos:paddle:outbox:relay --once
# Control idle sleep
php bin/console vortos:paddle:outbox:relay --sleep=5Dead-letter retry
# Inspect failed entries without resetting
php bin/console vortos:paddle:outbox:retry --dry-run
php bin/console vortos:paddle:outbox:retry --dry-run --operation=customer.create
php bin/console vortos:paddle:outbox:retry --dry-run --id=42
# Reset failed entries for re-delivery
php bin/console vortos:paddle:outbox:retry --force
php bin/console vortos:paddle:outbox:retry --operation=subscription.cancel --force
php bin/console vortos:paddle:outbox:retry --id=42 --force
php bin/console vortos:paddle:outbox:retry --limit=20 --forceWebhook idempotency pruning
# Remove expired webhook idempotency records (run daily)
php bin/console vortos:paddle:webhook:idempotency:pruneDeployment Runbook
- Deploy code.
- Run
php bin/console vortos:migrate:run. - Verify
PADDLE_MODE,PADDLE_API_KEY, andPADDLE_NOTIFICATION_SECRETare set. - Configure the webhook URL in the Paddle dashboard to point to
{app_url}/webhooks/paddle. - Start or reload the outbox relay worker.
- Watch
paddle_outboxfor failed entries after the first relay cycle. - Schedule
vortos:paddle:webhook:idempotency:pruneto run daily.
Monitoring
Watch paddle_outbox for entries that fail permanently:
SELECT operation, last_error, failed_at, created_at
FROM vortos_paddle_outbox
WHERE status = 'failed'
ORDER BY failed_at DESC
LIMIT 50;Watch pending backlog age:
SELECT COUNT(*), MIN(created_at)
FROM vortos_paddle_outbox
WHERE status = 'pending';Alert on failed entries
A non-zero count of status=failed entries means Paddle API calls were permanently dropped. Inspect last_error and use the retry command after resolving the underlying cause.
Testing Paddle Code
Inject the Immediate interfaces in unit tests and mock them directly:
use Vortos\Paddle\Customer\Contract\ImmediateCustomerServiceInterface;
use Vortos\Paddle\Customer\Customer;
$customers = $this->createMock(ImmediateCustomerServiceInterface::class);
$customers->method('get')->willReturn(Customer::fromSdk(...));For command handler tests that go through the unit of work, use the Transactional interfaces and assert against the outbox writer:
use Vortos\Paddle\Outbox\PaddleOutboxWriterInterface;
$outbox = $this->createMock(PaddleOutboxWriterInterface::class);
$outbox->expects($this->once())
->method('queue')
->with('customer.create', $this->arrayHasKey('email'));