client() ->get('subscribers/'.rawurlencode((string) $user->getKey())) ->throw() ->json(); $subscriber = Arr::get($response, 'subscriber', []); if (! is_array($subscriber)) { throw new RuntimeException('RevenueCat returned an invalid subscriber payload.'); } $subscription = $this->entitlement( $subscriber, (string) config('billing.revenuecat.subscription_entitlement'), ); $certification = $this->entitlement( $subscriber, (string) config('billing.revenuecat.certification_entitlement'), ); $subscriptionProductId = $this->stringValue($subscription['product_identifier'] ?? null); $subscriptionDetails = $subscriptionProductId ? Arr::get($subscriber, "subscriptions.{$subscriptionProductId}", []) : []; $user->forceFill([ 'subscription_product_id' => $this->entitlementIsActive($subscription) ? $subscriptionProductId : null, 'subscription_store' => $this->entitlementIsActive($subscription) ? $this->stringValue(is_array($subscriptionDetails) ? ($subscriptionDetails['store'] ?? null) : null) : null, 'subscription_expires_at' => $this->entitlementIsActive($subscription) ? $this->effectiveExpiration($subscription) : null, 'subscription_is_trial' => $this->entitlementIsActive($subscription) && is_array($subscriptionDetails) && strtoupper((string) ($subscriptionDetails['period_type'] ?? '')) === 'TRIAL', 'certification_purchased_at' => $this->entitlementIsActive($certification) ? $this->dateValue($certification['purchase_date'] ?? null) ?? now() : null, ])->save(); return $user->fresh(); } public function deleteCustomer(User $user): void { if (trim((string) config('billing.revenuecat.secret_key')) === '') { return; } $response = $this->client() ->delete('subscribers/'.rawurlencode((string) $user->getKey())); if (! $response->notFound()) { $response->throw(); } } private function client(): PendingRequest { $secretKey = trim((string) config('billing.revenuecat.secret_key')); if ($secretKey === '') { throw new RuntimeException('REVENUECAT_SECRET_KEY is not configured.'); } return Http::baseUrl(rtrim((string) config('billing.revenuecat.api_url'), '/')) ->withToken($secretKey) ->acceptJson() ->connectTimeout(5) ->timeout(12) ->retry([200, 500], throw: false); } /** * @param array $subscriber * @return array */ private function entitlement(array $subscriber, string $identifier): array { $entitlement = Arr::get($subscriber, "entitlements.{$identifier}", []); return is_array($entitlement) ? $entitlement : []; } /** * @param array $entitlement */ private function entitlementIsActive(array $entitlement): bool { if ($entitlement === []) { return false; } $expiration = $this->effectiveExpiration($entitlement); return $expiration === null || $expiration->isFuture(); } /** * @param array $entitlement */ private function effectiveExpiration(array $entitlement): ?CarbonImmutable { $expiration = $this->dateValue($entitlement['expires_date'] ?? null); $gracePeriodExpiration = $this->dateValue($entitlement['grace_period_expires_date'] ?? null); if ($expiration === null) { return $gracePeriodExpiration; } if ($gracePeriodExpiration === null) { return $expiration; } return $gracePeriodExpiration->greaterThan($expiration) ? $gracePeriodExpiration : $expiration; } private function dateValue(mixed $value): ?CarbonImmutable { if (! is_string($value) || $value === '') { return null; } return CarbonImmutable::parse($value); } private function stringValue(mixed $value): ?string { return is_string($value) && $value !== '' ? $value : null; } }