Files
dobetternorge-tools/account.php
T
daveadmin b21bfb2f1d Add NOK pricing catalog, credit ledger, success-based charging, and tier-gated model routing
- PricingCatalog.php: single source of truth for plans (free/plus/pro), top-ups,
  Stripe price env keys, tool costs (0–6 credits), STT variable billing, feature limits
- FreeTier.php: monthly-first credit deduction, ledger (user_tool_credit_ledger),
  STT reservation/settle/release, monthly reset, trial logic
- StripeClient.php: canonical SKUs (plus/pro/topup_100/300/1000), legacy aliases kept
- stripe-checkout.php: subscription vs payment mode, trial gating, catalog metadata
- stripe-webhook.php: idempotent via stripe_events, handles subscription lifecycle +
  invoice.paid renewal + one-time topup credit grants
- All API tools: success-based credit deduction (check before, charge after)
- transcribe.php: file-size heuristic reservation, settle from actual provider duration
- ask.php + LegalTools.php: ToolModels engine resolution — Pro gets gpt-4o
- KorrespondAgent.php + korrespond.php: tier-gated draft deployment —
  Free/Plus gets gpt-4o-mini, Pro gets gpt-4o
- pricing.php: NOK-only, plan cards, top-up packs, Organisation contact card,
  tool cost table, separate monthly/prepaid balance display
- 003_pricing_credit_catalog.sql: ledger and STT reservation tables

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 13:42:27 +02:00

246 lines
11 KiB
PHP

