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:
@@ -0,0 +1,104 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Inter-site endpoint hit by dobetternorge.no after a successful survey submit.
|
||||
* Awards +25 bonus credits to the matching user_tool_credits row.
|
||||
*
|
||||
* Auth: HMAC-SHA256 over (timestamp + "." + payload_json) using INTERSITE_HMAC_SECRET.
|
||||
* Headers required:
|
||||
* X-Intersite-Timestamp: unix seconds (must be within ±300s of server time)
|
||||
* X-Intersite-Signature: hex hmac-sha256
|
||||
*
|
||||
* Body: { "email": "...", "user_id": int|null, "credits": 25, "source": "survey" }
|
||||
*
|
||||
* Idempotency: refuses to award twice for the same user_id (uses survey_completed_at).
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/../includes/bootstrap.php';
|
||||
require_once __DIR__ . '/../includes/FreeTier.php';
|
||||
|
||||
dbnToolsRequireMethod('POST');
|
||||
|
||||
$secret = dbnToolsIntersiteSecret();
|
||||
if ($secret === '') {
|
||||
dbnToolsError('Intersite secret not configured on this server.', 500, 'intersite_misconfigured');
|
||||
}
|
||||
|
||||
$ts = (int)($_SERVER['HTTP_X_INTERSITE_TIMESTAMP'] ?? 0);
|
||||
$sig = (string)($_SERVER['HTTP_X_INTERSITE_SIGNATURE'] ?? '');
|
||||
if ($ts === 0 || $sig === '') {
|
||||
dbnToolsError('Missing intersite auth headers.', 401, 'missing_signature');
|
||||
}
|
||||
if (abs(time() - $ts) > 300) {
|
||||
dbnToolsError('Stale intersite timestamp.', 401, 'stale_timestamp');
|
||||
}
|
||||
|
||||
$raw = file_get_contents('php://input');
|
||||
if ($raw === false || $raw === '') {
|
||||
dbnToolsError('Empty body.', 400, 'empty_body');
|
||||
}
|
||||
|
||||
$expected = hash_hmac('sha256', $ts . '.' . $raw, $secret);
|
||||
if (!hash_equals($expected, $sig)) {
|
||||
dbnToolsError('Bad intersite signature.', 401, 'bad_signature');
|
||||
}
|
||||
|
||||
$data = json_decode($raw, true);
|
||||
if (!is_array($data)) {
|
||||
dbnToolsError('Invalid JSON body.', 400, 'invalid_json');
|
||||
}
|
||||
|
||||
$email = strtolower(trim((string)($data['email'] ?? '')));
|
||||
$userIdHint = isset($data['user_id']) ? (int)$data['user_id'] : 0;
|
||||
$credits = (int)($data['credits'] ?? 25);
|
||||
$source = (string)($data['source'] ?? 'survey');
|
||||
|
||||
if ($credits < 1 || $credits > 100) {
|
||||
dbnToolsError('Credits out of range.', 400, 'bad_credits');
|
||||
}
|
||||
if ($email === '' && $userIdHint <= 0) {
|
||||
dbnToolsError('Need email or user_id.', 400, 'missing_user');
|
||||
}
|
||||
|
||||
$db = dbnmDb();
|
||||
|
||||
// Resolve user_id from hint or email
|
||||
$userId = $userIdHint;
|
||||
if ($userId <= 0 && $email !== '') {
|
||||
$stmt = $db->prepare('SELECT id FROM users WHERE LOWER(email) = ? LIMIT 1');
|
||||
$stmt->execute([$email]);
|
||||
$userId = (int)($stmt->fetchColumn() ?: 0);
|
||||
}
|
||||
|
||||
if ($userId <= 0) {
|
||||
// No account yet — that's fine, dobetternorge.no will magic-link them; we'll award on next login.
|
||||
dbnToolsRespond([
|
||||
'ok' => true,
|
||||
'awarded' => false,
|
||||
'pending' => true,
|
||||
'message' => 'User not yet registered — will award on first login.',
|
||||
]);
|
||||
}
|
||||
|
||||
// Idempotency: don't double-award
|
||||
if (FreeTier::hasCompletedSurvey($userId)) {
|
||||
dbnToolsRespond([
|
||||
'ok' => true,
|
||||
'awarded' => false,
|
||||
'reason' => 'already_completed',
|
||||
'balance' => FreeTier::balance($userId),
|
||||
]);
|
||||
}
|
||||
|
||||
FreeTier::ensureRow($userId);
|
||||
$newBalance = FreeTier::awardBonus($userId, $credits, $source);
|
||||
FreeTier::markSurveyCompleted($userId);
|
||||
|
||||
dbnToolsRespond([
|
||||
'ok' => true,
|
||||
'awarded' => true,
|
||||
'credits' => $credits,
|
||||
'balance' => $newBalance,
|
||||
'user_id' => $userId,
|
||||
]);
|
||||
Reference in New Issue
Block a user