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:
+210
@@ -0,0 +1,210 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/includes/bootstrap.php';
|
||||
require_once __DIR__ . '/includes/FreeTier.php';
|
||||
|
||||
if (!dbnToolsIsAuthenticated()) {
|
||||
header('Location: /?return=' . urlencode('/billing.php'));
|
||||
exit;
|
||||
}
|
||||
|
||||
$uiLang = dbnToolsCurrentLanguage();
|
||||
$isSso = dbnToolsIsFreeTier();
|
||||
$userId = $isSso ? (int)$_SESSION['dbn_tools_sso_uid'] : 0;
|
||||
$email = (string)($_SESSION['dbn_tools_user_email'] ?? '');
|
||||
|
||||
$detail = $userId > 0 ? FreeTier::balanceDetail($userId) : [
|
||||
'balance' => 0, 'bonus_balance' => 0, 'tier' => 'caveau',
|
||||
'storage_used_bytes' => 0, 'storage_quota_bytes' => 0,
|
||||
'survey_completed_at' => null, 'subscription_period_end' => null,
|
||||
];
|
||||
|
||||
$tier = (string)$detail['tier'];
|
||||
$effective = (int)$detail['balance'] + (int)$detail['bonus_balance'];
|
||||
$storageMb = round($detail['storage_used_bytes'] / 1048576, 1);
|
||||
$quotaMb = $detail['storage_quota_bytes'] > 0 ? round($detail['storage_quota_bytes'] / 1048576, 0) : 0;
|
||||
$storagePct = $quotaMb > 0 ? min(100, round(($storageMb / $quotaMb) * 100)) : 0;
|
||||
|
||||
$tierLabels = [
|
||||
'free' => 'Gratis',
|
||||
'light' => 'Light',
|
||||
'pro' => 'Pro',
|
||||
'pro_plus' => 'Pro+ Familie',
|
||||
'caveau' => 'CaveauAI',
|
||||
];
|
||||
|
||||
// Recent usage
|
||||
$db = dbnmDb();
|
||||
$stmt = $db->prepare(
|
||||
'SELECT tool, credits_used, created_at FROM user_tool_usage_log
|
||||
WHERE user_id = ? ORDER BY created_at DESC LIMIT 25'
|
||||
);
|
||||
$stmt->execute([$userId]);
|
||||
$recent = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
$status = (string)($_GET['status'] ?? '');
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="<?= htmlspecialchars($uiLang) ?>">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Konto & fakturering — tools.dobetternorge.no</title>
|
||||
<meta name="robots" content="noindex, nofollow">
|
||||
<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>
|
||||
.billing-shell { max-width: 1000px; margin: 0 auto; padding: 2rem 1.5rem 4rem; }
|
||||
.billing-shell h1 { font-family: 'Crimson Pro', serif; margin: 0 0 0.5rem; }
|
||||
.billing-shell .sub { color: #6b7280; margin-bottom: 2rem; }
|
||||
.billing-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 1.25rem; margin-bottom: 2rem; }
|
||||
@media (max-width: 720px) { .billing-grid { grid-template-columns: 1fr; } }
|
||||
.billing-card { background: #fff; border: 1px solid #e5e7eb; border-radius: 12px; padding: 1.5rem; }
|
||||
.billing-card h2 { margin: 0 0 0.5rem; font-size: 1.1rem; color: #374151; }
|
||||
.tier-badge { display: inline-block; padding: 4px 12px; border-radius: 999px; font-size: 0.85rem; font-weight: 600; }
|
||||
.tier-free { background: #f3f4f6; color: #374151; }
|
||||
.tier-light { background: #ddd6fe; color: #5b21b6; }
|
||||
.tier-pro { background: #bfdbfe; color: #1e40af; }
|
||||
.tier-pro_plus { background: #fde68a; color: #92400e; }
|
||||
.tier-caveau { background: #d1fae5; color: #065f46; }
|
||||
.balance-big { font-size: 2.6rem; font-weight: 700; color: #00205B; margin: 0.5rem 0; }
|
||||
.balance-break { color: #6b7280; font-size: 0.9rem; }
|
||||
.storage-bar { background: #f3f4f6; border-radius: 999px; height: 10px; overflow: hidden; margin: 0.75rem 0; }
|
||||
.storage-bar > div { background: #00205B; height: 100%; transition: width 0.3s; }
|
||||
.billing-actions { display: flex; flex-wrap: wrap; gap: 0.75rem; margin: 2rem 0; }
|
||||
.btn { padding: 0.7rem 1.25rem; border-radius: 8px; border: none; font-weight: 600; cursor: pointer; text-decoration: none; display: inline-block; }
|
||||
.btn-primary { background: #00205B; color: #fff; }
|
||||
.btn-primary:hover { background: #001740; }
|
||||
.btn-secondary { background: #f3f4f6; color: #1f2937; }
|
||||
.btn-secondary:hover { background: #e5e7eb; }
|
||||
.usage-table { width: 100%; border-collapse: collapse; }
|
||||
.usage-table th, .usage-table td { padding: 8px 12px; border-bottom: 1px solid #f3f4f6; text-align: left; font-size: 0.9rem; }
|
||||
.usage-table th { color: #6b7280; font-weight: 500; }
|
||||
.usage-table .negative { color: #059669; font-weight: 600; }
|
||||
.usage-table .positive { color: #b91c1c; }
|
||||
.status-pill { display: inline-block; margin-bottom: 1.5rem; padding: 8px 14px; border-radius: 6px; font-size: 0.9rem; }
|
||||
.status-success { background: #d1fae5; color: #065f46; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main class="billing-shell">
|
||||
<h1>Konto & fakturering</h1>
|
||||
<p class="sub"><?= htmlspecialchars($email) ?> · <a href="/dashboard.php">Tilbake til dashbordet</a></p>
|
||||
|
||||
<?php if ($status === 'success'): ?>
|
||||
<p class="status-pill status-success">Betalingen er bekreftet. Hvis du nettopp abonnerte, kan det ta noen sekunder før kontoen oppdateres.</p>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="billing-grid">
|
||||
<div class="billing-card">
|
||||
<h2>Nåværende plan</h2>
|
||||
<p style="margin:0.5rem 0 1rem;">
|
||||
<span class="tier-badge tier-<?= htmlspecialchars($tier) ?>"><?= htmlspecialchars($tierLabels[$tier] ?? $tier) ?></span>
|
||||
</p>
|
||||
<?php if (!empty($detail['subscription_period_end'])): ?>
|
||||
<p class="balance-break">Fornyes <?= htmlspecialchars(date('j. F Y', strtotime((string)$detail['subscription_period_end']))) ?></p>
|
||||
<?php elseif ($tier === 'free'): ?>
|
||||
<p class="balance-break">Ingen aktiv abonnement. <a href="/pricing.php">Se planer</a></p>
|
||||
<?php endif; ?>
|
||||
<div class="billing-actions" style="margin: 1.25rem 0 0;">
|
||||
<?php if (in_array($tier, ['light','pro','pro_plus'], true)): ?>
|
||||
<button type="button" id="portalBtn" class="btn btn-secondary">Administrer abonnement</button>
|
||||
<?php endif; ?>
|
||||
<a class="btn btn-primary" href="/pricing.php">Se alle planer</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="billing-card">
|
||||
<h2>Tilgjengelige kreditter</h2>
|
||||
<p class="balance-big"><?= number_format($effective, 0, ',', ' ') ?></p>
|
||||
<p class="balance-break">
|
||||
<?= (int)$detail['balance'] ?> månedlige · <?= (int)$detail['bonus_balance'] ?> bonus
|
||||
</p>
|
||||
<?php if ($tier === 'pro_plus'): ?>
|
||||
<p style="margin-top:0.5rem; color:#059669; font-size:0.9rem;">✓ Pro+ har ubegrenset bruk (50 kall/time)</p>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<?php if (in_array($tier, ['light','pro','pro_plus'], true)): ?>
|
||||
<div class="billing-card">
|
||||
<h2>Sak-lagring</h2>
|
||||
<p class="balance-big" style="font-size:1.8rem;"><?= $storageMb ?> MB <span style="color:#6b7280; font-size:1rem;">/ <?= $quotaMb ?> MB</span></p>
|
||||
<div class="storage-bar"><div style="width: <?= $storagePct ?>%;"></div></div>
|
||||
<p class="balance-break"><a href="/min-sak.php">Gå til Min Sak</a></p>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="billing-card">
|
||||
<h2>Min Sak</h2>
|
||||
<p style="color:#6b7280; margin:0 0 1rem;">Last opp dine egne dokumenter — alle verktøyene kan deretter referere til din private sak.</p>
|
||||
<a class="btn btn-primary" href="/pricing.php">Oppgrader for å bygge sak</a>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="billing-card">
|
||||
<h2>Bonus-kreditter (undersøkelse)</h2>
|
||||
<?php if (!empty($detail['survey_completed_at'])): ?>
|
||||
<p style="color:#059669;">✓ Du har allerede mottatt 25 bonuskreditter for å fylle ut undersøkelsen.</p>
|
||||
<?php else: ?>
|
||||
<p style="color:#374151; margin:0 0 1rem;">Svar på 5 korte spørsmål om dine behov og motta 25 bonus-kreditter — utløper aldri.</p>
|
||||
<a class="btn btn-primary" href="https://dobetternorge.no/survey.php">Ta undersøkelsen</a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="billing-card">
|
||||
<h2>Nylig bruk (siste 25)</h2>
|
||||
<?php if (empty($recent)): ?>
|
||||
<p style="color:#6b7280;">Ingen bruk registrert ennå.</p>
|
||||
<?php else: ?>
|
||||
<table class="usage-table">
|
||||
<thead><tr><th>Verktøy / hendelse</th><th>Kreditter</th><th>Tidspunkt</th></tr></thead>
|
||||
<tbody>
|
||||
<?php foreach ($recent as $r): ?>
|
||||
<?php $credits = (int)$r['credits_used']; ?>
|
||||
<tr>
|
||||
<td><?= htmlspecialchars($r['tool']) ?></td>
|
||||
<td class="<?= $credits < 0 ? 'negative' : 'positive' ?>"><?= $credits < 0 ? '+' . abs($credits) : '-' . $credits ?></td>
|
||||
<td><?= htmlspecialchars((string)$r['created_at']) ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<script>
|
||||
(function() {
|
||||
const portalBtn = document.getElementById('portalBtn');
|
||||
if (portalBtn) {
|
||||
portalBtn.addEventListener('click', async () => {
|
||||
portalBtn.disabled = true;
|
||||
const original = portalBtn.textContent;
|
||||
portalBtn.textContent = 'Kobler til...';
|
||||
try {
|
||||
const res = await fetch('/api/stripe-portal.php', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: '{}',
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data.ok && data.url) {
|
||||
window.location.href = data.url;
|
||||
} else {
|
||||
alert(data.error?.message || 'Kunne ikke åpne portalen.');
|
||||
}
|
||||
} catch (e) {
|
||||
alert('Nettverksfeil: ' + e.message);
|
||||
} finally {
|
||||
portalBtn.disabled = false;
|
||||
portalBtn.textContent = original;
|
||||
}
|
||||
});
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user