diff --git a/app/Models/PaymentProvider.php b/app/Models/PaymentProvider.php index ae8d5c3..60ffcde 100644 --- a/app/Models/PaymentProvider.php +++ b/app/Models/PaymentProvider.php @@ -24,7 +24,7 @@ class PaymentProvider extends Model protected $casts = [ 'is_active' => 'boolean', - 'configuration' => 'array', + 'configuration' => 'encrypted:array', 'supports_recurring' => 'boolean', 'supports_one_time' => 'boolean', 'supported_currencies' => 'array', diff --git a/database/migrations/2025_12_03_102201_change_configuration_column_to_text_in_payment_providers_table.php b/database/migrations/2025_12_03_102201_change_configuration_column_to_text_in_payment_providers_table.php new file mode 100644 index 0000000..ba3c82f --- /dev/null +++ b/database/migrations/2025_12_03_102201_change_configuration_column_to_text_in_payment_providers_table.php @@ -0,0 +1,70 @@ +getDriverName(); + + // --- MySQL / MariaDB only --- + // Drop JSON check constraints if they exist (safely) + if (in_array($driver, ['mysql', 'mariadb'])) { + try { + // MySQL automatically names check constraints in different ways, + // so we attempt a generic drop and ignore failures. + DB::statement("ALTER TABLE `payment_providers` DROP CHECK `payment_providers.configuration`;"); + } catch (\Throwable $e) { + // ignore if constraint does not exist + } + } + + // --- PostgreSQL --- + // PostgreSQL may also require dropping check constraints but constraint names are random. + // We try to drop any constraint *that checks JSON*. + if ($driver === 'pgsql') { + try { + $constraint = DB::selectOne(" + SELECT conname + FROM pg_constraint + JOIN pg_class ON pg_constraint.conrelid = pg_class.oid + WHERE relname = 'payment_providers' + AND pg_get_constraintdef(pg_constraint.oid) LIKE '%configuration%json%'; + "); + + if (!empty($constraint->conname)) { + DB::statement("ALTER TABLE payment_providers DROP CONSTRAINT {$constraint->conname};"); + } + } catch (\Throwable $e) { + // ignore if constraint does not exist + } + } + + // --- SQLite & SQL Server --- + // No action needed: + // SQLite doesn’t enforce JSON CHECK constraints + // SQL Server does not create JSON CHECK constraints automatically. + + Schema::table('payment_providers', function (Blueprint $table) { + // Replace JSON type with LONGTEXT / TEXT depending on driver + // Laravel automatically maps this properly for each database. + $table->longText('configuration')->change(); + }); + } + + public function down(): void + { + Schema::table('payment_providers', function (Blueprint $table) { + // revert to JSON column + // Laravel maps this to appropriate driver types + $table->json('configuration')->change(); + }); + + // Optionally: you can restore JSON CHECK constraints here, but it's not required + } +}; diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php index cd30bda..a95f827 100644 --- a/database/seeders/DatabaseSeeder.php +++ b/database/seeders/DatabaseSeeder.php @@ -66,6 +66,7 @@ class DatabaseSeeder extends Seeder 'LanguageSeeder' => 'Seed languages data', 'CurrencySeeder' => 'Seed currencies data', 'PaymentSeeder' => 'Seed payment methods and data', + 'PaymentProviderSeeder' => 'Seed payment providers (Stripe, Lemon Squeezy, Polar, etc.)', 'EmailSeeder' => 'Seed email templates', 'NotificationSeeder' => 'Seed notification templates', 'SettingsSeeder' => 'Seed application settings', diff --git a/database/seeders/PaymentProviderSeeder.php b/database/seeders/PaymentProviderSeeder.php new file mode 100644 index 0000000..19e1d21 --- /dev/null +++ b/database/seeders/PaymentProviderSeeder.php @@ -0,0 +1,229 @@ +command->info('🌱 Seeding payment providers...'); + + // Skip encryption test for now to focus on basic seeding + $this->command->info('πŸ“ Skipping encryption test to focus on basic data seeding'); + + $providers = [ + [ + 'name' => 'stripe', + 'display_name' => 'Stripe', + 'description' => 'Accept payments via Stripe - Credit cards, Apple Pay, Google Pay and more', + 'is_active' => false, + 'configuration' => [ + 'class' => 'App\\Services\\Payments\\Providers\\StripeProvider', + 'secret_key' => env('STRIPE_SECRET') ?: 'sk_test_placeholder', + 'publishable_key' => env('STRIPE_PUBLISHABLE_KEY') ?: 'pk_test_placeholder', + 'webhook_secret' => env('STRIPE_WEBHOOK_SECRET') ?: 'whsec_placeholder', + 'webhook_url' => env('APP_URL', 'https://example.com') . '/webhook/stripe', + 'success_url' => env('APP_URL', 'https://example.com') . '/payment/success', + 'cancel_url' => env('APP_URL', 'https://example.com') . '/payment/cancel', + 'currency' => env('CASHIER_CURRENCY', 'USD'), + ], + 'supports_recurring' => true, + 'supports_one_time' => true, + 'supported_currencies' => [ + 'USD' => 'US Dollar' + ], + 'fee_structure' => [ + 'fixed_fee' => '0.50', + 'percentage_fee' => '3.9', + ], + 'priority' => 60, + 'is_fallback' => false, + ], + + [ + 'name' => 'lemon_squeezy', + 'display_name' => 'Lemon Squeezy', + 'description' => 'Modern payment platform for digital products and subscriptions', + 'is_active' => false, + 'configuration' => [ + 'class' => 'App\\Services\\Payments\\Providers\\LemonSqueezyProvider', + 'api_key' => env('LEMON_SQUEEZY_API_KEY', 'lsk_...'), + 'store_id' => env('LEMON_SQUEEZY_STORE_ID', '...'), + 'webhook_secret' => env('LEMON_SQUEEZY_WEBHOOK_SECRET', 'whsec_...'), + 'webhook_url' => env('APP_URL') . '/webhook/lemon-squeezy', + 'success_url' => env('APP_URL') . '/payment/success', + 'cancel_url' => env('APP_URL') . '/payment/cancel', + ], + 'supports_recurring' => true, + 'supports_one_time' => true, + 'supported_currencies' => [ + 'USD' => 'US Dollar' + ], + 'fee_structure' => [ + 'fixed_fee' => '0.50', + 'percentage_fee' => '5.0', + ], + 'priority' => 50, + 'is_fallback' => false, + ], + + [ + 'name' => 'polar', + 'display_name' => 'Polar.sh', + 'description' => 'Modern crowdfunding and payment platform for creators', + 'is_active' => false, + 'configuration' => [ + 'class' => 'App\\Services\\Payments\\Providers\\PolarProvider', + 'api_key' => env('POLAR_API_KEY', 'pol_...'), + 'webhook_secret' => env('POLAR_WEBHOOK_SECRET', 'whsec_...'), + 'sandbox' => env('POLAR_SANDBOX', false), + 'sandbox_api_key' => env('POLAR_SANDBOX_API_KEY', 'pol_test_...'), + 'sandbox_webhook_secret' => env('POLAR_SANDBOX_WEBHOOK_SECRET', 'whsec_test_...'), + 'access_token' => env('POLAR_ACCESS_TOKEN', 'polar_...'), + 'webhook_url' => env('APP_URL') . '/webhook/polar', + 'success_url' => env('APP_URL') . '/payment/success', + 'cancel_url' => env('APP_URL') . '/payment/cancel', + ], + 'supports_recurring' => true, + 'supports_one_time' => true, + 'supported_currencies' => [ + 'USD' => 'US Dollar', + 'EUR' => 'Euro', + ], + 'fee_structure' => [ + 'fixed_fee' => '0.50', + 'percentage_fee' => '5.0', + ], + 'priority' => 40, + 'is_fallback' => false, + ], + + [ + 'name' => 'oxapay', + 'display_name' => 'OxaPay', + 'description' => 'Cryptocurrency payment gateway supporting multiple digital assets', + 'is_active' => false, + 'configuration' => [ + 'class' => 'App\\Services\\Payments\\Providers\\OxapayProvider', + 'merchant_api_key' => env('OXAPAY_MERCHANT_API_KEY', 'merchant_...'), + 'payout_api_key' => env('OXAPAY_PAYOUT_API_KEY', 'payout_...'), + 'webhook_url' => env('OXAPAY_WEBHOOK_URL', env('APP_URL') . '/webhook/oxapay'), + 'success_url' => env('APP_URL') . '/payment/success', + 'cancel_url' => env('APP_URL') . '/payment/cancel', + 'sandbox' => env('OXAPAY_SANDBOX', true), + ], + 'supports_recurring' => false, + 'supports_one_time' => true, + 'supported_currencies' => [ + 'BTC' => 'Bitcoin', + 'ETH' => 'Ethereum', + 'USDT' => 'Tether USD', + 'USDC' => 'USD Coin', + 'LTC' => 'Litecoin', + 'BCH' => 'Bitcoin Cash', + 'BNB' => 'Binance Coin', + ], + 'fee_structure' => [ + 'fixed_fee' => '0.00', + 'percentage_fee' => '1.0', + ], + 'priority' => 30, + 'is_fallback' => false, + ], + + [ + 'name' => 'crypto', + 'display_name' => 'Native Crypto', + 'description' => 'Direct cryptocurrency payments with blockchain confirmations', + 'is_active' => false, + 'configuration' => [ + 'class' => 'App\\Services\\Payments\\Providers\\CryptoProvider', + 'webhook_secret' => env('CRYPTO_WEBHOOK_SECRET', 'crypto_whsec_...'), + 'confirmation_timeout_minutes' => env('CRYPTO_CONFIRMATION_TIMEOUT', 30), + 'exchange_rate_provider' => env('CRYPTO_EXCHANGE_RATE_PROVIDER', 'coingecko'), + 'coingecko_api_key' => env('COINGECKO_API_KEY'), + 'blockchair_api_key' => env('BLOCKCHAIR_API_KEY'), + 'webhook_url' => env('APP_URL') . '/webhook/crypto', + 'success_url' => env('APP_URL') . '/payment/success', + 'cancel_url' => env('APP_URL') . '/payment/cancel', + 'supported_wallets' => [ + 'btc' => ['bitcoin', 'lightning'], + 'eth' => ['ethereum', 'erc20'], + 'ltc' => ['litecoin'], + ], + ], + 'supports_recurring' => false, + 'supports_one_time' => true, + 'supported_currencies' => [ + 'BTC' => 'Bitcoin', + 'ETH' => 'Ethereum', + 'LTC' => 'Litecoin', + 'USDT' => 'Tether USD', + 'USDC' => 'USD Coin', + ], + 'fee_structure' => [ + 'fixed_fee' => '0.00', + 'percentage_fee' => '0.0', + 'note' => 'Only blockchain network fees apply', + ], + 'priority' => 20, + 'is_fallback' => false, + ], + + [ + 'name' => 'activation_key', + 'display_name' => 'Activation Key', + 'description' => 'Manual activation using pre-generated activation keys', + 'is_active' => true, + 'configuration' => [ + 'class' => 'App\\Services\\Payments\\Providers\\ActivationKeyProvider', + 'key_prefix' => env('ACTIVATION_KEY_PREFIX', 'AK-'), + 'key_length' => env('ACTIVATION_KEY_LENGTH', 32), + 'expiration_days' => env('ACTIVATION_KEY_EXPIRATION_DAYS'), + 'require_email_verification' => env('ACTIVATION_KEY_REQUIRE_EMAIL', true), + 'max_keys_per_user' => env('ACTIVATION_KEY_MAX_PER_USER', 5), + 'allowed_characters' => 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', + ], + 'supports_recurring' => false, + 'supports_one_time' => true, + 'supported_currencies' => [], // No currency needed for activation keys + 'fee_structure' => [ + 'fixed_fee' => '0.00', + 'percentage_fee' => '0.0', + 'note' => 'No fees for activation key system', + ], + 'priority' => 10, // Lowest priority + 'is_fallback' => true, + ], + ]; + + foreach ($providers as $providerData) { + try { + // First try to find existing provider + $existingProvider = PaymentProvider::where('name', $providerData['name'])->first(); + + if ($existingProvider) { + // Update existing provider + $existingProvider->update($providerData); + $this->command->info("βœ… Updated payment provider: {$providerData['display_name']} ({$providerData['name']})"); + } else { + // Create new provider + $provider = PaymentProvider::create($providerData); + $this->command->info("βœ… Created payment provider: {$providerData['display_name']} ({$providerData['name']})"); + } + } catch (\Exception $e) { + $this->command->error("❌ Error seeding provider {$providerData['name']}: {$e->getMessage()}"); + continue; + } + } + + $this->command->info('πŸŽ‰ Payment providers seeding completed!'); + $this->command->info('πŸ’‘ Configuration data is managed by Laravel\'s encrypted:array cast.'); + } +}