diff --git a/app/Models/User.php b/app/Models/User.php index aad3217..7c6bc76 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -30,6 +30,7 @@ class User extends Authenticatable implements FilamentUser, MustVerifyEmail 'email', 'password', 'level', + 'polar_cust_id', ]; /** diff --git a/app/Services/Payments/Providers/PolarProvider.php b/app/Services/Payments/Providers/PolarProvider.php index 0ecfa3f..f0a4e9d 100644 --- a/app/Services/Payments/Providers/PolarProvider.php +++ b/app/Services/Payments/Providers/PolarProvider.php @@ -786,54 +786,87 @@ class PolarProvider implements PaymentProviderContract // Helper methods protected function getOrCreateCustomer(User $user): array { - // First, try to find existing customer by external_id - try { - $response = $this->makeAuthenticatedRequest('GET', '/customers', [ - 'external_id' => (string) $user->id, - 'limit' => 1, + // NEW 1:1 BINDING LOGIC: Use polar_cust_id for secure user binding + + // Check if user already has a Polar customer ID stored + if ($user->polar_cust_id) { + Log::info('User has existing Polar customer ID, using it', [ + 'user_id' => $user->id, + 'polar_cust_id' => $user->polar_cust_id, ]); - if ($response->successful()) { - $data = $response->json(); - if (! empty($data['items'])) { - Log::info('Found existing Polar customer by external_id', [ + try { + $response = $this->makeAuthenticatedRequest('GET', '/customers/'.$user->polar_cust_id); + + if ($response->successful()) { + $customer = $response->json(); + Log::info('Successfully retrieved existing Polar customer', [ 'user_id' => $user->id, - 'customer_id' => $data['items'][0]['id'], + 'customer_id' => $customer['id'], ]); - return $data['items'][0]; + return $customer; + } else { + Log::warning('Stored Polar customer ID not found, will create new one', [ + 'user_id' => $user->id, + 'polar_cust_id' => $user->polar_cust_id, + 'status_code' => $response->status(), + ]); + // Clear the invalid ID and continue to create new customer + $user->update(['polar_cust_id' => null]); } + } catch (\Exception $e) { + Log::warning('Failed to retrieve stored Polar customer, will create new one', [ + 'user_id' => $user->id, + 'polar_cust_id' => $user->polar_cust_id, + 'error' => $e->getMessage(), + ]); + // Clear the invalid ID and continue to create new customer + $user->update(['polar_cust_id' => null]); } - } catch (\Exception $e) { - Log::info('Customer not found by external_id, will create new one', [ - 'user_id' => $user->id, - ]); } - // Try to find by email as fallback + // No stored Polar customer ID, search by email to find existing customer + Log::info('No stored Polar customer ID, searching by email', [ + 'user_id' => $user->id, + 'email' => $user->email, + ]); + try { $response = $this->makeAuthenticatedRequest('GET', '/customers', [ 'email' => $user->email, - 'limit' => 1, + 'limit' => 10, ]); if ($response->successful()) { $data = $response->json(); if (! empty($data['items'])) { + $customer = $data['items'][0]; // Take the first match Log::info('Found existing Polar customer by email', [ 'user_id' => $user->id, - 'customer_id' => $data['items'][0]['id'], + 'customer_id' => $customer['id'], + 'customer_email' => $customer['email'], ]); - return $data['items'][0]; + // Store the Polar customer ID for future use + $user->update(['polar_cust_id' => $customer['id']]); + + return $customer; } } } catch (\Exception $e) { - Log::info('Customer not found by email, will create new one', [ + Log::info('No existing Polar customer found by email', [ 'user_id' => $user->id, + 'error' => $e->getMessage(), ]); } + // No existing customer found, create new one + Log::info('Creating new Polar customer for user', [ + 'user_id' => $user->id, + 'email' => $user->email, + ]); + // Create new customer $customerData = [ 'email' => $user->email, @@ -864,7 +897,38 @@ class PolarProvider implements PaymentProviderContract 'email' => $user->email, ]); - return $this->findExistingCustomer($user); + // With the new 1:1 binding system, this shouldn't happen often + // But if it does, we'll handle it by searching by email again + Log::warning('Customer creation conflict, searching by email as fallback', [ + 'user_id' => $user->id, + 'email' => $user->email, + ]); + + // Fallback: search by email one more time + try { + $response = $this->makeAuthenticatedRequest('GET', '/customers', [ + 'email' => $user->email, + 'limit' => 10, + ]); + + if ($response->successful()) { + $data = $response->json(); + if (! empty($data['items'])) { + $customer = $data['items'][0]; + // Store the found customer ID + $user->update(['polar_cust_id' => $customer['id']]); + + return $customer; + } + } + } catch (\Exception $e) { + Log::error('Fallback email search also failed', [ + 'user_id' => $user->id, + 'error' => $e->getMessage(), + ]); + } + + throw new \Exception('Unable to create or find Polar customer account. Please contact support.'); } } } @@ -884,51 +948,18 @@ class PolarProvider implements PaymentProviderContract throw new \Exception('Invalid response from Polar API: missing customer ID'); } - Log::info('Created new Polar customer', [ + // Store the new Polar customer ID for 1:1 binding + $user->update(['polar_cust_id' => $customer['id']]); + + Log::info('Created new Polar customer and stored ID for 1:1 binding', [ 'user_id' => $user->id, 'customer_id' => $customer['id'], + 'external_id' => $customer['external_id'], ]); return $customer; } - protected function findExistingCustomer(User $user): array - { - // Try multiple approaches to find the customer - $attempts = [ - fn () => $this->makeAuthenticatedRequest('GET', '/customers', ['external_id' => (string) $user->id, 'limit' => 100]), - fn () => $this->makeAuthenticatedRequest('GET', '/customers', ['email' => $user->email, 'limit' => 100]), - fn () => $this->makeAuthenticatedRequest('GET', '/customers', ['limit' => 1000]), - ]; - - foreach ($attempts as $attempt) { - try { - $response = $attempt(); - if ($response->successful()) { - $data = $response->json(); - foreach ($data['items'] ?? [] as $customer) { - if (($customer['email'] && strtolower($customer['email']) === strtolower($user->email)) || - ($customer['external_id'] && (string) $customer['external_id'] === (string) $user->id)) { - Log::info('Found existing Polar customer', [ - 'user_id' => $user->id, - 'customer_id' => $customer['id'], - ]); - - return $customer; - } - } - } - } catch (\Exception $e) { - Log::warning('Customer lookup attempt failed', [ - 'user_id' => $user->id, - 'error' => $e->getMessage(), - ]); - } - } - - throw new \Exception('Customer exists in Polar but could not be retrieved after multiple lookup attempts'); - } - protected function getOrCreatePrice(Plan $plan): string { // Look for existing product by plan metadata diff --git a/database/migrations/2025_12_04_204710_add_polar_cust_id_to_users_table.php b/database/migrations/2025_12_04_204710_add_polar_cust_id_to_users_table.php new file mode 100644 index 0000000..330ddfe --- /dev/null +++ b/database/migrations/2025_12_04_204710_add_polar_cust_id_to_users_table.php @@ -0,0 +1,29 @@ +string('polar_cust_id')->nullable()->after('id'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('users', function (Blueprint $table) { + $table->dropColumn('polar_cust_id'); + }); + } +};