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>
This commit is contained in:
@@ -332,6 +332,61 @@ function dbnToolsWithTelemetry(string $tool, string $language, callable $handler
|
||||
}
|
||||
}
|
||||
|
||||
function dbnToolsWithChargedTelemetry(string $tool, string $language, int $creditUserId, callable $handler, ?int $credits = null, array $creditMetadata = []): void
|
||||
{
|
||||
$start = microtime(true);
|
||||
|
||||
try {
|
||||
$payload = $handler();
|
||||
if ($creditUserId > 0) {
|
||||
$balance = $credits === null
|
||||
? dbnToolsFreeTierDeduct($creditUserId, $tool)
|
||||
: dbnToolsFreeTierDeductAmount($creditUserId, $tool, $credits, $creditMetadata);
|
||||
if ($balance >= 0 && !headers_sent()) {
|
||||
header('X-Credits-Remaining: ' . $balance);
|
||||
}
|
||||
$payload['balance'] = $balance;
|
||||
}
|
||||
|
||||
$latency = (int)round((microtime(true) - $start) * 1000);
|
||||
$payload['ok'] = $payload['ok'] ?? true;
|
||||
$payload['latency_ms'] = $latency;
|
||||
|
||||
dbnToolsLogMetadata([
|
||||
'tool' => $tool,
|
||||
'language' => $language,
|
||||
'ok' => true,
|
||||
'latency_ms' => $latency,
|
||||
'chunk_count' => (int)($payload['trace_metadata']['chunk_count'] ?? 0),
|
||||
'source_count' => (int)($payload['trace_metadata']['source_count'] ?? 0),
|
||||
'deployment' => $payload['trace_metadata']['deployment'] ?? null,
|
||||
]);
|
||||
|
||||
dbnToolsRespond($payload);
|
||||
} catch (DbnToolsHttpException $e) {
|
||||
$latency = (int)round((microtime(true) - $start) * 1000);
|
||||
dbnToolsLogMetadata([
|
||||
'tool' => $tool,
|
||||
'language' => $language,
|
||||
'ok' => false,
|
||||
'latency_ms' => $latency,
|
||||
'error_code' => $e->errorCode,
|
||||
]);
|
||||
dbnToolsError($e->getMessage(), $e->status, $e->errorCode, $e->extra);
|
||||
} catch (Throwable $e) {
|
||||
$latency = (int)round((microtime(true) - $start) * 1000);
|
||||
dbnToolsLogMetadata([
|
||||
'tool' => $tool,
|
||||
'language' => $language,
|
||||
'ok' => false,
|
||||
'latency_ms' => $latency,
|
||||
'error_code' => 'internal_error',
|
||||
]);
|
||||
error_log('DBN tools error: ' . $e->getMessage());
|
||||
dbnToolsError('The tool could not complete this request.', 500, 'internal_error');
|
||||
}
|
||||
}
|
||||
|
||||
function dbnToolsAiPortalRoot(): string
|
||||
{
|
||||
$root = dbnToolsEnv('DBN_AI_PORTAL_ROOT');
|
||||
@@ -516,6 +571,40 @@ function dbnToolsFreeTierCheck(string $tool): int
|
||||
return $uid;
|
||||
}
|
||||
|
||||
function dbnToolsFreeTierCheckAmount(string $tool, int $credits): int
|
||||
{
|
||||
if (!dbnToolsIsFreeTier()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
require_once __DIR__ . '/FreeTier.php';
|
||||
$uid = (int)$_SESSION['dbn_tools_sso_uid'];
|
||||
$result = FreeTier::checkAmount($uid, $tool, max(0, $credits));
|
||||
|
||||
if (!$result['ok']) {
|
||||
$isRateLimit = ($result['reason'] ?? '') === 'rate_limit';
|
||||
$tier = (string)($result['tier'] ?? 'free');
|
||||
$cap = FreeTier::hourlyCap($tier);
|
||||
http_response_code($isRateLimit ? 429 : 402);
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
header('Cache-Control: no-store');
|
||||
echo json_encode([
|
||||
'ok' => false,
|
||||
'error' => ['code' => $result['reason'], 'message' => $isRateLimit
|
||||
? "Rate limit reached - your tier ({$tier}) allows {$cap} requests per hour."
|
||||
: 'No credits remaining. See /pricing.php to top up or upgrade.',
|
||||
],
|
||||
'balance' => $result['balance'],
|
||||
'bonus_balance' => $result['bonus_balance'] ?? 0,
|
||||
'tier' => $tier,
|
||||
'cost' => $result['cost'] ?? $credits,
|
||||
], JSON_UNESCAPED_UNICODE);
|
||||
exit;
|
||||
}
|
||||
|
||||
return $uid;
|
||||
}
|
||||
|
||||
/** Return the current SSO user's tier, or 'free' if not SSO / no row. */
|
||||
function dbnToolsCurrentTier(): string
|
||||
{
|
||||
@@ -619,6 +708,15 @@ function dbnToolsFreeTierDeduct(int $uid, string $tool): int
|
||||
return FreeTier::deduct($uid, $tool);
|
||||
}
|
||||
|
||||
function dbnToolsFreeTierDeductAmount(int $uid, string $tool, int $credits, array $metadata = []): int
|
||||
{
|
||||
if ($uid === 0) {
|
||||
return -1;
|
||||
}
|
||||
require_once __DIR__ . '/FreeTier.php';
|
||||
return FreeTier::deductAmount($uid, $tool, $credits, $metadata);
|
||||
}
|
||||
|
||||
function dbnToolsClientSlug(): string
|
||||
{
|
||||
return dbnToolsEnv('DBN_CAVEAU_CLIENT_SLUG') ?: 'dobetter';
|
||||
|
||||
Reference in New Issue
Block a user