Add comprehensive webhook validation and processing system with Polar.sh integration: - Create built-in Standard Webhooks package following official specification - Implement HMAC-SHA256 signature validation with base64 encoding - Add webhook factory for multi-provider support (Polar, Stripe, generic) - Replace custom Polar webhook validation with Standard Webhooks implementation - Add proper exception handling with custom WebhookVerificationException - Support sandbox mode bypass for development environments - Update Polar provider to use database-driven configuration - Enhance webhook test suite with proper Standard Webhooks format - Add PaymentProvider model HasFactory trait for testing - Implement timestamp tolerance checking (±5 minutes) for replay protection - Support multiple signature versions and proper header validation This provides a secure, reusable webhook validation system that can be extended to other payment providers while maintaining full compliance with Standard Webhooks specification. BREAKING CHANGE: Polar webhook validation now uses Standard Webhooks format with headers 'webhook-id', 'webhook-timestamp', 'webhook-signature' instead of previous Polar-specific headers.
105 lines
2.8 KiB
PHP
105 lines
2.8 KiB
PHP
<?php
|
|
|
|
namespace Database\Factories;
|
|
|
|
use App\Models\Plan;
|
|
use App\Models\Subscription;
|
|
use App\Models\User;
|
|
use Illuminate\Database\Eloquent\Factories\Factory;
|
|
|
|
/**
|
|
* @extends Factory<Subscription>
|
|
*/
|
|
class SubscriptionFactory extends Factory
|
|
{
|
|
/**
|
|
* Define the model's default state.
|
|
*
|
|
* @return array<string, mixed>
|
|
*/
|
|
public function definition(): array
|
|
{
|
|
return [
|
|
'user_id' => User::factory(),
|
|
'plan_id' => Plan::factory(),
|
|
'type' => 'default',
|
|
'stripe_id' => fake()->uuid(),
|
|
'stripe_status' => 'active',
|
|
'stripe_price' => fake()->randomNumber(4),
|
|
'quantity' => 1,
|
|
'trial_ends_at' => null,
|
|
'ends_at' => fake()->dateTimeBetween('+1 month', '+1 year'),
|
|
'provider' => 'polar',
|
|
'provider_subscription_id' => fake()->uuid(),
|
|
'provider_checkout_id' => fake()->uuid(),
|
|
'unified_status' => 'active',
|
|
'cancelled_at' => null,
|
|
'cancellation_reason' => null,
|
|
'paused_at' => null,
|
|
'resumed_at' => null,
|
|
'migration_batch_id' => null,
|
|
'is_migrated' => false,
|
|
'legacy_data' => null,
|
|
'synced_at' => now(),
|
|
'provider_data' => '[]',
|
|
'last_provider_sync' => now(),
|
|
'starts_at' => now(),
|
|
'status' => 'active',
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Indicate that the subscription is cancelled.
|
|
*/
|
|
public function cancelled(): static
|
|
{
|
|
return $this->state(fn (array $attributes) => [
|
|
'status' => 'cancelled',
|
|
'cancelled_at' => now(),
|
|
'cancellation_reason' => 'customer_request',
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Indicate that the subscription is paused.
|
|
*/
|
|
public function paused(): static
|
|
{
|
|
return $this->state(fn (array $attributes) => [
|
|
'status' => 'paused',
|
|
'paused_at' => now(),
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Indicate that the subscription is on trial.
|
|
*/
|
|
public function trialing(): static
|
|
{
|
|
return $this->state(fn (array $attributes) => [
|
|
'status' => 'trialing',
|
|
'trial_ends_at' => now()->addDays(14),
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Indicate that the subscription uses a specific provider.
|
|
*/
|
|
public function provider(string $provider): static
|
|
{
|
|
return $this->state(fn (array $attributes) => [
|
|
'provider' => $provider,
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Indicate that the subscription has no provider subscription ID (for testing fallback logic).
|
|
*/
|
|
public function withoutProviderId(): static
|
|
{
|
|
return $this->state(fn (array $attributes) => [
|
|
'provider_subscription_id' => null,
|
|
]);
|
|
}
|
|
}
|