Files
daily-meal-api/app/Services/RevenueCatService.php
T
leonm 29e85589b0
CI / 🧪 Tests Laravel (push) Successful in 2m24s
CI / 🐳 Build & Push Image (push) Successful in 1m13s
feat: revenue cat
2026-08-12 16:00:11 +02:00

152 lines
4.7 KiB
PHP

<?php
namespace App\Services;
use App\Models\User;
use Carbon\CarbonImmutable;
use Illuminate\Http\Client\PendingRequest;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Http;
use RuntimeException;
class RevenueCatService
{
public function sync(User $user): User
{
$response = $this->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<string, mixed> $subscriber
* @return array<string, mixed>
*/
private function entitlement(array $subscriber, string $identifier): array
{
$entitlement = Arr::get($subscriber, "entitlements.{$identifier}", []);
return is_array($entitlement) ? $entitlement : [];
}
/**
* @param array<string, mixed> $entitlement
*/
private function entitlementIsActive(array $entitlement): bool
{
if ($entitlement === []) {
return false;
}
$expiration = $this->effectiveExpiration($entitlement);
return $expiration === null || $expiration->isFuture();
}
/**
* @param array<string, mixed> $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;
}
}