> */ public static function plans(): array { return [ 'free' => [ 'sku' => 'free', 'tier' => 'free', 'name' => 'Gratis', 'price_nok' => 0, 'unit_amount' => 0, 'currency' => 'nok', 'monthly_credits' => 30, 'effective_credit_cost' => null, 'storage_bytes' => 0, 'seats' => 1, 'hourly_cap' => 10, 'trial_days' => 0, 'stripe_price_key' => null, 'features' => [ 'Pasted-text tools', 'Norwegian legal corpus search', 'No My Case storage', ], ], 'plus' => [ 'sku' => 'plus', 'tier' => 'plus', 'name' => 'Pluss', 'price_nok' => 149, 'unit_amount' => 14900, 'currency' => 'nok', 'monthly_credits' => 250, 'effective_credit_cost' => 0.60, 'storage_bytes' => 524288000, 'seats' => 1, 'hourly_cap' => 20, 'trial_days' => 14, 'stripe_price_key' => 'STRIPE_PRICE_PLUS_NOK', 'features' => [ '500 MB My Case storage', 'Use private case context in tools', 'Saved analyses', '14-day trial', ], ], 'pro' => [ 'sku' => 'pro', 'tier' => 'pro', 'name' => 'Pro Familie', 'price_nok' => 399, 'unit_amount' => 39900, 'currency' => 'nok', 'monthly_credits' => 900, 'effective_credit_cost' => 0.44, 'storage_bytes' => 5368709120, 'seats' => 3, 'hourly_cap' => 40, 'trial_days' => 0, 'stripe_price_key' => 'STRIPE_PRICE_PRO_NOK', 'features' => [ '5 GB shared My Case storage', '3 users on one case', 'Full Azure model route', 'Priority for complex analysis', ], ], ]; } /** @return array> */ public static function topups(): array { return [ 'topup_100' => [ 'sku' => 'topup_100', 'name' => 'Ekstra 100', 'price_nok' => 129, 'unit_amount' => 12900, 'currency' => 'nok', 'credits' => 100, 'cost_per_credit' => 1.29, 'stripe_price_key' => 'STRIPE_PRICE_TOPUP_100_NOK', 'aliases' => ['topup_s'], ], 'topup_300' => [ 'sku' => 'topup_300', 'name' => 'Saks-pakke 300', 'price_nok' => 299, 'unit_amount' => 29900, 'currency' => 'nok', 'credits' => 300, 'cost_per_credit' => 1.00, 'stripe_price_key' => 'STRIPE_PRICE_TOPUP_300_NOK', 'aliases' => ['topup_m'], ], 'topup_1000' => [ 'sku' => 'topup_1000', 'name' => 'Stor pakke 1000', 'price_nok' => 849, 'unit_amount' => 84900, 'currency' => 'nok', 'credits' => 1000, 'cost_per_credit' => 0.85, 'stripe_price_key' => 'STRIPE_PRICE_TOPUP_1000_NOK', 'aliases' => ['topup_l'], ], ]; } /** @return array */ public static function toolCosts(): array { return [ 'search' => 0, 'corpus-search' => 0, 'ask' => 1, 'extract' => 1, 'summarize' => 1, 'translate' => 1, 'korrespond_refine' => 1, 'timeline' => 1, // nova_lite=1, azure_mini=1; azure_full overrides to 2 in api/timeline.php 'redact' => 1, // minimum (gpt-4o-mini); azure_full overrides to 2 in api/redact.php 'barnevernet' => 3, 'advocate' => 3, 'korrespond' => 3, 'legal-analysis' => 3, 'discrepancy' => 4, 'deep-research' => 6, 'transcribe' => 5, ]; } /** @return array */ public static function stt(): array { return [ 'minimum_credits' => 5, 'credits_per_started_minute' => 1, 'reservation_bytes_per_credit' => 300000, ]; } /** @return array|null */ public static function plan(string $sku): ?array { $plans = self::plans(); return $plans[$sku] ?? null; } /** @return array|null */ public static function topup(string $sku): ?array { $canonical = self::canonicalSku($sku); $topups = self::topups(); return $topups[$canonical] ?? null; } public static function canonicalSku(string $sku): string { $sku = trim($sku); if ($sku === 'light') { return 'plus'; } if ($sku === 'pro-plus') { return 'pro'; } foreach (self::topups() as $canonical => $topup) { if ($sku === $canonical || in_array($sku, $topup['aliases'] ?? [], true)) { return $canonical; } } return $sku; } public static function isSubscriptionSku(string $sku): bool { $sku = self::canonicalSku($sku); return in_array($sku, ['plus', 'pro'], true); } public static function isTopupSku(string $sku): bool { return self::topup($sku) !== null; } /** @return list */ public static function subscriptionSkus(): array { return ['plus', 'pro']; } /** @return list */ public static function topupSkus(): array { return array_keys(self::topups()); } public static function stripePriceKey(string $sku): ?string { $sku = self::canonicalSku($sku); if (self::isSubscriptionSku($sku)) { return self::plans()[$sku]['stripe_price_key'] ?? null; } $topup = self::topup($sku); return $topup['stripe_price_key'] ?? null; } public static function monthlyAllowance(string $tier): int { return (int)(self::plans()[$tier]['monthly_credits'] ?? self::plans()['free']['monthly_credits']); } public static function hourlyCap(string $tier): int { return (int)(self::plans()[$tier]['hourly_cap'] ?? self::plans()['free']['hourly_cap']); } public static function storageQuota(string $tier): int { return (int)(self::plans()[$tier]['storage_bytes'] ?? 0); } public static function isPaidTier(string $tier): bool { return in_array($tier, ['plus', 'pro'], true); } public static function toolCost(string $tool): int { return self::toolCosts()[$tool] ?? 1; } public static function topupCredits(string $sku): int { $topup = self::topup($sku); return $topup ? (int)$topup['credits'] : 0; } public static function planTrialDays(string $sku): int { $sku = self::canonicalSku($sku); return (int)(self::plans()[$sku]['trial_days'] ?? 0); } public static function transcribeCreditsForSeconds(float $seconds): int { $stt = self::stt(); $perMinute = max(1, (int)$stt['credits_per_started_minute']); $minimum = max(1, (int)$stt['minimum_credits']); $minutes = max(1, (int)ceil(max(0.0, $seconds) / 60)); return max($minimum, $minutes * $perMinute); } public static function estimateTranscribeCreditsFromBytes(int $bytes): int { $stt = self::stt(); $bytesPerCredit = max(1, (int)$stt['reservation_bytes_per_credit']); return max((int)$stt['minimum_credits'], (int)ceil(max(1, $bytes) / $bytesPerCredit)); } public static function estimateTranscribeCreditsFromFile(string $filename, int $bytes): int { $stt = self::stt(); $minimum = (int)$stt['minimum_credits']; $ext = strtolower(pathinfo($filename, PATHINFO_EXTENSION)); $bytesPerCredit = match ($ext) { 'wav', 'flac' => 2000000, 'mp3', 'm4a', 'mp4', 'aac', 'ogg', 'oga', 'webm' => (int)$stt['reservation_bytes_per_credit'], default => 960000, }; return max($minimum, (int)ceil(max(1, $bytes) / max(1, $bytesPerCredit))); } public static function formatNok(int $nok): string { return 'NOK ' . number_format($nok, 0, ',', ' '); } public static function formatCredits(int $credits): string { return number_format($credits, 0, ',', ' '); } }