Implement academy analytics, billing, and web stories updates

This commit is contained in:
2026-05-26 07:27:29 +02:00
parent 456c3d6bb0
commit 0b33a1b074
177 changed files with 27360 additions and 2685 deletions

View File

@@ -0,0 +1,148 @@
<?php
declare(strict_types=1);
namespace App\Services\Academy;
use Illuminate\Support\Arr;
use Illuminate\Support\Str;
use RuntimeException;
final class AcademyBillingPlanService
{
public function enabled(): bool
{
return (bool) config('academy_billing.enabled', false);
}
public function subscriptionName(): string
{
return (string) config('academy_billing.subscription_name', 'academy');
}
/**
* @return array<string, array<string, mixed>>
*/
public function plans(): array
{
$plans = config('academy_billing.plans', []);
return is_array($plans) ? $plans : [];
}
public function normalizePlanKey(?string $planKey): string
{
return Str::of((string) $planKey)
->trim()
->lower()
->replace('-', '_')
->value();
}
/**
* @return array<string, mixed>|null
*/
public function plan(?string $planKey): ?array
{
$normalized = $this->normalizePlanKey($planKey);
if ($normalized === '') {
return null;
}
$plan = Arr::get($this->plans(), $normalized);
if (! is_array($plan)) {
return null;
}
$plan['key'] = $normalized;
$plan['tier'] = $this->normalizeTier((string) ($plan['tier'] ?? 'free'));
$plan['interval'] = Str::lower(trim((string) ($plan['interval'] ?? 'monthly')));
$plan['amount'] = trim((string) ($plan['amount'] ?? ''));
$plan['currency'] = Str::upper(trim((string) ($plan['currency'] ?? config('cashier.currency', 'EUR'))));
$plan['stripe_price_id'] = trim((string) ($plan['stripe_price_id'] ?? ''));
$plan['configured'] = $plan['stripe_price_id'] !== '';
$plan['price_id_valid'] = $this->isValidPriceId($plan['stripe_price_id']);
$plan['price_display'] = $plan['amount'] !== '' ? $plan['amount'].' '.$plan['currency'] : null;
return $plan;
}
/**
* @return array<string, mixed>|null
*/
public function planForPriceId(?string $priceId): ?array
{
$priceId = trim((string) $priceId);
if ($priceId === '') {
return null;
}
foreach (array_keys($this->plans()) as $planKey) {
$plan = $this->plan((string) $planKey);
if ($plan !== null && ($plan['stripe_price_id'] ?? null) === $priceId) {
return $plan;
}
}
return null;
}
/**
* @return array<int, string>
*/
public function missingPriceIds(?string $planKey = null): array
{
if ($planKey !== null) {
$plan = $this->plan($planKey);
return $plan !== null && ! ($plan['configured'] ?? false)
? [$this->normalizePlanKey($planKey)]
: [];
}
return collect(array_keys($this->plans()))
->filter(fn (string $key): bool => ! ((bool) ($this->plan($key)['configured'] ?? false)))
->values()
->all();
}
public function assertConfigured(?string $planKey = null): void
{
if (app()->environment(['local', 'testing'])) {
return;
}
$missingPlans = $this->missingPriceIds($planKey);
if ($missingPlans === []) {
return;
}
throw new RuntimeException('Academy billing price IDs are missing for: '.implode(', ', $missingPlans));
}
public function normalizeTier(string $tier): string
{
return match (Str::lower(trim($tier))) {
'admin' => 'admin',
'pro' => 'pro',
'creator', 'premium' => 'creator',
default => 'free',
};
}
public function isValidPriceId(?string $priceId): bool
{
$priceId = trim((string) $priceId);
if ($priceId === '') {
return false;
}
return preg_match('/^price_[A-Za-z0-9]+$/', $priceId) === 1;
}
}