<?php
declare(strict_types=1);
require_once __DIR__ . '/includes/bootstrap.php';
require_once __DIR__ . '/includes/FreeTier.php';
require_once __DIR__ . '/includes/PricingCatalog.php';
if (!dbnToolsIsAuthenticated()) {
header('Location: /?return=' . urlencode($_SERVER['REQUEST_URI'] ?? '/account.php'));
exit;
}
$uiLang = dbnToolsCurrentLanguage();
$authUser = dbnToolsAuthenticatedUser();
$isSso = dbnToolsIsFreeTier();
$email = (string)($authUser['email'] ?? '');
$role = (string)($authUser['role'] ?? '');
// Credits & plan (SSO users only)
$detail = $isSso ? FreeTier::balanceDetail((int)$_SESSION['dbn_tools_sso_uid']) : null;
$tier = $detail ? (string)$detail['tier'] : ($isSso ? 'free' : 'caveau');
$catalogPlans = PricingCatalog::plans();
$tierLabels = [
'free' => [$catalogPlans['free']['name'], '#f3f4f6', '#374151'],
'plus' => [$catalogPlans['plus']['name'], '#ddd6fe', '#5b21b6'],
'pro' => [$catalogPlans['pro']['name'], '#bfdbfe', '#1e40af'],
'caveau' => ['CaveauAI', '#d1fae5', '#065f46'],
];
$tierLabel = $tierLabels[$tier] ?? $tierLabels['free'];
$monthlyAllowance = $detail ? FreeTier::monthlyAllowance($tier) : 0;
$creditsUsed = $detail ? max(0, $monthlyAllowance - (int)$detail['balance']) : 0;
// Team (Caveau sessions only)
$teamMembers = [];
if (!$isSso && !empty($authUser['client_id'])) {
try {
$db = dbnToolsDb();
$stmt = $db->prepare(
"SELECT cu.email, cu.role, cu.created_at
FROM client_users cu
WHERE cu.client_id = ?
ORDER BY FIELD(cu.role,'owner','admin','editor','viewer'), cu.created_at ASC"
);
$stmt->execute([(int)$authUser['client_id']]);
$teamMembers = $stmt->fetchAll(PDO::FETCH_ASSOC);
} catch (Throwable $e) {
// non-fatal
}
}
// Renewal date label
$renewalLabel = '';
if ($detail) {
if (!empty($detail['trial_active']) && !empty($detail['trial_expires_at'])) {
$renewalLabel = date('d M Y', strtotime((string)$detail['trial_expires_at']))
. ' (' . (int)$detail['trial_days_remaining'] . ' ' . dbnToolsT('trial_days_left', $uiLang) . ')';
} elseif (!empty($detail['subscription_period_end'])) {
$renewalLabel = date('d M Y', strtotime((string)$detail['subscription_period_end']));
}
}
?>
<!doctype html>
<html lang="<?= htmlspecialchars($uiLang) ?>">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title><?= htmlspecialchars(dbnToolsT('account_title', $uiLang)) ?> — Do Better Norge</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">
<link rel="stylesheet" href="assets/css/dbn-tools-redesign.css">
</head>
<body data-authenticated="true" class="lt-app">
<script>
window.DBN_TOOLS_AUTHENTICATED = true;
window.DBN_TOOLS_LANG = <?= json_encode($uiLang, JSON_UNESCAPED_UNICODE) ?>;
</script>
<?php include __DIR__ . '/includes/nav.php'; ?>
<div class="account-shell">
<div class="account-hero">
<h1><?= htmlspecialchars(dbnToolsT('account_title', $uiLang)) ?></h1>
<p><?= htmlspecialchars($email) ?></p>
</div>
<?php if ($detail): ?>
<!-- ── Credits & Plan ───────────────────────────────────────── -->
<section class="account-section">
<p class="account-section__title"><?= htmlspecialchars(dbnToolsT('account_credits', $uiLang)) ?></p>
<span class="account-tier-pill" style="background:<?= htmlspecialchars($tierLabel[1]) ?>; color:<?= htmlspecialchars($tierLabel[2]) ?>;">
<?= htmlspecialchars($tierLabel[0]) ?>
</span>
<?php if (!empty($detail['trial_active'])): ?>
<span class="account-tier-pill" style="background:#fef3c7;color:#92400e;margin-left:0.4rem;">
<?= htmlspecialchars(dbnToolsT('trial_active_label', $uiLang)) ?>
</span>
<?php endif; ?>
<p class="account-credits-big">
<?= number_format((int)$detail['balance'] + (int)$detail['bonus_balance'], 0, ',', ' ') ?>
</p>
<p class="account-credits-sub">
<?= (int)$detail['balance'] ?> <?= htmlspecialchars(dbnToolsT('credits_monthly', $uiLang)) ?>
· <?= (int)$detail['bonus_balance'] ?> <?= $uiLang === 'no' ? 'forhåndsbetalte' : 'prepaid' ?>
</p>
<?php if ($renewalLabel): ?>
<div class="account-row">
<span class="account-row__label"><?= htmlspecialchars(dbnToolsT('renewal_date', $uiLang)) ?></span>
<span class="account-row__value"><?= htmlspecialchars($renewalLabel) ?></span>
</div>
<?php endif; ?>
<?php if (in_array($tier, ['free'], true)): ?>
<a href="/pricing.php" class="account-upgrade-cta">Upgrade plan →</a>
<?php endif; ?>
</section>
<?php endif; ?>
<!-- ── Profile ──────────────────────────────────────────────── -->
<section class="account-section">
<p class="account-section__title"><?= htmlspecialchars(dbnToolsT('account_profile', $uiLang)) ?></p>
<div class="account-row">
<span class="account-row__label">Email</span>
<span class="account-row__value"><?= htmlspecialchars($email) ?></span>
</div>
<div class="account-row">
<span class="account-row__label">Login</span>
<span class="account-row__value">
<?php if ($isSso): ?>
<?= htmlspecialchars(dbnToolsT('login_method_sso', $uiLang)) ?>
<?php else: ?>
<?= htmlspecialchars(dbnToolsT('login_method_email', $uiLang)) ?>
<?php endif; ?>
</span>
</div>
<?php if ($role !== '' && $role !== 'sso'): ?>
<div class="account-row">
<span class="account-row__label">Role</span>
<span class="account-row__value"><span class="account-role-pill"><?= htmlspecialchars($role) ?></span></span>
</div>
<?php endif; ?>
</section>
<!-- ── Team ─────────────────────────────────────────────────── -->
<section class="account-section">
<p class="account-section__title"><?= htmlspecialchars(dbnToolsT('account_team', $uiLang)) ?></p>
<?php if ($isSso): ?>
<p style="color:var(--muted,#667085); font-size:0.88rem;"><?= htmlspecialchars(dbnToolsT('team_single_sso', $uiLang)) ?></p>
<?php elseif (empty($teamMembers)): ?>
<p style="color:var(--muted,#667085); font-size:0.88rem;">—</p>
<?php else: ?>
<table class="account-team-table">
<thead>
<tr>
<th>Email</th>
<th>Role</th>
<th>Added</th>
</tr>
</thead>
<tbody>
<?php foreach ($teamMembers as $member): ?>
<tr>
<td><?= htmlspecialchars($member['email']) ?></td>
<td><span class="account-role-pill"><?= htmlspecialchars($member['role']) ?></span></td>
<td><?= htmlspecialchars(date('d M Y', strtotime((string)$member['created_at']))) ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endif; ?>
</section>
<?php if ($isSso && $detail && in_array((string)($detail['tier'] ?? 'free'), ['plus', 'pro'], true)): ?>
<section class="account-section">
<p class="account-section__title">MCP access</p>
<p style="color:var(--muted,#667085); font-size:0.88rem; margin-top:0;">
Connect Claude, Cursor, and other AI tools to all 19 DBN legal preparation tools via MCP.
</p>
<a href="/mcp.php" class="account-upgrade-btn" style="display:inline-block; text-decoration:none; margin-top:0.75rem;">
Set up MCP →
</a>
</section>
<?php endif; ?>
<?php if ($detail): ?>
<!-- ── Usage ────────────────────────────────────────────────── -->
<section class="account-section">
<p class="account-section__title"><?= htmlspecialchars(dbnToolsT('account_usage', $uiLang)) ?></p>
<div class="account-row">
<span class="account-row__label"><?= htmlspecialchars(dbnToolsT('usage_credits_used', $uiLang)) ?></span>
<span class="account-row__value"><?= $creditsUsed ?> / <?= $monthlyAllowance ?></span>
</div>
<?php if ($monthlyAllowance > 0): ?>
<div class="account-usage-bar-wrap">
<div class="account-usage-bar" style="width:<?= min(100, round($creditsUsed / $monthlyAllowance * 100)) ?>%"></div>
</div>
<?php endif; ?>
<?php if (!empty($detail['storage_quota_bytes']) && (int)$detail['storage_quota_bytes'] > 0):
$storagePct = min(100, round((int)$detail['storage_used_bytes'] / (int)$detail['storage_quota_bytes'] * 100));
$usedMb = round((int)$detail['storage_used_bytes'] / 1048576, 1);
$quotaMb = round((int)$detail['storage_quota_bytes'] / 1048576, 0);
?>
<div class="account-row" style="margin-top:0.75rem;">
<span class="account-row__label"><?= htmlspecialchars(dbnToolsT('usage_storage_used', $uiLang)) ?></span>
<span class="account-row__value"><?= $usedMb ?> / <?= $quotaMb ?> MB</span>
</div>
<div class="account-usage-bar-wrap">
<div class="account-usage-bar" style="width:<?= $storagePct ?>%"></div>
</div>
<?php endif; ?>
<p style="margin-top:1rem; color:var(--muted,#667085); font-size:0.82rem; font-style:italic;">
<?= htmlspecialchars(dbnToolsT('usage_log_coming', $uiLang)) ?>
</p>
<?php if (empty($detail['survey_completed_at'])): ?>
<a href="https://dobetternorge.no/survey.php" class="account-survey-cta">
<div>
<strong><?= htmlspecialchars(dbnToolsT('earn_credits_eyebrow', $uiLang)) ?></strong>
<span><?= htmlspecialchars(dbnToolsT('survey_cta_text', $uiLang)) ?></span>
</div>
<span style="font-size:1.2rem;">→</span>
</a>
<?php endif; ?>
</section>
<?php endif; ?>
</div>
<?php require_once __DIR__ . '/includes/footer.php'; ?>
</body>
</html>