Files
dobetternorge-tools/includes/PricingCatalog.php
T
daveadmin 56cd87dd7b redact: UX overhaul — engine simplification, credits, spinner, save-to-docs, badges
- Remove GPU/regex engine options; keep only azure_mini (1 credit) and azure_full (2 credits)
- Variable credit cost: engine-aware pre-check and charge in api/redact.php; PricingCatalog base = 1
- Fix ATTORNEY not preserved when keepOfficials=true: add to LLM prompt, generic-tag, pseudonym regexes
- Replace Azure credits hint with per-engine credit cost text (all 4 languages)
- Single-file upload only (was: up to 5); simplify status messages
- Clear previous redaction output and show pulsing spinner when a new run starts
- Add "Save to My Docs" button in redact output panel (corpus-save.js path)
- corpus-save.js: capture source_doc_ids from button dataset, pass in POST payload
- api/save-to-corpus.php: accept source_doc_ids, store first as source_url=corpus-doc:{id}
- doc-picker.js: show "✂ Redacted" badge for documents saved from the redact tool
- CSS: .redact-working spinner, doc-item__badge--redact pill styles

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 08:18:51 +02:00

298 lines
9.2 KiB
PHP

<?php
declare(strict_types=1);
/**
* Authoritative pricing, entitlement, and credit catalog for DBN Tools.
*
* Current database columns are intentionally preserved:
* - user_tool_credits.balance = monthly credits
* - user_tool_credits.bonus_balance = prepaid/top-up credits
*/
final class PricingCatalog
{
public const VERSION = 'dbn-tools-nok-2026-05-v1';
/** @return array<string,array<string,mixed>> */
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<string,array<string,mixed>> */
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<string,int> */
public static function toolCosts(): array
{
return [
'search' => 0,
'corpus-search' => 0,
'ask' => 1,
'extract' => 1,
'summarize' => 1,
'translate' => 1,
'korrespond_refine' => 1,
'timeline' => 2,
'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<string,mixed> */
public static function stt(): array
{
return [
'minimum_credits' => 5,
'credits_per_started_minute' => 1,
'reservation_bytes_per_credit' => 300000,
];
}
/** @return array<string,mixed>|null */
public static function plan(string $sku): ?array
{
$plans = self::plans();
return $plans[$sku] ?? null;
}
/** @return array<string,mixed>|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<string> */
public static function subscriptionSkus(): array
{
return ['plus', 'pro'];
}
/** @return list<string> */
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, ',', ' ');
}
}