diff --git a/.github/workflows/linting.yaml b/.github/workflows/linting.yaml index 99019438..1cd305a9 100644 --- a/.github/workflows/linting.yaml +++ b/.github/workflows/linting.yaml @@ -37,6 +37,9 @@ jobs: key: ${{ runner.os }}-${{ matrix.php }}-composer-${{ hashFiles('**/composer.lock') }} restore-keys: ${{ runner.os }}-${{ matrix.php }}-composer- + - name: Configure Flux Pro auth + run: composer config http-basic.composer.fluxui.dev "${{ secrets.FLUX_EMAIL }}" "${{ secrets.FLUX_LICENSE_KEY }}" + - name: Install Dependencies run: composer install -q --no-ansi --no-interaction --no-scripts --no-progress --prefer-dist diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index c1928c15..eedef6a6 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -47,6 +47,9 @@ jobs: key: ${{ runner.os }}-${{ matrix.php }}-composer-${{ hashFiles('**/composer.lock') }} restore-keys: ${{ runner.os }}-${{ matrix.php }}-composer- + - name: Configure Flux Pro auth + run: composer config http-basic.composer.fluxui.dev "${{ secrets.FLUX_EMAIL }}" "${{ secrets.FLUX_LICENSE_KEY }}" + - name: Install Dependencies run: composer install -q --no-ansi --no-interaction --no-scripts --no-progress --prefer-dist diff --git a/CLAUDE.md b/CLAUDE.md index 126421d1..45e08cd3 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -8,8 +8,8 @@ The Laravel Boost guidelines are specifically curated by Laravel maintainers for ## Foundational Context This application is a Laravel application and its main Laravel ecosystems package & versions are below. You are an expert with them all. Ensure you abide by these specific packages & versions. -- php - 8.4.18 -- filament/filament (FILAMENT) - v3 +- php - 8.4.19 +- filament/filament (FILAMENT) - v5 - laravel/cashier (CASHIER) - v15 - laravel/framework (LARAVEL) - v12 - laravel/horizon (HORIZON) - v5 @@ -18,7 +18,7 @@ This application is a Laravel application and its main Laravel ecosystems packag - laravel/prompts (PROMPTS) - v0 - laravel/sanctum (SANCTUM) - v4 - laravel/socialite (SOCIALITE) - v5 -- livewire/livewire (LIVEWIRE) - v3 +- livewire/livewire (LIVEWIRE) - v4 - laravel/mcp (MCP) - v0 - laravel/pint (PINT) - v1 - laravel/sail (SAIL) - v1 diff --git a/app/Http/Controllers/DeveloperOnboardingController.php b/app/Http/Controllers/DeveloperOnboardingController.php index d1f218ac..e0cc8169 100644 --- a/app/Http/Controllers/DeveloperOnboardingController.php +++ b/app/Http/Controllers/DeveloperOnboardingController.php @@ -4,8 +4,10 @@ use App\Models\DeveloperAccount; use App\Services\StripeConnectService; +use App\Support\StripeConnectCountries; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; +use Illuminate\Validation\Rule; use Illuminate\View\View; class DeveloperOnboardingController extends Controller @@ -32,16 +34,33 @@ public function start(Request $request): RedirectResponse { $request->validate([ 'accepted_plugin_terms' => ['required', 'accepted'], + 'country' => ['required', 'string', 'size:2', Rule::in(StripeConnectCountries::supportedCountryCodes())], + 'payout_currency' => ['required', 'string', 'size:3'], ], [ 'accepted_plugin_terms.required' => 'You must accept the Plugin Developer Terms and Conditions.', 'accepted_plugin_terms.accepted' => 'You must accept the Plugin Developer Terms and Conditions.', + 'country.required' => 'Please select your country.', + 'country.in' => 'The selected country is not supported for Stripe Connect.', + 'payout_currency.required' => 'Please select a payout currency.', ]); + $country = strtoupper($request->input('country')); + $payoutCurrency = strtoupper($request->input('payout_currency')); + + if (! StripeConnectCountries::isValidCurrencyForCountry($country, $payoutCurrency)) { + return back()->withErrors(['payout_currency' => 'The selected currency is not available for your country.']); + } + $user = $request->user(); $developerAccount = $user->developerAccount; if (! $developerAccount) { - $developerAccount = $this->stripeConnectService->createConnectAccount($user); + $developerAccount = $this->stripeConnectService->createConnectAccount($user, $country, $payoutCurrency); + } else { + $developerAccount->update([ + 'country' => $country, + 'payout_currency' => $payoutCurrency, + ]); } if (! $developerAccount->hasAcceptedCurrentTerms()) { diff --git a/app/Livewire/Customer/Developer/Onboarding.php b/app/Livewire/Customer/Developer/Onboarding.php index 0c51ca6a..337d59ef 100644 --- a/app/Livewire/Customer/Developer/Onboarding.php +++ b/app/Livewire/Customer/Developer/Onboarding.php @@ -2,6 +2,7 @@ namespace App\Livewire\Customer\Developer; +use App\Support\StripeConnectCountries; use Livewire\Attributes\Computed; use Livewire\Attributes\Layout; use Livewire\Attributes\Title; @@ -11,6 +12,10 @@ #[Title('Developer Onboarding')] class Onboarding extends Component { + public string $country = ''; + + public string $payoutCurrency = ''; + public function mount(): void { $developerAccount = auth()->user()->developerAccount; @@ -18,6 +23,20 @@ public function mount(): void if ($developerAccount && $developerAccount->hasCompletedOnboarding()) { $this->redirect(route('customer.developer.dashboard'), navigate: true); } + + if ($developerAccount) { + $this->country = $developerAccount->country ?? ''; + $this->payoutCurrency = $developerAccount->payout_currency ?? ''; + } + } + + public function updatedCountry(string $value): void + { + if (StripeConnectCountries::isSupported($value)) { + $this->payoutCurrency = StripeConnectCountries::defaultCurrency($value); + } else { + $this->payoutCurrency = ''; + } } #[Computed] @@ -32,6 +51,39 @@ public function hasExistingAccount(): bool return $this->developerAccount !== null; } + /** + * @return array}> + */ + #[Computed] + public function countries(): array + { + $countries = StripeConnectCountries::all(); + + uasort($countries, fn (array $a, array $b) => $a['name'] <=> $b['name']); + + return $countries; + } + + /** + * @return array + */ + #[Computed] + public function availableCurrencies(): array + { + if (! $this->country || ! StripeConnectCountries::isSupported($this->country)) { + return []; + } + + $currencies = StripeConnectCountries::availableCurrencies($this->country); + + $named = []; + foreach ($currencies as $code) { + $named[$code] = StripeConnectCountries::currencyName($code); + } + + return $named; + } + public function render() { return view('livewire.customer.developer.onboarding'); diff --git a/app/Services/StripeConnectService.php b/app/Services/StripeConnectService.php index e521187f..925b2ea0 100644 --- a/app/Services/StripeConnectService.php +++ b/app/Services/StripeConnectService.php @@ -19,15 +19,17 @@ */ class StripeConnectService { - public function createConnectAccount(User $user): DeveloperAccount + public function createConnectAccount(User $user, string $country, string $payoutCurrency): DeveloperAccount { $account = Cashier::stripe()->accounts->create([ 'type' => 'express', + 'country' => $country, 'email' => $user->email, 'metadata' => [ 'user_id' => $user->id, ], 'capabilities' => [ + 'card_payments' => ['requested' => true], 'transfers' => ['requested' => true], ], ]); @@ -38,6 +40,8 @@ public function createConnectAccount(User $user): DeveloperAccount 'stripe_connect_status' => StripeConnectStatus::Pending, 'payouts_enabled' => false, 'charges_enabled' => false, + 'country' => $country, + 'payout_currency' => $payoutCurrency, ]); } @@ -109,7 +113,7 @@ public function processTransfer(PluginPayout $payout): bool try { $transferParams = [ 'amount' => $payout->developer_amount, - 'currency' => 'usd', + 'currency' => strtolower($developerAccount->payout_currency ?? 'usd'), 'destination' => $developerAccount->stripe_connect_account_id, 'metadata' => [ 'payout_id' => $payout->id, diff --git a/app/Support/StripeConnectCountries.php b/app/Support/StripeConnectCountries.php new file mode 100644 index 00000000..824029f1 --- /dev/null +++ b/app/Support/StripeConnectCountries.php @@ -0,0 +1,251 @@ +}> + */ + public const SUPPORTED_COUNTRIES = [ + 'AE' => ['name' => 'United Arab Emirates', 'flag' => "\u{1F1E6}\u{1F1EA}", 'default_currency' => 'AED', 'currencies' => ['AED']], + 'AG' => ['name' => 'Antigua & Barbuda', 'flag' => "\u{1F1E6}\u{1F1EC}", 'default_currency' => 'XCD', 'currencies' => ['XCD']], + 'AL' => ['name' => 'Albania', 'flag' => "\u{1F1E6}\u{1F1F1}", 'default_currency' => 'ALL', 'currencies' => ['ALL']], + 'AM' => ['name' => 'Armenia', 'flag' => "\u{1F1E6}\u{1F1F2}", 'default_currency' => 'AMD', 'currencies' => ['AMD']], + 'AR' => ['name' => 'Argentina', 'flag' => "\u{1F1E6}\u{1F1F7}", 'default_currency' => 'ARS', 'currencies' => ['ARS']], + 'AT' => ['name' => 'Austria', 'flag' => "\u{1F1E6}\u{1F1F9}", 'default_currency' => 'EUR', 'currencies' => ['EUR', 'GBP', 'USD', 'CHF', 'DKK', 'NOK', 'SEK']], + 'AU' => ['name' => 'Australia', 'flag' => "\u{1F1E6}\u{1F1FA}", 'default_currency' => 'AUD', 'currencies' => ['AUD', 'USD']], + 'BA' => ['name' => 'Bosnia & Herzegovina', 'flag' => "\u{1F1E7}\u{1F1E6}", 'default_currency' => 'BAM', 'currencies' => ['BAM']], + 'BE' => ['name' => 'Belgium', 'flag' => "\u{1F1E7}\u{1F1EA}", 'default_currency' => 'EUR', 'currencies' => ['EUR', 'GBP', 'USD', 'CHF', 'DKK', 'NOK', 'SEK']], + 'BG' => ['name' => 'Bulgaria', 'flag' => "\u{1F1E7}\u{1F1EC}", 'default_currency' => 'EUR', 'currencies' => ['EUR', 'GBP', 'USD', 'CHF', 'DKK', 'NOK', 'SEK']], + 'BH' => ['name' => 'Bahrain', 'flag' => "\u{1F1E7}\u{1F1ED}", 'default_currency' => 'BHD', 'currencies' => ['BHD']], + 'BJ' => ['name' => 'Benin', 'flag' => "\u{1F1E7}\u{1F1EF}", 'default_currency' => 'XOF', 'currencies' => ['XOF']], + 'BN' => ['name' => 'Brunei', 'flag' => "\u{1F1E7}\u{1F1F3}", 'default_currency' => 'BND', 'currencies' => ['BND']], + 'BO' => ['name' => 'Bolivia', 'flag' => "\u{1F1E7}\u{1F1F4}", 'default_currency' => 'BOB', 'currencies' => ['BOB']], + 'BS' => ['name' => 'Bahamas', 'flag' => "\u{1F1E7}\u{1F1F8}", 'default_currency' => 'BSD', 'currencies' => ['BSD']], + 'BW' => ['name' => 'Botswana', 'flag' => "\u{1F1E7}\u{1F1FC}", 'default_currency' => 'BWP', 'currencies' => ['BWP']], + 'CA' => ['name' => 'Canada', 'flag' => "\u{1F1E8}\u{1F1E6}", 'default_currency' => 'CAD', 'currencies' => ['CAD', 'USD']], + 'CH' => ['name' => 'Switzerland', 'flag' => "\u{1F1E8}\u{1F1ED}", 'default_currency' => 'CHF', 'currencies' => ['CHF', 'EUR', 'GBP', 'USD', 'DKK', 'NOK', 'SEK']], + 'CI' => ['name' => "C\u{00F4}te d'Ivoire", 'flag' => "\u{1F1E8}\u{1F1EE}", 'default_currency' => 'XOF', 'currencies' => ['XOF']], + 'CL' => ['name' => 'Chile', 'flag' => "\u{1F1E8}\u{1F1F1}", 'default_currency' => 'CLP', 'currencies' => ['CLP']], + 'CO' => ['name' => 'Colombia', 'flag' => "\u{1F1E8}\u{1F1F4}", 'default_currency' => 'COP', 'currencies' => ['COP']], + 'CR' => ['name' => 'Costa Rica', 'flag' => "\u{1F1E8}\u{1F1F7}", 'default_currency' => 'CRC', 'currencies' => ['CRC']], + 'CY' => ['name' => 'Cyprus', 'flag' => "\u{1F1E8}\u{1F1FE}", 'default_currency' => 'EUR', 'currencies' => ['EUR', 'GBP', 'USD', 'CHF', 'DKK', 'NOK', 'SEK']], + 'CZ' => ['name' => 'Czech Republic', 'flag' => "\u{1F1E8}\u{1F1FF}", 'default_currency' => 'CZK', 'currencies' => ['CZK', 'EUR', 'GBP', 'USD', 'CHF', 'DKK', 'NOK', 'SEK']], + 'DE' => ['name' => 'Germany', 'flag' => "\u{1F1E9}\u{1F1EA}", 'default_currency' => 'EUR', 'currencies' => ['EUR', 'GBP', 'USD', 'CHF', 'DKK', 'NOK', 'SEK']], + 'DK' => ['name' => 'Denmark', 'flag' => "\u{1F1E9}\u{1F1F0}", 'default_currency' => 'DKK', 'currencies' => ['DKK', 'EUR', 'GBP', 'USD', 'CHF', 'NOK', 'SEK']], + 'DO' => ['name' => 'Dominican Republic', 'flag' => "\u{1F1E9}\u{1F1F4}", 'default_currency' => 'DOP', 'currencies' => ['DOP']], + 'EC' => ['name' => 'Ecuador', 'flag' => "\u{1F1EA}\u{1F1E8}", 'default_currency' => 'USD', 'currencies' => ['USD']], + 'EE' => ['name' => 'Estonia', 'flag' => "\u{1F1EA}\u{1F1EA}", 'default_currency' => 'EUR', 'currencies' => ['EUR', 'GBP', 'USD', 'CHF', 'DKK', 'NOK', 'SEK']], + 'EG' => ['name' => 'Egypt', 'flag' => "\u{1F1EA}\u{1F1EC}", 'default_currency' => 'EGP', 'currencies' => ['EGP']], + 'ES' => ['name' => 'Spain', 'flag' => "\u{1F1EA}\u{1F1F8}", 'default_currency' => 'EUR', 'currencies' => ['EUR', 'GBP', 'USD', 'CHF', 'DKK', 'NOK', 'SEK']], + 'ET' => ['name' => 'Ethiopia', 'flag' => "\u{1F1EA}\u{1F1F9}", 'default_currency' => 'ETB', 'currencies' => ['ETB']], + 'FI' => ['name' => 'Finland', 'flag' => "\u{1F1EB}\u{1F1EE}", 'default_currency' => 'EUR', 'currencies' => ['EUR', 'GBP', 'USD', 'CHF', 'DKK', 'NOK', 'SEK']], + 'FR' => ['name' => 'France', 'flag' => "\u{1F1EB}\u{1F1F7}", 'default_currency' => 'EUR', 'currencies' => ['EUR', 'GBP', 'USD', 'CHF', 'DKK', 'NOK', 'SEK']], + 'GB' => ['name' => 'United Kingdom', 'flag' => "\u{1F1EC}\u{1F1E7}", 'default_currency' => 'GBP', 'currencies' => ['GBP', 'EUR', 'USD', 'CHF', 'DKK', 'NOK', 'SEK']], + 'GH' => ['name' => 'Ghana', 'flag' => "\u{1F1EC}\u{1F1ED}", 'default_currency' => 'GHS', 'currencies' => ['GHS']], + 'GM' => ['name' => 'Gambia', 'flag' => "\u{1F1EC}\u{1F1F2}", 'default_currency' => 'GMD', 'currencies' => ['GMD']], + 'GR' => ['name' => 'Greece', 'flag' => "\u{1F1EC}\u{1F1F7}", 'default_currency' => 'EUR', 'currencies' => ['EUR', 'GBP', 'USD', 'CHF', 'DKK', 'NOK', 'SEK']], + 'GT' => ['name' => 'Guatemala', 'flag' => "\u{1F1EC}\u{1F1F9}", 'default_currency' => 'GTQ', 'currencies' => ['GTQ']], + 'GY' => ['name' => 'Guyana', 'flag' => "\u{1F1EC}\u{1F1FE}", 'default_currency' => 'GYD', 'currencies' => ['GYD']], + 'HK' => ['name' => 'Hong Kong', 'flag' => "\u{1F1ED}\u{1F1F0}", 'default_currency' => 'HKD', 'currencies' => ['HKD', 'USD']], + 'HR' => ['name' => 'Croatia', 'flag' => "\u{1F1ED}\u{1F1F7}", 'default_currency' => 'EUR', 'currencies' => ['EUR', 'GBP', 'USD', 'CHF', 'DKK', 'NOK', 'SEK']], + 'HU' => ['name' => 'Hungary', 'flag' => "\u{1F1ED}\u{1F1FA}", 'default_currency' => 'HUF', 'currencies' => ['HUF', 'EUR', 'GBP', 'USD', 'CHF', 'DKK', 'NOK', 'SEK']], + 'ID' => ['name' => 'Indonesia', 'flag' => "\u{1F1EE}\u{1F1E9}", 'default_currency' => 'IDR', 'currencies' => ['IDR']], + 'IE' => ['name' => 'Ireland', 'flag' => "\u{1F1EE}\u{1F1EA}", 'default_currency' => 'EUR', 'currencies' => ['EUR', 'GBP', 'USD', 'CHF', 'DKK', 'NOK', 'SEK']], + 'IL' => ['name' => 'Israel', 'flag' => "\u{1F1EE}\u{1F1F1}", 'default_currency' => 'ILS', 'currencies' => ['ILS']], + 'IN' => ['name' => 'India', 'flag' => "\u{1F1EE}\u{1F1F3}", 'default_currency' => 'INR', 'currencies' => ['INR']], + 'IS' => ['name' => 'Iceland', 'flag' => "\u{1F1EE}\u{1F1F8}", 'default_currency' => 'ISK', 'currencies' => ['ISK', 'EUR', 'GBP', 'USD', 'CHF', 'DKK', 'NOK', 'SEK']], + 'IT' => ['name' => 'Italy', 'flag' => "\u{1F1EE}\u{1F1F9}", 'default_currency' => 'EUR', 'currencies' => ['EUR', 'GBP', 'USD', 'CHF', 'DKK', 'NOK', 'SEK']], + 'JM' => ['name' => 'Jamaica', 'flag' => "\u{1F1EF}\u{1F1F2}", 'default_currency' => 'JMD', 'currencies' => ['JMD']], + 'JO' => ['name' => 'Jordan', 'flag' => "\u{1F1EF}\u{1F1F4}", 'default_currency' => 'JOD', 'currencies' => ['JOD']], + 'JP' => ['name' => 'Japan', 'flag' => "\u{1F1EF}\u{1F1F5}", 'default_currency' => 'JPY', 'currencies' => ['JPY']], + 'KE' => ['name' => 'Kenya', 'flag' => "\u{1F1F0}\u{1F1EA}", 'default_currency' => 'KES', 'currencies' => ['KES']], + 'KH' => ['name' => 'Cambodia', 'flag' => "\u{1F1F0}\u{1F1ED}", 'default_currency' => 'USD', 'currencies' => ['USD']], + 'KR' => ['name' => 'South Korea', 'flag' => "\u{1F1F0}\u{1F1F7}", 'default_currency' => 'KRW', 'currencies' => ['KRW']], + 'KW' => ['name' => 'Kuwait', 'flag' => "\u{1F1F0}\u{1F1FC}", 'default_currency' => 'KWD', 'currencies' => ['KWD']], + 'LC' => ['name' => 'St. Lucia', 'flag' => "\u{1F1F1}\u{1F1E8}", 'default_currency' => 'XCD', 'currencies' => ['XCD']], + 'LI' => ['name' => 'Liechtenstein', 'flag' => "\u{1F1F1}\u{1F1EE}", 'default_currency' => 'CHF', 'currencies' => ['CHF', 'EUR', 'GBP', 'USD', 'DKK', 'NOK', 'SEK']], + 'LK' => ['name' => 'Sri Lanka', 'flag' => "\u{1F1F1}\u{1F1F0}", 'default_currency' => 'LKR', 'currencies' => ['LKR']], + 'LT' => ['name' => 'Lithuania', 'flag' => "\u{1F1F1}\u{1F1F9}", 'default_currency' => 'EUR', 'currencies' => ['EUR', 'GBP', 'USD', 'CHF', 'DKK', 'NOK', 'SEK']], + 'LU' => ['name' => 'Luxembourg', 'flag' => "\u{1F1F1}\u{1F1FA}", 'default_currency' => 'EUR', 'currencies' => ['EUR', 'GBP', 'USD', 'CHF', 'DKK', 'NOK', 'SEK']], + 'LV' => ['name' => 'Latvia', 'flag' => "\u{1F1F1}\u{1F1FB}", 'default_currency' => 'EUR', 'currencies' => ['EUR', 'GBP', 'USD', 'CHF', 'DKK', 'NOK', 'SEK']], + 'MA' => ['name' => 'Morocco', 'flag' => "\u{1F1F2}\u{1F1E6}", 'default_currency' => 'MAD', 'currencies' => ['MAD']], + 'MC' => ['name' => 'Monaco', 'flag' => "\u{1F1F2}\u{1F1E8}", 'default_currency' => 'EUR', 'currencies' => ['EUR']], + 'MD' => ['name' => 'Moldova', 'flag' => "\u{1F1F2}\u{1F1E9}", 'default_currency' => 'MDL', 'currencies' => ['MDL']], + 'MG' => ['name' => 'Madagascar', 'flag' => "\u{1F1F2}\u{1F1EC}", 'default_currency' => 'MGA', 'currencies' => ['MGA']], + 'MK' => ['name' => 'North Macedonia', 'flag' => "\u{1F1F2}\u{1F1F0}", 'default_currency' => 'MKD', 'currencies' => ['MKD']], + 'MN' => ['name' => 'Mongolia', 'flag' => "\u{1F1F2}\u{1F1F3}", 'default_currency' => 'MNT', 'currencies' => ['MNT']], + 'MO' => ['name' => 'Macao', 'flag' => "\u{1F1F2}\u{1F1F4}", 'default_currency' => 'MOP', 'currencies' => ['MOP']], + 'MT' => ['name' => 'Malta', 'flag' => "\u{1F1F2}\u{1F1F9}", 'default_currency' => 'EUR', 'currencies' => ['EUR', 'GBP', 'USD', 'CHF', 'DKK', 'NOK', 'SEK']], + 'MU' => ['name' => 'Mauritius', 'flag' => "\u{1F1F2}\u{1F1FA}", 'default_currency' => 'MUR', 'currencies' => ['MUR']], + 'MX' => ['name' => 'Mexico', 'flag' => "\u{1F1F2}\u{1F1FD}", 'default_currency' => 'MXN', 'currencies' => ['MXN']], + 'MY' => ['name' => 'Malaysia', 'flag' => "\u{1F1F2}\u{1F1FE}", 'default_currency' => 'MYR', 'currencies' => ['MYR']], + 'NA' => ['name' => 'Namibia', 'flag' => "\u{1F1F3}\u{1F1E6}", 'default_currency' => 'NAD', 'currencies' => ['NAD']], + 'NG' => ['name' => 'Nigeria', 'flag' => "\u{1F1F3}\u{1F1EC}", 'default_currency' => 'NGN', 'currencies' => ['NGN']], + 'NL' => ['name' => 'Netherlands', 'flag' => "\u{1F1F3}\u{1F1F1}", 'default_currency' => 'EUR', 'currencies' => ['EUR', 'GBP', 'USD', 'CHF', 'DKK', 'NOK', 'SEK']], + 'NO' => ['name' => 'Norway', 'flag' => "\u{1F1F3}\u{1F1F4}", 'default_currency' => 'NOK', 'currencies' => ['NOK', 'EUR', 'GBP', 'USD', 'CHF', 'DKK', 'SEK']], + 'NZ' => ['name' => 'New Zealand', 'flag' => "\u{1F1F3}\u{1F1FF}", 'default_currency' => 'NZD', 'currencies' => ['NZD']], + 'OM' => ['name' => 'Oman', 'flag' => "\u{1F1F4}\u{1F1F2}", 'default_currency' => 'OMR', 'currencies' => ['OMR']], + 'PA' => ['name' => 'Panama', 'flag' => "\u{1F1F5}\u{1F1E6}", 'default_currency' => 'USD', 'currencies' => ['USD']], + 'PE' => ['name' => 'Peru', 'flag' => "\u{1F1F5}\u{1F1EA}", 'default_currency' => 'PEN', 'currencies' => ['PEN']], + 'PH' => ['name' => 'Philippines', 'flag' => "\u{1F1F5}\u{1F1ED}", 'default_currency' => 'PHP', 'currencies' => ['PHP']], + 'PK' => ['name' => 'Pakistan', 'flag' => "\u{1F1F5}\u{1F1F0}", 'default_currency' => 'PKR', 'currencies' => ['PKR']], + 'PL' => ['name' => 'Poland', 'flag' => "\u{1F1F5}\u{1F1F1}", 'default_currency' => 'PLN', 'currencies' => ['PLN', 'EUR', 'GBP', 'USD', 'CHF', 'DKK', 'NOK', 'SEK']], + 'PT' => ['name' => 'Portugal', 'flag' => "\u{1F1F5}\u{1F1F9}", 'default_currency' => 'EUR', 'currencies' => ['EUR', 'GBP', 'USD', 'CHF', 'DKK', 'NOK', 'SEK']], + 'PY' => ['name' => 'Paraguay', 'flag' => "\u{1F1F5}\u{1F1FE}", 'default_currency' => 'PYG', 'currencies' => ['PYG']], + 'QA' => ['name' => 'Qatar', 'flag' => "\u{1F1F6}\u{1F1E6}", 'default_currency' => 'QAR', 'currencies' => ['QAR']], + 'RO' => ['name' => 'Romania', 'flag' => "\u{1F1F7}\u{1F1F4}", 'default_currency' => 'RON', 'currencies' => ['RON', 'EUR', 'GBP', 'USD', 'CHF', 'DKK', 'NOK', 'SEK']], + 'RS' => ['name' => 'Serbia', 'flag' => "\u{1F1F7}\u{1F1F8}", 'default_currency' => 'RSD', 'currencies' => ['RSD']], + 'RW' => ['name' => 'Rwanda', 'flag' => "\u{1F1F7}\u{1F1FC}", 'default_currency' => 'RWF', 'currencies' => ['RWF']], + 'SA' => ['name' => 'Saudi Arabia', 'flag' => "\u{1F1F8}\u{1F1E6}", 'default_currency' => 'SAR', 'currencies' => ['SAR']], + 'SE' => ['name' => 'Sweden', 'flag' => "\u{1F1F8}\u{1F1EA}", 'default_currency' => 'SEK', 'currencies' => ['SEK', 'EUR', 'GBP', 'USD', 'CHF', 'DKK', 'NOK']], + 'SG' => ['name' => 'Singapore', 'flag' => "\u{1F1F8}\u{1F1EC}", 'default_currency' => 'SGD', 'currencies' => ['SGD', 'USD']], + 'SI' => ['name' => 'Slovenia', 'flag' => "\u{1F1F8}\u{1F1EE}", 'default_currency' => 'EUR', 'currencies' => ['EUR', 'GBP', 'USD', 'CHF', 'DKK', 'NOK', 'SEK']], + 'SK' => ['name' => 'Slovakia', 'flag' => "\u{1F1F8}\u{1F1F0}", 'default_currency' => 'EUR', 'currencies' => ['EUR', 'GBP', 'USD', 'CHF', 'DKK', 'NOK', 'SEK']], + 'SN' => ['name' => 'Senegal', 'flag' => "\u{1F1F8}\u{1F1F3}", 'default_currency' => 'XOF', 'currencies' => ['XOF']], + 'SV' => ['name' => 'El Salvador', 'flag' => "\u{1F1F8}\u{1F1FB}", 'default_currency' => 'USD', 'currencies' => ['USD']], + 'TH' => ['name' => 'Thailand', 'flag' => "\u{1F1F9}\u{1F1ED}", 'default_currency' => 'THB', 'currencies' => ['THB']], + 'TN' => ['name' => 'Tunisia', 'flag' => "\u{1F1F9}\u{1F1F3}", 'default_currency' => 'TND', 'currencies' => ['TND']], + 'TR' => ['name' => "T\u{00FC}rkiye", 'flag' => "\u{1F1F9}\u{1F1F7}", 'default_currency' => 'TRY', 'currencies' => ['TRY']], + 'TT' => ['name' => 'Trinidad & Tobago', 'flag' => "\u{1F1F9}\u{1F1F9}", 'default_currency' => 'TTD', 'currencies' => ['TTD']], + 'TW' => ['name' => 'Taiwan', 'flag' => "\u{1F1F9}\u{1F1FC}", 'default_currency' => 'TWD', 'currencies' => ['TWD']], + 'TZ' => ['name' => 'Tanzania', 'flag' => "\u{1F1F9}\u{1F1FF}", 'default_currency' => 'TZS', 'currencies' => ['TZS']], + 'US' => ['name' => 'United States', 'flag' => "\u{1F1FA}\u{1F1F8}", 'default_currency' => 'USD', 'currencies' => ['USD']], + 'UY' => ['name' => 'Uruguay', 'flag' => "\u{1F1FA}\u{1F1FE}", 'default_currency' => 'UYU', 'currencies' => ['UYU']], + 'UZ' => ['name' => 'Uzbekistan', 'flag' => "\u{1F1FA}\u{1F1FF}", 'default_currency' => 'UZS', 'currencies' => ['UZS']], + 'VN' => ['name' => 'Vietnam', 'flag' => "\u{1F1FB}\u{1F1F3}", 'default_currency' => 'VND', 'currencies' => ['VND']], + 'ZA' => ['name' => 'South Africa', 'flag' => "\u{1F1FF}\u{1F1E6}", 'default_currency' => 'ZAR', 'currencies' => ['ZAR']], + ]; + + /** + * @var array + */ + public const CURRENCY_NAMES = [ + 'AED' => 'UAE Dirham', + 'ALL' => 'Albanian Lek', + 'AMD' => 'Armenian Dram', + 'ARS' => 'Argentine Peso', + 'AUD' => 'Australian Dollar', + 'BAM' => 'Convertible Mark', + 'BHD' => 'Bahraini Dinar', + 'BND' => 'Brunei Dollar', + 'BOB' => 'Boliviano', + 'BSD' => 'Bahamian Dollar', + 'BWP' => 'Botswana Pula', + 'CAD' => 'Canadian Dollar', + 'CHF' => 'Swiss Franc', + 'CLP' => 'Chilean Peso', + 'COP' => 'Colombian Peso', + 'CRC' => 'Costa Rican Colon', + 'CZK' => 'Czech Koruna', + 'DKK' => 'Danish Krone', + 'DOP' => 'Dominican Peso', + 'EGP' => 'Egyptian Pound', + 'ETB' => 'Ethiopian Birr', + 'EUR' => 'Euro', + 'GBP' => 'British Pound', + 'GHS' => 'Ghanaian Cedi', + 'GMD' => 'Gambian Dalasi', + 'GTQ' => 'Guatemalan Quetzal', + 'GYD' => 'Guyanese Dollar', + 'HKD' => 'Hong Kong Dollar', + 'HUF' => 'Hungarian Forint', + 'IDR' => 'Indonesian Rupiah', + 'ILS' => 'Israeli Shekel', + 'INR' => 'Indian Rupee', + 'ISK' => 'Icelandic Krona', + 'JMD' => 'Jamaican Dollar', + 'JOD' => 'Jordanian Dinar', + 'JPY' => 'Japanese Yen', + 'KES' => 'Kenyan Shilling', + 'KRW' => 'South Korean Won', + 'KWD' => 'Kuwaiti Dinar', + 'LKR' => 'Sri Lankan Rupee', + 'MAD' => 'Moroccan Dirham', + 'MDL' => 'Moldovan Leu', + 'MGA' => 'Malagasy Ariary', + 'MKD' => 'Macedonian Denar', + 'MNT' => 'Mongolian Tugrik', + 'MOP' => 'Macanese Pataca', + 'MUR' => 'Mauritian Rupee', + 'MXN' => 'Mexican Peso', + 'MYR' => 'Malaysian Ringgit', + 'NAD' => 'Namibian Dollar', + 'NGN' => 'Nigerian Naira', + 'NOK' => 'Norwegian Krone', + 'NZD' => 'New Zealand Dollar', + 'OMR' => 'Omani Rial', + 'PEN' => 'Peruvian Sol', + 'PHP' => 'Philippine Peso', + 'PKR' => 'Pakistani Rupee', + 'PLN' => 'Polish Zloty', + 'PYG' => 'Paraguayan Guarani', + 'QAR' => 'Qatari Riyal', + 'RON' => 'Romanian Leu', + 'RSD' => 'Serbian Dinar', + 'RWF' => 'Rwandan Franc', + 'SAR' => 'Saudi Riyal', + 'SEK' => 'Swedish Krona', + 'SGD' => 'Singapore Dollar', + 'THB' => 'Thai Baht', + 'TND' => 'Tunisian Dinar', + 'TRY' => 'Turkish Lira', + 'TTD' => 'Trinidad & Tobago Dollar', + 'TWD' => 'New Taiwan Dollar', + 'TZS' => 'Tanzanian Shilling', + 'USD' => 'US Dollar', + 'UYU' => 'Uruguayan Peso', + 'UZS' => 'Uzbekistani Som', + 'VND' => 'Vietnamese Dong', + 'XCD' => 'East Caribbean Dollar', + 'XOF' => 'West African CFA Franc', + 'ZAR' => 'South African Rand', + ]; + + public static function currencyName(string $code): string + { + return self::CURRENCY_NAMES[strtoupper($code)] ?? strtoupper($code); + } + + /** + * @return array}> + */ + public static function all(): array + { + return self::SUPPORTED_COUNTRIES; + } + + public static function isSupported(string $code): bool + { + return isset(self::SUPPORTED_COUNTRIES[strtoupper($code)]); + } + + public static function defaultCurrency(string $countryCode): ?string + { + return self::SUPPORTED_COUNTRIES[strtoupper($countryCode)]['default_currency'] ?? null; + } + + /** + * @return list + */ + public static function availableCurrencies(string $countryCode): array + { + return self::SUPPORTED_COUNTRIES[strtoupper($countryCode)]['currencies'] ?? []; + } + + public static function isValidCurrencyForCountry(string $countryCode, string $currencyCode): bool + { + return in_array(strtoupper($currencyCode), self::availableCurrencies($countryCode), true); + } + + /** + * @return list + */ + public static function supportedCountryCodes(): array + { + return array_keys(self::SUPPORTED_COUNTRIES); + } +} diff --git a/composer.json b/composer.json index bdf6a617..aa8d99a9 100644 --- a/composer.json +++ b/composer.json @@ -28,6 +28,7 @@ "league/flysystem-aws-s3-v3": "^3.30", "livewire/blaze": "^1.0", "livewire/flux": "^2.13", + "livewire/flux-pro": "^2.13", "livewire/livewire": "^4.0", "sentry/sentry-laravel": "^4.13", "simonhamp/the-og": "^0.7.0", @@ -100,5 +101,10 @@ } }, "minimum-stability": "stable", - "prefer-stable": true + "prefer-stable": true, + "repositories": [{ + "name": "flux-pro", + "type": "composer", + "url": "https://composer.fluxui.dev" + }] } diff --git a/composer.lock b/composer.lock index fdcee1a1..905b04d4 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "65176a8ba5da980556da411f0a6d6354", + "content-hash": "0cc77dd249810289ccbf001bbaec7aa5", "packages": [ { "name": "artesaos/seotools", @@ -4734,6 +4734,79 @@ }, "time": "2026-03-03T03:32:35+00:00" }, + { + "name": "livewire/flux-pro", + "version": "2.13.0", + "dist": { + "type": "zip", + "url": "https://composer.fluxui.dev/download/a1357bcd-0c9f-48ec-a532-2aced09459e7/flux-pro-2.13.0.zip", + "reference": "3d91059d053ac94a2f522944f581c3876963656c", + "shasum": "dc3d4e5baedc6e525f41f25474885413e5c1e13e" + }, + "require": { + "illuminate/console": "^10.0|^11.0|^12.0|^13.0", + "illuminate/support": "^10.0|^11.0|^12.0|^13.0", + "illuminate/view": "^10.0|^11.0|^12.0|^13.0", + "laravel/prompts": "^0.1.24|^0.2|^0.3", + "livewire/flux": "2.13.0|dev-main", + "livewire/livewire": "^3.7.4|^4.0", + "php": "^8.1", + "symfony/console": "^6.0|^7.0|^8.0" + }, + "require-dev": { + "livewire/volt": "*", + "orchestra/testbench": "^10.8|^11.0" + }, + "type": "library", + "extra": { + "laravel": { + "aliases": { + "Flux": "FluxPro\\FluxPro" + }, + "providers": [ + "FluxPro\\FluxProServiceProvider" + ] + } + }, + "autoload": { + "files": [ + "src/helpers.php" + ], + "psr-4": { + "FluxPro\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "App\\": "workbench/app/" + } + }, + "scripts": { + "serve": [ + "Composer\\Config::disableProcessTimeout", + "@php vendor/bin/testbench workbench:build --ansi", + "@php vendor/bin/testbench serve --port 3000 --ansi" + ] + }, + "license": [ + "proprietary" + ], + "authors": [ + { + "name": "Caleb Porzio", + "email": "calebporzio@gmail.com" + } + ], + "description": "The pro version of Flux, the official UI component library for Livewire.", + "keywords": [ + "components", + "flux", + "laravel", + "livewire", + "ui" + ], + "time": "2026-03-03T04:07:04+00:00" + }, { "name": "livewire/livewire", "version": "v4.2.1", diff --git a/database/factories/DeveloperAccountFactory.php b/database/factories/DeveloperAccountFactory.php index 5dc72735..16fec8b5 100644 --- a/database/factories/DeveloperAccountFactory.php +++ b/database/factories/DeveloperAccountFactory.php @@ -26,6 +26,8 @@ public function definition(): array 'payouts_enabled' => true, 'charges_enabled' => true, 'onboarding_completed_at' => now(), + 'country' => 'US', + 'payout_currency' => 'USD', ]; } diff --git a/database/migrations/2026_03_24_000001_add_country_and_payout_currency_to_developer_accounts_table.php b/database/migrations/2026_03_24_000001_add_country_and_payout_currency_to_developer_accounts_table.php new file mode 100644 index 00000000..6fa682ae --- /dev/null +++ b/database/migrations/2026_03_24_000001_add_country_and_payout_currency_to_developer_accounts_table.php @@ -0,0 +1,29 @@ +string('country', 2)->nullable()->after('stripe_connect_status'); + $table->string('payout_currency', 3)->nullable()->after('country'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('developer_accounts', function (Blueprint $table) { + $table->dropColumn(['country', 'payout_currency']); + }); + } +}; diff --git a/polyscope.json b/polyscope.json index fb535e68..8fe67fd7 100644 --- a/polyscope.json +++ b/polyscope.json @@ -2,7 +2,10 @@ "scripts": { "setup": [ "herd link", - "herd secure" + "herd secure", + "composer install", + "npm i", + "npm run build" ], "archive": [ "herd unsecure", diff --git a/resources/views/livewire/customer/developer/onboarding.blade.php b/resources/views/livewire/customer/developer/onboarding.blade.php index b5de5996..71a322c2 100644 --- a/resources/views/livewire/customer/developer/onboarding.blade.php +++ b/resources/views/livewire/customer/developer/onboarding.blade.php @@ -24,6 +24,14 @@ @endif
+ {{-- Status for existing account --}} + @if ($this->hasExistingAccount && $this->developerAccount) + + Onboarding Incomplete + Your Stripe account requires additional information before you can receive payouts. + + @endif + {{-- Hero Card --}}
@@ -38,11 +46,7 @@ @endif - @if ($this->hasExistingAccount) - You've started the onboarding process. Complete the remaining steps to start receiving payouts. - @else - Connect your Stripe account to receive payments when users purchase your plugins. - @endif + Connect your Stripe account to receive payments when users purchase your plugins.
@@ -69,19 +73,52 @@
- {{-- Status for existing account --}} - @if ($this->hasExistingAccount && $this->developerAccount) - - Onboarding Incomplete - Your Stripe account requires additional information before you can receive payouts. - - @endif + {{-- Country & Currency Selection --}} +
+ Your Country + + Select the country where your bank account is located. This determines which currencies are available for payouts. + + +
+
+ + @foreach ($this->countries as $code => $details) + + {{ $details['flag'] }} {{ $details['name'] }} + + @endforeach + + @error('country') + {{ $message }} + @enderror +
+ + @if (count($this->availableCurrencies) > 0) +
+ + @foreach ($this->availableCurrencies as $code => $name) + + {{ $name }} ({{ $code }}) + + @endforeach + + @error('payout_currency') + {{ $message }} + @enderror +
+ @endif +
+
{{-- Developer Terms Agreement & CTA Button --}}
@csrf + + + @if ($this->developerAccount?->hasAcceptedCurrentTerms()) @@ -137,7 +174,7 @@ class="mt-0.5 size-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-5
@endif - + @if ($this->hasExistingAccount) Continue Onboarding @else diff --git a/tests/Feature/DeveloperTermsTest.php b/tests/Feature/DeveloperTermsTest.php index 06d5284a..f7986f2e 100644 --- a/tests/Feature/DeveloperTermsTest.php +++ b/tests/Feature/DeveloperTermsTest.php @@ -80,6 +80,7 @@ public function onboarding_start_records_terms_acceptance(): void $mockService = Mockery::mock(StripeConnectService::class); $mockService->shouldReceive('createConnectAccount') ->once() + ->with($user, 'US', 'USD') ->andReturnUsing(fn () => DeveloperAccount::factory()->pending()->create(['user_id' => $user->id])); $mockService->shouldReceive('createOnboardingLink') ->once() @@ -90,6 +91,8 @@ public function onboarding_start_records_terms_acceptance(): void $response = $this->actingAs($user) ->post(route('customer.developer.onboarding.start'), [ 'accepted_plugin_terms' => '1', + 'country' => 'US', + 'payout_currency' => 'USD', ]); $response->assertRedirect('https://connect.stripe.com/setup/test'); @@ -124,6 +127,8 @@ public function onboarding_start_does_not_overwrite_existing_terms_acceptance(): $this->actingAs($user) ->post(route('customer.developer.onboarding.start'), [ 'accepted_plugin_terms' => '1', + 'country' => 'GB', + 'payout_currency' => 'GBP', ]); $developerAccount->refresh(); @@ -131,6 +136,8 @@ public function onboarding_start_does_not_overwrite_existing_terms_acceptance(): $originalTime->toDateTimeString(), $developerAccount->accepted_plugin_terms_at->toDateTimeString() ); + $this->assertEquals('GB', $developerAccount->country); + $this->assertEquals('GBP', $developerAccount->payout_currency); } /** @test */ @@ -240,4 +247,121 @@ public function plugin_create_page_shows_github_required_for_non_connected_user( ->assertStatus(200) ->assertSee('GitHub Connection Required'); } + + /** @test */ + public function onboarding_start_requires_country(): void + { + $user = User::factory()->create(); + + $response = $this->actingAs($user) + ->post(route('customer.developer.onboarding.start'), [ + 'accepted_plugin_terms' => '1', + 'payout_currency' => 'USD', + ]); + + $response->assertSessionHasErrors('country'); + } + + /** @test */ + public function onboarding_start_rejects_invalid_country_code(): void + { + $user = User::factory()->create(); + + $response = $this->actingAs($user) + ->post(route('customer.developer.onboarding.start'), [ + 'accepted_plugin_terms' => '1', + 'country' => 'XX', + 'payout_currency' => 'USD', + ]); + + $response->assertSessionHasErrors('country'); + } + + /** @test */ + public function onboarding_start_requires_payout_currency(): void + { + $user = User::factory()->create(); + + $response = $this->actingAs($user) + ->post(route('customer.developer.onboarding.start'), [ + 'accepted_plugin_terms' => '1', + 'country' => 'US', + ]); + + $response->assertSessionHasErrors('payout_currency'); + } + + /** @test */ + public function onboarding_start_rejects_invalid_currency_for_country(): void + { + $user = User::factory()->create(); + + $response = $this->actingAs($user) + ->post(route('customer.developer.onboarding.start'), [ + 'accepted_plugin_terms' => '1', + 'country' => 'US', + 'payout_currency' => 'EUR', + ]); + + $response->assertSessionHasErrors('payout_currency'); + } + + /** @test */ + public function onboarding_start_stores_country_and_currency_on_developer_account(): void + { + $user = User::factory()->create(); + + $mockService = Mockery::mock(StripeConnectService::class); + $mockService->shouldReceive('createConnectAccount') + ->once() + ->with($user, 'FR', 'EUR') + ->andReturnUsing(fn () => DeveloperAccount::factory()->pending()->create([ + 'user_id' => $user->id, + 'country' => 'FR', + 'payout_currency' => 'EUR', + ])); + $mockService->shouldReceive('createOnboardingLink') + ->once() + ->andReturn('https://connect.stripe.com/setup/test'); + + $this->app->instance(StripeConnectService::class, $mockService); + + $this->actingAs($user) + ->post(route('customer.developer.onboarding.start'), [ + 'accepted_plugin_terms' => '1', + 'country' => 'FR', + 'payout_currency' => 'EUR', + ]); + + $developerAccount = $user->fresh()->developerAccount; + $this->assertEquals('FR', $developerAccount->country); + $this->assertEquals('EUR', $developerAccount->payout_currency); + } + + /** @test */ + public function onboarding_page_shows_country_and_currency_fields(): void + { + $user = User::factory()->create(); + + Livewire::actingAs($user) + ->test(Onboarding::class) + ->assertSee('Your Country') + ->assertSee('Select your country') + ->assertStatus(200); + } + + /** @test */ + public function onboarding_component_updates_currency_when_country_changes(): void + { + $user = User::factory()->create(); + + Livewire::actingAs($user) + ->test(Onboarding::class) + ->set('country', 'FR') + ->assertSet('payoutCurrency', 'EUR') + ->set('country', 'US') + ->assertSet('payoutCurrency', 'USD') + ->set('country', 'GB') + ->assertSet('payoutCurrency', 'GBP'); + } } diff --git a/tests/Feature/Livewire/Customer/DeveloperPagesTest.php b/tests/Feature/Livewire/Customer/DeveloperPagesTest.php index 8b5c6946..688f6c6d 100644 --- a/tests/Feature/Livewire/Customer/DeveloperPagesTest.php +++ b/tests/Feature/Livewire/Customer/DeveloperPagesTest.php @@ -53,6 +53,7 @@ public function test_onboarding_component_shows_start_selling_for_new_user(): vo ->assertSee('Start Selling Plugins') ->assertSee('Connect with Stripe') ->assertSee('Plugin Developer Terms and Conditions') + ->assertSee('Your Country') ->assertStatus(200); } diff --git a/tests/Unit/StripeConnectCountriesTest.php b/tests/Unit/StripeConnectCountriesTest.php new file mode 100644 index 00000000..a8d1fb30 --- /dev/null +++ b/tests/Unit/StripeConnectCountriesTest.php @@ -0,0 +1,145 @@ +assertCount(108, $countries); + $this->assertArrayHasKey('US', $countries); + $this->assertArrayHasKey('GB', $countries); + $this->assertArrayHasKey('DE', $countries); + } + + /** @test */ + public function is_supported_returns_true_for_valid_country(): void + { + $this->assertTrue(StripeConnectCountries::isSupported('US')); + $this->assertTrue(StripeConnectCountries::isSupported('GB')); + $this->assertTrue(StripeConnectCountries::isSupported('FR')); + } + + /** @test */ + public function is_supported_returns_false_for_invalid_country(): void + { + $this->assertFalse(StripeConnectCountries::isSupported('XX')); + $this->assertFalse(StripeConnectCountries::isSupported('ZZ')); + } + + /** @test */ + public function is_supported_is_case_insensitive(): void + { + $this->assertTrue(StripeConnectCountries::isSupported('us')); + $this->assertTrue(StripeConnectCountries::isSupported('gb')); + } + + /** @test */ + public function default_currency_returns_correct_currency(): void + { + $this->assertEquals('USD', StripeConnectCountries::defaultCurrency('US')); + $this->assertEquals('GBP', StripeConnectCountries::defaultCurrency('GB')); + $this->assertEquals('EUR', StripeConnectCountries::defaultCurrency('DE')); + $this->assertEquals('AUD', StripeConnectCountries::defaultCurrency('AU')); + $this->assertEquals('JPY', StripeConnectCountries::defaultCurrency('JP')); + } + + /** @test */ + public function default_currency_returns_null_for_unsupported_country(): void + { + $this->assertNull(StripeConnectCountries::defaultCurrency('XX')); + } + + /** @test */ + public function available_currencies_returns_array(): void + { + $currencies = StripeConnectCountries::availableCurrencies('US'); + + $this->assertIsArray($currencies); + $this->assertContains('USD', $currencies); + } + + /** @test */ + public function available_currencies_returns_multiple_for_european_countries(): void + { + $currencies = StripeConnectCountries::availableCurrencies('DE'); + + $this->assertContains('EUR', $currencies); + $this->assertContains('GBP', $currencies); + $this->assertContains('USD', $currencies); + } + + /** @test */ + public function available_currencies_returns_empty_for_unsupported_country(): void + { + $this->assertEmpty(StripeConnectCountries::availableCurrencies('XX')); + } + + /** @test */ + public function is_valid_currency_for_country_validates_correctly(): void + { + $this->assertTrue(StripeConnectCountries::isValidCurrencyForCountry('US', 'USD')); + $this->assertTrue(StripeConnectCountries::isValidCurrencyForCountry('DE', 'EUR')); + $this->assertTrue(StripeConnectCountries::isValidCurrencyForCountry('DE', 'USD')); + $this->assertFalse(StripeConnectCountries::isValidCurrencyForCountry('US', 'EUR')); + $this->assertFalse(StripeConnectCountries::isValidCurrencyForCountry('JP', 'USD')); + } + + /** @test */ + public function supported_country_codes_returns_array_of_codes(): void + { + $codes = StripeConnectCountries::supportedCountryCodes(); + + $this->assertContains('US', $codes); + $this->assertContains('GB', $codes); + $this->assertCount(108, $codes); + } + + /** @test */ + public function currency_name_returns_correct_name(): void + { + $this->assertEquals('US Dollar', StripeConnectCountries::currencyName('USD')); + $this->assertEquals('Euro', StripeConnectCountries::currencyName('EUR')); + $this->assertEquals('British Pound', StripeConnectCountries::currencyName('GBP')); + $this->assertEquals('Japanese Yen', StripeConnectCountries::currencyName('JPY')); + } + + /** @test */ + public function currency_name_returns_code_for_unknown_currency(): void + { + $this->assertEquals('ZZZ', StripeConnectCountries::currencyName('ZZZ')); + } + + /** @test */ + public function every_used_currency_has_a_name(): void + { + $countries = StripeConnectCountries::all(); + $currencies = collect($countries)->pluck('currencies')->flatten()->unique(); + + foreach ($currencies as $code) { + $name = StripeConnectCountries::currencyName($code); + $this->assertNotEquals($code, $name, "Currency {$code} is missing a name in CURRENCY_NAMES"); + } + } + + /** @test */ + public function each_country_has_required_keys(): void + { + foreach (StripeConnectCountries::all() as $code => $details) { + $this->assertArrayHasKey('name', $details, "Country {$code} missing 'name'"); + $this->assertArrayHasKey('flag', $details, "Country {$code} missing 'flag'"); + $this->assertArrayHasKey('default_currency', $details, "Country {$code} missing 'default_currency'"); + $this->assertArrayHasKey('currencies', $details, "Country {$code} missing 'currencies'"); + $this->assertNotEmpty($details['currencies'], "Country {$code} has empty currencies"); + $this->assertContains($details['default_currency'], $details['currencies'], "Country {$code} default currency not in currencies list"); + $this->assertEquals(2, strlen($code), "Country code {$code} is not 2 characters"); + $this->assertEquals(3, strlen($details['default_currency']), "Country {$code} default currency is not 3 characters"); + } + } +}