diff --git a/packages/core-php/src/Mod/Tenant/Controllers/ReferralController.php b/packages/core-php/src/Mod/Tenant/Controllers/ReferralController.php index 88ecbd7..2382ac1 100644 --- a/packages/core-php/src/Mod/Tenant/Controllers/ReferralController.php +++ b/packages/core-php/src/Mod/Tenant/Controllers/ReferralController.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace Core\Mod\Tenant\Controllers; +use Core\Helpers\PrivacyHelper; use Core\Mod\Trees\Models\TreePlanting; use Core\Mod\Trees\Models\TreePlantingStats; use Illuminate\Http\RedirectResponse; @@ -55,24 +56,32 @@ class ReferralController extends \Core\Front\Controller $provider = strtolower($provider); $model = $model ? strtolower($model) : null; - // Build referral data + // Build referral data for session (includes hashed IP for fraud detection) $referral = [ 'provider' => $provider, 'model' => $model, 'referred_at' => now()->toIso8601String(), - 'ip' => $request->ip(), + 'ip_hash' => PrivacyHelper::hashIp($request->ip()), ]; // Track the referral visit in stats (raw inbound count) TreePlantingStats::incrementReferrals($provider, $model); - // Store in session (primary) + // Store in session (primary) - includes hashed IP $request->session()->put(self::REFERRAL_SESSION, $referral); + // Cookie data - exclude IP for privacy (GDPR compliance) + // Provider/model is sufficient for referral attribution + $cookieData = [ + 'provider' => $provider, + 'model' => $model, + 'referred_at' => $referral['referred_at'], + ]; + // Set 30-day cookie (backup for session expiry) $cookie = Cookie::make( name: self::REFERRAL_COOKIE, - value: json_encode($referral), + value: json_encode($cookieData), minutes: self::COOKIE_LIFETIME, path: '/', domain: config('session.domain'), @@ -90,7 +99,7 @@ class ReferralController extends \Core\Front\Controller /** * Get the agent referral from session or cookie. * - * @return array{provider: string, model: ?string, referred_at: string, ip: string}|null + * @return array{provider: string, model: ?string, referred_at: string, ip_hash?: string}|null */ public static function getReferral(Request $request): ?array { diff --git a/packages/core-php/src/Mod/Trees/Listeners/PlantTreeForAgentReferral.php b/packages/core-php/src/Mod/Trees/Listeners/PlantTreeForAgentReferral.php index 126adb0..01e749e 100644 --- a/packages/core-php/src/Mod/Trees/Listeners/PlantTreeForAgentReferral.php +++ b/packages/core-php/src/Mod/Trees/Listeners/PlantTreeForAgentReferral.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace Core\Mod\Trees\Listeners; +use Core\Helpers\PrivacyHelper; use Core\Mod\Tenant\Controllers\ReferralController; use Core\Mod\Trees\Models\TreePlanting; use Core\Mod\Tenant\Models\AgentReferralBonus; @@ -64,8 +65,8 @@ class PlantTreeForAgentReferral 'status' => $status, 'metadata' => [ 'referred_at' => $referral['referred_at'] ?? now()->toIso8601String(), - 'referral_ip' => $referral['ip'] ?? null, - 'signup_ip' => $request->ip(), + 'referral_ip_hash' => $referral['ip_hash'] ?? null, + 'signup_ip_hash' => PrivacyHelper::hashIp($request->ip()), ], ]); diff --git a/packages/core-php/src/Mod/Trees/Tests/Feature/ReferralRouteTest.php b/packages/core-php/src/Mod/Trees/Tests/Feature/ReferralRouteTest.php index bc216b5..a495ab9 100644 --- a/packages/core-php/src/Mod/Trees/Tests/Feature/ReferralRouteTest.php +++ b/packages/core-php/src/Mod/Trees/Tests/Feature/ReferralRouteTest.php @@ -69,11 +69,13 @@ describe('Referral Route', function () { expect($referral['referred_at'])->not->toBeNull(); }); - it('stores client IP in referral', function () { + it('stores hashed client IP in referral for privacy', function () { $response = $this->get('/ref/anthropic'); $referral = $response->getSession()->get('agent_referral'); - expect($referral['ip'])->not->toBeNull(); + expect($referral['ip_hash'])->not->toBeNull(); + // Verify it's a hash (64 chars for SHA-256) + expect(strlen($referral['ip_hash']))->toBe(64); }); });