Add monetization spine + Build Your Own Case (Min Sak)

- Stripe: StripeClient.php, checkout/portal/webhook endpoints, idempotent event handling
- FreeTier: tier-aware credits (free/light/pro/pro_plus), bonus_balance, hourly caps per tier
- pricing.php + billing.php: 4-tier cards, 3 topups, Customer Portal, balance breakdown
- Min Sak: CaseStore.php, AzureDocIntelligence.php, AzureSearchAdmin.php — per-user hybrid RAG
- api/case/: upload, list, delete, ingest-callback (HMAC-auth'd from n8n)
- award-survey-credits: inter-site HMAC endpoint for dobetternorge.no survey bonus
- dashboard.php: tier badge, balance breakdown card, Min Sak CTA, survey CTA
- KorrespondAgent + all 3 other agents: use_my_case toggle wired to dbnToolsCaseContext()
- bootstrap.php: dbnToolsCaseContext(), dbnToolsIntersiteSecret(), dbnToolsCurrentTier()

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-20 20:52:54 +02:00
parent ed5489d174
commit ba9cddf9a1
30 changed files with 2804 additions and 133 deletions
+308
View File
@@ -0,0 +1,308 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/includes/bootstrap.php';
require_once __DIR__ . '/includes/FreeTier.php';
$uiLang = dbnToolsCurrentLanguage();
$isAuthed = dbnToolsIsAuthenticated();
$currentTier = $isAuthed ? dbnToolsCurrentTier() : 'free';
$surveyDone = false;
if ($isAuthed && dbnToolsIsFreeTier()) {
$surveyDone = FreeTier::hasCompletedSurvey((int)$_SESSION['dbn_tools_sso_uid']);
}
$status = (string)($_GET['status'] ?? '');
$loginUrl = 'https://dobetternorge.no/tools-login.php?return=' . urlencode('/pricing.php');
$surveyUrl = 'https://dobetternorge.no/survey.php';
$tiers = [
[
'sku' => 'free',
'name' => 'Gratis',
'price' => '€0',
'period' => 'alltid',
'credits' => '30 kreditter / måned',
'storage' => 'Ingen sak-lagring',
'seats' => '1 plass',
'cap' => '10 verktøy/time',
'features' => [
'Tilgang til alle 13 verktøy',
'Spørsmål, søk, redaksjon',
'Korrespondanse-utkast',
],
'cta' => $isAuthed ? null : 'Logg inn for å starte',
'highlight' => false,
],
[
'sku' => 'light',
'name' => 'Light',
'price' => '€9',
'period' => '/ måned',
'credits' => '120 kreditter / måned',
'storage' => '100 MB sak-lagring',
'seats' => '1 plass',
'cap' => '15 verktøy/time',
'features' => [
'Alt i Gratis',
'Bygg din egen sak (Min Sak)',
'Privat dokument-RAG i alle verktøy',
'OCR på opplastede PDF-er',
],
'highlight' => false,
],
[
'sku' => 'pro',
'name' => 'Pro',
'price' => '€29',
'period' => '/ måned',
'credits' => '500 kreditter / måned',
'storage' => '1 GB sak-lagring',
'seats' => '1 plass',
'cap' => '30 verktøy/time',
'features' => [
'Alt i Light',
'Hybrid søk (BM25 + vektor) i din sak',
'Prioritert kø ved opplasting',
'Tidslinje-rapport på saken din',
],
'highlight' => true,
'badge' => 'Mest populær',
],
[
'sku' => 'pro_plus',
'name' => 'Pro+ Familie',
'price' => '€79',
'period' => '/ måned',
'credits' => 'Ubegrenset',
'storage' => '10 GB sak-lagring',
'seats' => '3 plasser (familie)',
'cap' => '50 verktøy/time per plass',
'features' => [
'Alt i Pro',
'Inviter 2 familiemedlemmer eller advokat',
'Delt sak-arkiv med revisjonslogg',
'Ubegrensede saksrapporter',
],
'highlight' => false,
'badge' => 'For familier',
],
];
$topups = [
['sku' => 'topup_s', 'price' => '€5', 'credits' => 30, 'note' => 'Impulskjøp'],
['sku' => 'topup_m', 'price' => '€15', 'credits' => 100, 'note' => 'Beste verdi'],
['sku' => 'topup_l', 'price' => '€40', 'credits' => 300, 'note' => 'Tunge brukere'],
];
?>
<!doctype html>
<html lang="<?= htmlspecialchars($uiLang) ?>">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Priser — Do Better Norge verktøy</title>
<meta name="description" content="Priser for tools.dobetternorge.no: gratis tier, abonnementer og kreditt-topp-opp. Bygg din egen sak med privat RAG.">
<link rel="canonical" href="https://tools.dobetternorge.no/pricing.php">
<meta name="theme-color" content="#00205B">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Crimson+Pro:wght@400;600;700&family=IBM+Plex+Sans:wght@400;500;600;700&display=swap">
<link rel="stylesheet" href="assets/css/tools.css">
<style>
.pricing-shell { max-width: 1200px; margin: 0 auto; padding: 2rem 1.5rem 4rem; }
.pricing-hero { text-align: center; margin-bottom: 3rem; }
.pricing-hero h1 { font-family: 'Crimson Pro', serif; font-size: 2.5rem; margin: 0 0 0.75rem; }
.pricing-hero p { color: #4b5563; font-size: 1.1rem; max-width: 640px; margin: 0 auto; }
.pricing-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 1.25rem; margin-bottom: 3rem; }
.pricing-card { background: #fff; border: 1px solid #e5e7eb; border-radius: 12px; padding: 1.75rem 1.5rem; display: flex; flex-direction: column; position: relative; }
.pricing-card.is-highlight { border-color: #00205B; border-width: 2px; box-shadow: 0 8px 24px rgba(0,32,91,0.08); }
.pricing-card .pricing-badge { position: absolute; top: -10px; right: 14px; background: #00205B; color: #fff; padding: 4px 10px; font-size: 0.72rem; border-radius: 999px; letter-spacing: 0.04em; text-transform: uppercase; font-weight: 600; }
.pricing-card h2 { margin: 0 0 0.25rem; font-size: 1.4rem; font-family: 'Crimson Pro', serif; }
.pricing-price { display: flex; align-items: baseline; gap: 0.4rem; margin: 0.5rem 0 1rem; }
.pricing-price .amount { font-size: 2.2rem; font-weight: 700; color: #00205B; }
.pricing-price .period { color: #6b7280; font-size: 0.95rem; }
.pricing-meta { margin: 0 0 1.25rem; padding: 0; list-style: none; font-size: 0.92rem; color: #374151; }
.pricing-meta li { padding: 6px 0; border-bottom: 1px dashed #f3f4f6; }
.pricing-meta li:last-child { border-bottom: none; }
.pricing-features { list-style: none; padding: 0; margin: 0 0 1.5rem; flex: 1; }
.pricing-features li { padding: 5px 0 5px 1.4rem; position: relative; font-size: 0.92rem; color: #1f2937; }
.pricing-features li::before { content: "✓"; position: absolute; left: 0; color: #059669; font-weight: 700; }
.pricing-cta { display: block; text-align: center; padding: 0.75rem 1rem; border-radius: 8px; font-weight: 600; text-decoration: none; transition: all 0.15s; cursor: pointer; border: none; font-size: 0.95rem; }
.pricing-cta.primary { background: #00205B; color: #fff; }
.pricing-cta.primary:hover { background: #001740; }
.pricing-cta.secondary { background: #f3f4f6; color: #1f2937; }
.pricing-cta.secondary:hover { background: #e5e7eb; }
.pricing-cta.current { background: #d1fae5; color: #065f46; cursor: default; }
.pricing-topups { margin-top: 2rem; padding: 2rem 1.5rem; background: #f9fafb; border-radius: 12px; }
.pricing-topups h2 { font-family: 'Crimson Pro', serif; margin: 0 0 0.5rem; font-size: 1.6rem; }
.pricing-topups p.lead { color: #6b7280; margin: 0 0 1.5rem; }
.topup-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 1rem; }
.topup-card { background: #fff; border: 1px solid #e5e7eb; border-radius: 10px; padding: 1.25rem; text-align: center; }
.topup-card .price { font-size: 1.6rem; font-weight: 700; color: #00205B; }
.topup-card .credits { color: #374151; font-size: 0.95rem; margin: 0.25rem 0 0.5rem; }
.topup-card .note { color: #6b7280; font-size: 0.82rem; margin-bottom: 0.75rem; }
.survey-banner { background: linear-gradient(135deg, #00205B, #003478); color: #fff; padding: 1.75rem 1.5rem; border-radius: 12px; margin-bottom: 2rem; display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 1rem; }
.survey-banner .copy { flex: 1; min-width: 260px; }
.survey-banner h3 { margin: 0 0 0.35rem; font-size: 1.3rem; font-family: 'Crimson Pro', serif; }
.survey-banner p { margin: 0; opacity: 0.9; font-size: 0.95rem; }
.survey-banner a { background: #ffd166; color: #00205B; padding: 0.7rem 1.4rem; border-radius: 8px; font-weight: 700; text-decoration: none; white-space: nowrap; }
.pricing-faq { margin-top: 3rem; }
.pricing-faq details { background: #fff; border: 1px solid #e5e7eb; border-radius: 8px; padding: 1rem 1.25rem; margin-bottom: 0.6rem; }
.pricing-faq summary { font-weight: 600; cursor: pointer; }
.pricing-faq p { color: #4b5563; margin: 0.75rem 0 0; font-size: 0.92rem; }
.status-pill-info { display: inline-block; margin-bottom: 1.5rem; padding: 6px 12px; background: #fef3c7; color: #92400e; border-radius: 6px; font-size: 0.9rem; }
.status-pill-success { background: #d1fae5; color: #065f46; }
.status-pill-error { background: #fee2e2; color: #991b1b; }
</style>
</head>
<body>
<main class="pricing-shell">
<header class="pricing-hero">
<p style="margin:0 0 0.5rem; text-transform:uppercase; letter-spacing:0.08em; color:#6b7280; font-size:0.85rem;">Do Better Norge — verktøy</p>
<h1>Bygg din egen sak. Bruk hele verktøyboksen.</h1>
<p>13 AI-verktøy for barnevernssaker. Last opp dine egne dokumenter, og la verktøyene jobbe på din private sak — ikke bare generisk lov.</p>
</header>
<?php if ($status === 'success'): ?>
<p class="status-pill-info status-pill-success">Takk! Din betaling er bekreftet. Det kan ta noen sekunder før kontoen oppdateres.</p>
<?php elseif ($status === 'canceled'): ?>
<p class="status-pill-info">Kassen ble avbrutt. Du kan prøve igjen når som helst.</p>
<?php endif; ?>
<?php if ($isAuthed && !$surveyDone): ?>
<div class="survey-banner">
<div class="copy">
<h3>Tjen 25 ekstra kreditter</h3>
<p>Svar på 5 korte spørsmål om hva som hjelper deg mest. Ingen salgspitch — bare research som hjelper oss å forbedre verktøyene.</p>
</div>
<a href="<?= htmlspecialchars($surveyUrl) ?>">Ta undersøkelsen</a>
</div>
<?php endif; ?>
<section class="pricing-grid" aria-label="Abonnementer">
<?php foreach ($tiers as $tier): ?>
<article class="pricing-card<?= !empty($tier['highlight']) ? ' is-highlight' : '' ?>">
<?php if (!empty($tier['badge'])): ?>
<span class="pricing-badge"><?= htmlspecialchars($tier['badge']) ?></span>
<?php endif; ?>
<h2><?= htmlspecialchars($tier['name']) ?></h2>
<div class="pricing-price">
<span class="amount"><?= htmlspecialchars($tier['price']) ?></span>
<span class="period"><?= htmlspecialchars($tier['period']) ?></span>
</div>
<ul class="pricing-meta">
<li><?= htmlspecialchars($tier['credits']) ?></li>
<li><?= htmlspecialchars($tier['storage']) ?></li>
<li><?= htmlspecialchars($tier['seats']) ?></li>
<li><?= htmlspecialchars($tier['cap']) ?></li>
</ul>
<ul class="pricing-features">
<?php foreach ($tier['features'] as $feature): ?>
<li><?= htmlspecialchars($feature) ?></li>
<?php endforeach; ?>
</ul>
<?php if ($tier['sku'] === 'free'): ?>
<?php if (!$isAuthed): ?>
<a class="pricing-cta primary" href="<?= htmlspecialchars($loginUrl) ?>"><?= htmlspecialchars($tier['cta'] ?? 'Logg inn') ?></a>
<?php elseif ($currentTier === 'free'): ?>
<span class="pricing-cta current">Din nåværende plan</span>
<?php else: ?>
<span class="pricing-cta secondary">Tilgjengelig</span>
<?php endif; ?>
<?php else: ?>
<?php if (!$isAuthed): ?>
<a class="pricing-cta primary" href="<?= htmlspecialchars($loginUrl) ?>">Logg inn for å abonnere</a>
<?php elseif ($currentTier === $tier['sku']): ?>
<span class="pricing-cta current">Din nåværende plan</span>
<?php else: ?>
<button type="button" class="pricing-cta primary" data-sku="<?= htmlspecialchars($tier['sku']) ?>" data-checkout="subscription">
Velg <?= htmlspecialchars($tier['name']) ?>
</button>
<?php endif; ?>
<?php endif; ?>
</article>
<?php endforeach; ?>
</section>
<section class="pricing-topups" aria-label="Engangskjøp">
<h2>Topp opp kreditter</h2>
<p class="lead">Trenger du flere kreditter denne måneden? Kjøp en engangspakke — de utløper aldri.</p>
<div class="topup-grid">
<?php foreach ($topups as $topup): ?>
<div class="topup-card">
<div class="price"><?= htmlspecialchars($topup['price']) ?></div>
<div class="credits"><?= (int)$topup['credits'] ?> kreditter</div>
<div class="note"><?= htmlspecialchars($topup['note']) ?></div>
<?php if ($isAuthed): ?>
<button type="button" class="pricing-cta primary" data-sku="<?= htmlspecialchars($topup['sku']) ?>" data-checkout="topup">Kjøp</button>
<?php else: ?>
<a class="pricing-cta primary" href="<?= htmlspecialchars($loginUrl) ?>">Logg inn først</a>
<?php endif; ?>
</div>
<?php endforeach; ?>
</div>
</section>
<section class="pricing-faq" aria-label="Ofte stilte spørsmål">
<h2 style="font-family:'Crimson Pro', serif; margin-bottom:1rem;">Ofte stilte spørsmål</h2>
<details>
<summary>Hva er forskjellen mellom månedlige kreditter og bonuskreditter?</summary>
<p>Månedlige kreditter (fra abonnement eller gratis tier) tilbakestilles første hver måned. Bonuskreditter (fra undersøkelsen eller topp-opp) utløper aldri og brukes etter de månedlige er oppbrukt.</p>
</details>
<details>
<summary>Hva er Min Sak?</summary>
<p>Min Sak er din private dokumentbank. Last opp PDF-er fra saken din, så blir de OCR-ert, analysert og lagret i din egen sikre korpus. Alle verktøyene kan deretter referere til dine egne dokumenter i stedet for bare generisk lov.</p>
</details>
<details>
<summary>Hvor er dataene mine lagret?</summary>
<p>Alt innenfor EU: servere i Falkenstein (Tyskland) og Helsinki (Finland), AI-tjenester i Vest-Europa og Norge Øst. Vi er hostet hos Hetzner og bruker Microsoft Azure for AI. Stripe behandler betalinger gjennom Irland.</p>
</details>
<details>
<summary>Kan jeg dele en konto med advokaten min?</summary>
<p>Ja — Pro+ Familie inkluderer 3 plasser. Du kan invitere advokat, samboer eller en annen familiemedlem. Alle ser de samme dokumentene, men hvem som gjorde hva blir logget.</p>
</details>
<details>
<summary>Hva skjer hvis jeg sier opp?</summary>
<p>Du faller tilbake til gratis-tier. Bonuskredittene dine beholdes. Dokumentene i Min Sak oppbevares i 90 dager før de slettes — så du har tid til å eksportere dem eller fornye.</p>
</details>
<details>
<summary>Tilbyr dere refusjon?</summary>
<p>Ja, full refusjon innen 7 dager hvis du ikke er fornøyd. Send oss en e-post.</p>
</details>
</section>
</main>
<script>
(function() {
const buttons = document.querySelectorAll('button[data-checkout]');
buttons.forEach(btn => {
btn.addEventListener('click', async () => {
const sku = btn.getAttribute('data-sku');
btn.disabled = true;
const original = btn.textContent;
btn.textContent = 'Kobler til...';
try {
const res = await fetch('/api/stripe-checkout.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ sku })
});
const data = await res.json();
if (data.ok && data.url) {
window.location.href = data.url;
} else {
btn.textContent = 'Feil — prøv igjen';
alert(data.error?.message || 'Kunne ikke starte kassen.');
}
} catch (e) {
btn.textContent = original;
alert('Nettverksfeil: ' + e.message);
} finally {
setTimeout(() => { btn.disabled = false; btn.textContent = original; }, 1500);
}
});
});
})();
</script>
</body>
</html>