- Voice enhancements, user management, payment method
This commit is contained in:
+140
@@ -0,0 +1,140 @@
|
||||
<?php
|
||||
|
||||
function stripeRequest(string $method, string $path, array $fields = []): array {
|
||||
$secret = trim((string)getConfigVal('stripe.secret_key', ''));
|
||||
if ($secret === '') throw new RuntimeException('Stripe is not configured');
|
||||
if (!function_exists('curl_init')) throw new RuntimeException('PHP cURL extension is required');
|
||||
$url = 'https://api.stripe.com/v1/' . ltrim($path, '/');
|
||||
if ($method === 'GET' && $fields) $url .= '?' . http_build_query($fields);
|
||||
$curl = curl_init($url);
|
||||
$options = [
|
||||
CURLOPT_USERPWD => $secret . ':',
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_TIMEOUT => 25,
|
||||
CURLOPT_CUSTOMREQUEST => $method,
|
||||
CURLOPT_HTTPHEADER => ['Content-Type: application/x-www-form-urlencoded'],
|
||||
];
|
||||
if ($method !== 'GET' && $fields) $options[CURLOPT_POSTFIELDS] = http_build_query($fields);
|
||||
curl_setopt_array($curl, $options);
|
||||
$body = curl_exec($curl);
|
||||
$status = (int)curl_getinfo($curl, CURLINFO_RESPONSE_CODE);
|
||||
$error = curl_error($curl);
|
||||
curl_close($curl);
|
||||
$decoded = json_decode((string)$body, true);
|
||||
if ($body === false || $status < 200 || $status >= 300) {
|
||||
$message = $decoded['error']['message'] ?? $error ?: ('Stripe HTTP ' . $status);
|
||||
throw new RuntimeException($message);
|
||||
}
|
||||
return is_array($decoded) ? $decoded : [];
|
||||
}
|
||||
|
||||
function stripeReturnUrl(string $configKey, string $fallback): string {
|
||||
$configured = trim((string)getConfigVal($configKey, ''));
|
||||
return $configured !== '' ? $configured : $fallback;
|
||||
}
|
||||
|
||||
function createStripeCheckout(array $user, array $plan): string {
|
||||
if (empty($user['email_verified_at']) || empty($user['email'])) {
|
||||
throw new RuntimeException('A verified email is required before checkout');
|
||||
}
|
||||
if (empty($plan['stripe_price_id'])) throw new RuntimeException('This plan does not have a Stripe price ID');
|
||||
$base = appBaseUrl();
|
||||
$fields = [
|
||||
'mode' => 'subscription',
|
||||
'line_items[0][price]' => $plan['stripe_price_id'],
|
||||
'line_items[0][quantity]' => 1,
|
||||
'success_url' => stripeReturnUrl('stripe.success_url', $base . '/index.html?billing=success'),
|
||||
'cancel_url' => stripeReturnUrl('stripe.cancel_url', $base . '/index.html?billing=cancelled'),
|
||||
'client_reference_id' => (string)$user['id'],
|
||||
'metadata[user_id]' => (string)$user['id'],
|
||||
'metadata[plan_id]' => (string)$plan['id'],
|
||||
'subscription_data[metadata][user_id]' => (string)$user['id'],
|
||||
'subscription_data[metadata][plan_id]' => (string)$plan['id'],
|
||||
'allow_promotion_codes' => 'true',
|
||||
];
|
||||
if (!empty($user['stripe_customer_id'])) $fields['customer'] = $user['stripe_customer_id'];
|
||||
else $fields['customer_email'] = $user['email'];
|
||||
$session = stripeRequest('POST', 'checkout/sessions', $fields);
|
||||
if (empty($session['url'])) throw new RuntimeException('Stripe did not return a checkout URL');
|
||||
return $session['url'];
|
||||
}
|
||||
|
||||
function createStripePortal(array $user): string {
|
||||
if (empty($user['stripe_customer_id'])) throw new RuntimeException('No Stripe customer is connected');
|
||||
$session = stripeRequest('POST', 'billing_portal/sessions', [
|
||||
'customer' => $user['stripe_customer_id'],
|
||||
'return_url' => stripeReturnUrl('stripe.portal_return_url', appBaseUrl() . '/index.html?view=account'),
|
||||
]);
|
||||
if (empty($session['url'])) throw new RuntimeException('Stripe did not return a portal URL');
|
||||
return $session['url'];
|
||||
}
|
||||
|
||||
function verifyStripeSignature(string $payload, string $header): bool {
|
||||
$secret = trim((string)getConfigVal('stripe.webhook_secret', ''));
|
||||
if ($secret === '' || $header === '') return false;
|
||||
$timestamp = null;
|
||||
$signatures = [];
|
||||
foreach (explode(',', $header) as $part) {
|
||||
[$key, $value] = array_pad(explode('=', trim($part), 2), 2, '');
|
||||
if ($key === 't') $timestamp = $value;
|
||||
if ($key === 'v1') $signatures[] = $value;
|
||||
}
|
||||
if (!$timestamp || abs(time() - (int)$timestamp) > 300) return false;
|
||||
$expected = hash_hmac('sha256', $timestamp . '.' . $payload, $secret);
|
||||
foreach ($signatures as $signature) if (hash_equals($expected, $signature)) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
function planIdFromStripeObject(array $object): ?int {
|
||||
$metadataPlan = (int)($object['metadata']['plan_id'] ?? 0);
|
||||
if ($metadataPlan > 0 && planById($metadataPlan)) return $metadataPlan;
|
||||
$priceId = $object['items']['data'][0]['price']['id'] ?? '';
|
||||
if ($priceId !== '') {
|
||||
$stmt = getDB()->prepare("SELECT id FROM plans WHERE stripe_price_id=?");
|
||||
$stmt->execute([$priceId]);
|
||||
$id = (int)$stmt->fetchColumn();
|
||||
if ($id > 0) return $id;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function syncSubscriptionFromStripe(array $object): void {
|
||||
$db = getDB();
|
||||
$subscriptionId = (string)($object['id'] ?? '');
|
||||
$customerId = (string)($object['customer'] ?? '');
|
||||
$userId = (int)($object['metadata']['user_id'] ?? 0);
|
||||
if ($userId < 1 && $subscriptionId !== '') {
|
||||
$stmt = $db->prepare("SELECT user_id FROM subscriptions WHERE stripe_subscription_id=?");
|
||||
$stmt->execute([$subscriptionId]);
|
||||
$userId = (int)$stmt->fetchColumn();
|
||||
}
|
||||
if ($userId < 1 && $customerId !== '') {
|
||||
$stmt = $db->prepare("SELECT id FROM users WHERE stripe_customer_id=?");
|
||||
$stmt->execute([$customerId]);
|
||||
$userId = (int)$stmt->fetchColumn();
|
||||
}
|
||||
if ($userId < 1) return;
|
||||
|
||||
$status = (string)($object['status'] ?? 'active');
|
||||
$paidStatuses = ['active', 'trialing', 'past_due'];
|
||||
$planId = planIdFromStripeObject($object);
|
||||
if (!in_array($status, $paidStatuses, true)) {
|
||||
$planId = (int)$db->query("SELECT id FROM plans WHERE slug='free'")->fetchColumn();
|
||||
}
|
||||
if (!$planId) return;
|
||||
$periodEnd = (int)($object['current_period_end'] ?? 0) ?: null;
|
||||
$cancel = !empty($object['cancel_at_period_end']) ? 1 : 0;
|
||||
|
||||
$db->beginTransaction();
|
||||
$db->prepare("INSERT INTO subscriptions
|
||||
(user_id,plan_id,stripe_customer_id,stripe_subscription_id,status,current_period_end,cancel_at_period_end,updated_at)
|
||||
VALUES (?,?,?,?,?,?,?,?)
|
||||
ON CONFLICT(user_id) DO UPDATE SET plan_id=excluded.plan_id,stripe_customer_id=excluded.stripe_customer_id,
|
||||
stripe_subscription_id=excluded.stripe_subscription_id,status=excluded.status,
|
||||
current_period_end=excluded.current_period_end,cancel_at_period_end=excluded.cancel_at_period_end,updated_at=excluded.updated_at")
|
||||
->execute([$userId, $planId, $customerId, $subscriptionId, $status, $periodEnd, $cancel, time()]);
|
||||
$db->prepare("UPDATE users SET plan_id=?,stripe_customer_id=?,stripe_subscription_id=?,
|
||||
subscription_status=?,subscription_period_end=?,cancel_at_period_end=? WHERE id=?")
|
||||
->execute([$planId, $customerId, $subscriptionId, $status, $periodEnd, $cancel, $userId]);
|
||||
$db->commit();
|
||||
}
|
||||
Reference in New Issue
Block a user