Initial commit

This commit is contained in:
2026-05-22 12:52:44 +00:00
commit 996ca0d621
122 changed files with 22749 additions and 0 deletions
+284
View File
@@ -0,0 +1,284 @@
<?php
/**
* Tom's Java Jive - Authentication Helper
*/
require_once __DIR__ . '/functions.php';
/**
* Start secure session
*/
function initSession() {
if (session_status() === PHP_SESSION_NONE) {
ini_set('session.cookie_httponly', 1);
ini_set('session.cookie_secure', isset($_SERVER['HTTPS']));
ini_set('session.use_strict_mode', 1);
session_name(SESSION_NAME);
session_start();
}
}
/**
* Admin Authentication
*/
class AdminAuth {
public static function login($email, $password) {
$admin = db()->fetch(
"SELECT * FROM admin_users WHERE email = :email",
['email' => strtolower($email)]
);
if (!$admin || !verifyPassword($password, $admin['password_hash'])) {
return false;
}
// Update last login
db()->update('admin_users',
['last_login' => date('Y-m-d H:i:s')],
'user_id = :id',
['id' => $admin['user_id']]
);
// Set session
$_SESSION['admin'] = [
'user_id' => $admin['user_id'],
'email' => $admin['email'],
'name' => $admin['name'],
'is_master' => (bool)$admin['is_master'],
'permissions' => json_decode($admin['permissions'] ?? '[]', true)
];
// Regenerate session ID for security
session_regenerate_id(true);
return true;
}
public static function logout() {
unset($_SESSION['admin']);
session_regenerate_id(true);
}
public static function isLoggedIn() {
return isset($_SESSION['admin']['user_id']);
}
public static function getUser() {
return $_SESSION['admin'] ?? null;
}
public static function require() {
if (!self::isLoggedIn()) {
if (isAjax()) {
jsonResponse(['error' => 'Unauthorized'], 401);
}
$_SESSION['admin_redirect'] = currentUrl();
redirect('/admin/login.php');
}
}
public static function hasPermission($permission) {
$admin = self::getUser();
if (!$admin) return false;
if ($admin['is_master']) return true;
return in_array($permission, $admin['permissions'] ?? []);
}
public static function register($email, $password, $name = null, $isMaster = false) {
$userId = generateId('admin_');
db()->insert('admin_users', [
'user_id' => $userId,
'email' => strtolower($email),
'password_hash' => hashPassword($password),
'name' => $name ?? $email,
'is_admin' => 1,
'is_master' => $isMaster ? 1 : 0
]);
return $userId;
}
}
/**
* Customer Authentication
*/
class CustomerAuth {
public static function login($email, $password) {
$customer = db()->fetch(
"SELECT * FROM customers WHERE email = :email AND password_hash IS NOT NULL",
['email' => strtolower($email)]
);
if (!$customer || !verifyPassword($password, $customer['password_hash'])) {
return false;
}
// Set session
$_SESSION['customer'] = [
'customer_id' => $customer['customer_id'],
'email' => $customer['email'],
'name' => $customer['name']
];
session_regenerate_id(true);
return true;
}
public static function logout() {
unset($_SESSION['customer']);
session_regenerate_id(true);
}
public static function isLoggedIn() {
return isset($_SESSION['customer']['customer_id']);
}
public static function getUser() {
return $_SESSION['customer'] ?? null;
}
public static function getFullUser() {
if (!self::isLoggedIn()) return null;
return db()->fetch(
"SELECT customer_id, email, name, phone, shipping_address, billing_address,
wallet_balance, reward_points, addresses, preferences, password_hash, created_at
FROM customers WHERE customer_id = :id",
['id' => $_SESSION['customer']['customer_id']]
);
}
public static function require() {
if (!self::isLoggedIn()) {
if (isAjax()) {
jsonResponse(['error' => 'Unauthorized'], 401);
}
$_SESSION['redirect_after_login'] = currentUrl();
redirect('/login.php');
}
}
public static function register($email, $password, $name = null, $phone = null) {
// Check if email exists
$existing = db()->fetch(
"SELECT customer_id FROM customers WHERE email = :email",
['email' => strtolower($email)]
);
if ($existing) {
return ['error' => 'Email already registered'];
}
$customerId = generateId('cust_');
db()->insert('customers', [
'customer_id' => $customerId,
'email' => strtolower($email),
'password_hash' => hashPassword($password),
'name' => $name,
'phone' => $phone
]);
// Auto login after registration
$_SESSION['customer'] = [
'customer_id' => $customerId,
'email' => strtolower($email),
'name' => $name
];
return ['success' => true, 'customer_id' => $customerId];
}
public static function createGuest($email, $name = null, $phone = null) {
// Check if customer exists
$existing = db()->fetch(
"SELECT customer_id FROM customers WHERE email = :email",
['email' => strtolower($email)]
);
if ($existing) {
return $existing['customer_id'];
}
$customerId = generateId('cust_');
db()->insert('customers', [
'customer_id' => $customerId,
'email' => strtolower($email),
'name' => $name,
'phone' => $phone,
'is_guest' => 1
]);
return $customerId;
}
public static function requestPasswordReset($email) {
$customer = db()->fetch(
"SELECT customer_id FROM customers WHERE email = :email AND password_hash IS NOT NULL",
['email' => strtolower($email)]
);
if (!$customer) {
return false;
}
$token = bin2hex(random_bytes(32));
$expiresAt = date('Y-m-d H:i:s', strtotime('+1 hour'));
db()->insert('password_reset_tokens', [
'email' => strtolower($email),
'token' => $token,
'user_type' => 'customer',
'expires_at' => $expiresAt
]);
// Send email
$resetUrl = SITE_URL . '/reset-password.php?token=' . $token;
$html = "
<h2>Password Reset Request</h2>
<p>Click the link below to reset your password:</p>
<p><a href='{$resetUrl}'>{$resetUrl}</a></p>
<p>This link will expire in 1 hour.</p>
<p>If you didn't request this, please ignore this email.</p>
";
sendEmail($email, 'Password Reset - ' . SITE_NAME, $html);
return true;
}
public static function resetPassword($token, $newPassword) {
$reset = db()->fetch(
"SELECT * FROM password_reset_tokens
WHERE token = :token AND user_type = 'customer' AND used = 0 AND expires_at > NOW()",
['token' => $token]
);
if (!$reset) {
return ['error' => 'Invalid or expired token'];
}
// Update password
db()->update('customers',
['password_hash' => hashPassword($newPassword)],
'email = :email',
['email' => $reset['email']]
);
// Mark token as used
db()->update('password_reset_tokens',
['used' => 1],
'id = :id',
['id' => $reset['id']]
);
return ['success' => true];
}
}
// Initialize session on include
initSession();
+104
View File
@@ -0,0 +1,104 @@
<?php
/**
* Tom's Java Jive - Database Class
*
* Singleton PDO database connection
*/
require_once __DIR__ . '/../config/database.php';
class Database {
private static $instance = null;
private $pdo;
private function __construct() {
try {
$dsn = "mysql:host=" . DB_HOST . ";dbname=" . DB_NAME . ";charset=" . DB_CHARSET;
$this->pdo = new PDO($dsn, DB_USER, DB_PASS, DB_OPTIONS);
} catch (PDOException $e) {
if (ENVIRONMENT === 'development') {
die("Database connection failed: " . $e->getMessage());
} else {
die("Database connection failed. Please try again later.");
}
}
}
public static function getInstance() {
if (self::$instance === null) {
self::$instance = new self();
}
return self::$instance;
}
public function getConnection() {
return $this->pdo;
}
public function query($sql, $params = []) {
$stmt = $this->pdo->prepare($sql);
$stmt->execute($params);
return $stmt;
}
public function fetch($sql, $params = []) {
return $this->query($sql, $params)->fetch();
}
public function fetchAll($sql, $params = []) {
return $this->query($sql, $params)->fetchAll();
}
public function insert($table, $data) {
$columns = implode(', ', array_keys($data));
$placeholders = ':' . implode(', :', array_keys($data));
$sql = "INSERT INTO {$table} ({$columns}) VALUES ({$placeholders})";
$this->query($sql, $data);
return $this->pdo->lastInsertId();
}
public function update($table, $data, $where, $whereParams = []) {
$set = [];
foreach (array_keys($data) as $column) {
$set[] = "{$column} = :{$column}";
}
$setString = implode(', ', $set);
$sql = "UPDATE {$table} SET {$setString} WHERE {$where}";
return $this->query($sql, array_merge($data, $whereParams))->rowCount();
}
public function delete($table, $where, $params = []) {
$sql = "DELETE FROM {$table} WHERE {$where}";
return $this->query($sql, $params)->rowCount();
}
public function count($table, $where = '1=1', $params = []) {
$sql = "SELECT COUNT(*) as count FROM {$table} WHERE {$where}";
$result = $this->fetch($sql, $params);
return $result['count'] ?? 0;
}
public function lastInsertId() {
return $this->pdo->lastInsertId();
}
public function beginTransaction() {
return $this->pdo->beginTransaction();
}
public function commit() {
return $this->pdo->commit();
}
public function rollback() {
return $this->pdo->rollBack();
}
}
// Helper function to get database instance
function db() {
return Database::getInstance();
}
+369
View File
@@ -0,0 +1,369 @@
<?php
/**
* Tom's Java Jive - SendGrid Email Service
*
* Handles all transactional emails using SendGrid API
*/
class SendGridEmail {
private string $apiKey;
private string $fromEmail;
private string $fromName;
public function __construct() {
// Load from settings or config
$this->apiKey = getSetting('sendgrid_api_key', defined('SENDGRID_API_KEY') ? SENDGRID_API_KEY : '');
$this->fromEmail = getSetting('sendgrid_from_email', defined('SENDER_EMAIL') ? SENDER_EMAIL : 'noreply@tomsjavajive.com');
$this->fromName = getSetting('sendgrid_from_name', defined('SENDER_NAME') ? SENDER_NAME : "Tom's Java Jive");
}
/**
* Send email via SendGrid API
*/
public function send(string $to, string $subject, string $htmlContent, ?string $textContent = null): array {
$data = [
'personalizations' => [
[
'to' => [['email' => $to]],
'subject' => $subject
]
],
'from' => [
'email' => $this->fromEmail,
'name' => $this->fromName
],
'content' => []
];
if ($textContent) {
$data['content'][] = ['type' => 'text/plain', 'value' => $textContent];
}
$data['content'][] = ['type' => 'text/html', 'value' => $htmlContent];
$ch = curl_init('https://api.sendgrid.com/v3/mail/send');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode($data),
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . $this->apiKey,
'Content-Type: application/json'
],
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 30
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$error = curl_error($ch);
curl_close($ch);
if ($error) {
return ['success' => false, 'error' => $error];
}
// SendGrid returns 202 for accepted
if ($httpCode >= 200 && $httpCode < 300) {
return ['success' => true];
}
return ['success' => false, 'error' => $response, 'code' => $httpCode];
}
/**
* Send order confirmation email
*/
public function sendOrderConfirmation(array $order): array {
$items = json_decode($order['items'], true);
$itemsHtml = '';
foreach ($items as $item) {
$itemsHtml .= sprintf(
'<tr><td style="padding: 10px; border-bottom: 1px solid #eee;">%s</td>
<td style="padding: 10px; border-bottom: 1px solid #eee; text-align: center;">%d</td>
<td style="padding: 10px; border-bottom: 1px solid #eee; text-align: right;">%s</td></tr>',
htmlspecialchars($item['name']),
$item['quantity'],
formatCurrency($item['total'])
);
}
$html = $this->getTemplate('order_confirmation', [
'order_number' => $order['order_number'],
'customer_name' => $order['customer_name'] ?? 'Valued Customer',
'items_html' => $itemsHtml,
'subtotal' => formatCurrency($order['subtotal']),
'tax' => formatCurrency($order['tax']),
'discount' => $order['discount'] > 0 ? '-' . formatCurrency($order['discount']) : '$0.00',
'total' => formatCurrency($order['total']),
'payment_method' => ucfirst($order['payment_method'] ?? 'N/A'),
'order_date' => date('F j, Y', strtotime($order['created_at']))
]);
return $this->send(
$order['customer_email'],
"Order Confirmation - #{$order['order_number']}",
$html
);
}
/**
* Send shipping notification email
*/
public function sendShippingNotification(array $order): array {
$html = $this->getTemplate('shipping_notification', [
'order_number' => $order['order_number'],
'customer_name' => $order['customer_name'] ?? 'Valued Customer',
'tracking_number' => $order['tracking_number'],
'tracking_url' => $order['tracking_url'] ?? '#',
'carrier' => $order['shipping_carrier'] ?? 'Our shipping partner'
]);
return $this->send(
$order['customer_email'],
"Your Order Has Shipped - #{$order['order_number']}",
$html
);
}
/**
* Send password reset email
*/
public function sendPasswordReset(string $email, string $resetToken, string $name = ''): array {
$resetUrl = SITE_URL . '/reset-password.php?token=' . $resetToken;
$html = $this->getTemplate('password_reset', [
'customer_name' => $name ?: 'there',
'reset_url' => $resetUrl,
'expires' => '1 hour'
]);
return $this->send(
$email,
"Reset Your Password - Tom's Java Jive",
$html
);
}
/**
* Send welcome email to new customer
*/
public function sendWelcome(string $email, string $name = ''): array {
$html = $this->getTemplate('welcome', [
'customer_name' => $name ?: 'Coffee Lover',
'shop_url' => SITE_URL . '/shop.php'
]);
return $this->send(
$email,
"Welcome to Tom's Java Jive!",
$html
);
}
/**
* Send abandoned cart reminder
*/
public function sendAbandonedCartReminder(array $cart): array {
$items = json_decode($cart['items'], true);
$itemsHtml = '';
foreach ($items as $item) {
$itemsHtml .= sprintf(
'<div style="padding: 10px; border-bottom: 1px solid #eee;">%s - %s</div>',
htmlspecialchars($item['name']),
formatCurrency($item['price'])
);
}
$html = $this->getTemplate('abandoned_cart', [
'items_html' => $itemsHtml,
'total' => formatCurrency($cart['subtotal']),
'cart_url' => SITE_URL . '/cart.php'
]);
return $this->send(
$cart['customer_email'],
"You left something behind!",
$html
);
}
/**
* Get email template with variables replaced
*/
private function getTemplate(string $name, array $vars = []): string {
$templates = [
'order_confirmation' => '
<div style="max-width: 600px; margin: 0 auto; font-family: Arial, sans-serif;">
<div style="background: #FF5E1A; padding: 20px; text-align: center;">
<h1 style="color: white; margin: 0;">Order Confirmed!</h1>
</div>
<div style="padding: 30px; background: #fff;">
<p>Hi {{customer_name}},</p>
<p>Thank you for your order! We\'ve received it and will begin processing right away.</p>
<div style="background: #f9f9f9; padding: 20px; border-radius: 8px; margin: 20px 0;">
<h3 style="margin-top: 0;">Order #{{order_number}}</h3>
<p style="color: #666; margin-bottom: 0;">{{order_date}}</p>
</div>
<table style="width: 100%; border-collapse: collapse;">
<thead>
<tr style="background: #f5f5f5;">
<th style="padding: 10px; text-align: left;">Item</th>
<th style="padding: 10px; text-align: center;">Qty</th>
<th style="padding: 10px; text-align: right;">Price</th>
</tr>
</thead>
<tbody>
{{items_html}}
</tbody>
</table>
<div style="margin-top: 20px; text-align: right;">
<p style="margin: 5px 0;">Subtotal: {{subtotal}}</p>
<p style="margin: 5px 0;">Tax: {{tax}}</p>
<p style="margin: 5px 0; color: #10B981;">Discount: {{discount}}</p>
<p style="margin: 5px 0; font-size: 1.25em; font-weight: bold; color: #FF5E1A;">Total: {{total}}</p>
</div>
<p style="margin-top: 30px;">Payment Method: {{payment_method}}</p>
<p style="color: #666; font-size: 0.9em; margin-top: 30px;">
If you have any questions, reply to this email or visit our website.
</p>
</div>
<div style="background: #333; color: #fff; padding: 20px; text-align: center;">
<p style="margin: 0;">&copy; ' . date('Y') . ' Tom\'s Java Jive. All rights reserved.</p>
</div>
</div>
',
'shipping_notification' => '
<div style="max-width: 600px; margin: 0 auto; font-family: Arial, sans-serif;">
<div style="background: #FF5E1A; padding: 20px; text-align: center;">
<h1 style="color: white; margin: 0;">Your Order Has Shipped!</h1>
</div>
<div style="padding: 30px; background: #fff;">
<p>Hi {{customer_name}},</p>
<p>Great news! Your order #{{order_number}} is on its way to you.</p>
<div style="background: #f9f9f9; padding: 20px; border-radius: 8px; margin: 20px 0; text-align: center;">
<p style="margin: 0 0 10px; color: #666;">Tracking Number</p>
<p style="margin: 0; font-size: 1.25em; font-weight: bold;">{{tracking_number}}</p>
<p style="margin: 10px 0 0;">Carrier: {{carrier}}</p>
</div>
<div style="text-align: center; margin: 30px 0;">
<a href="{{tracking_url}}" style="display: inline-block; background: #FF5E1A; color: white; padding: 15px 30px; text-decoration: none; border-radius: 8px; font-weight: bold;">Track Your Package</a>
</div>
<p style="color: #666; font-size: 0.9em;">
Please allow 24-48 hours for tracking information to update.
</p>
</div>
<div style="background: #333; color: #fff; padding: 20px; text-align: center;">
<p style="margin: 0;">&copy; ' . date('Y') . ' Tom\'s Java Jive. All rights reserved.</p>
</div>
</div>
',
'password_reset' => '
<div style="max-width: 600px; margin: 0 auto; font-family: Arial, sans-serif;">
<div style="background: #FF5E1A; padding: 20px; text-align: center;">
<h1 style="color: white; margin: 0;">Reset Your Password</h1>
</div>
<div style="padding: 30px; background: #fff;">
<p>Hi {{customer_name}},</p>
<p>We received a request to reset your password. Click the button below to create a new password:</p>
<div style="text-align: center; margin: 30px 0;">
<a href="{{reset_url}}" style="display: inline-block; background: #FF5E1A; color: white; padding: 15px 30px; text-decoration: none; border-radius: 8px; font-weight: bold;">Reset Password</a>
</div>
<p style="color: #666; font-size: 0.9em;">
This link will expire in {{expires}}. If you didn\'t request this, you can safely ignore this email.
</p>
</div>
<div style="background: #333; color: #fff; padding: 20px; text-align: center;">
<p style="margin: 0;">&copy; ' . date('Y') . ' Tom\'s Java Jive. All rights reserved.</p>
</div>
</div>
',
'welcome' => '
<div style="max-width: 600px; margin: 0 auto; font-family: Arial, sans-serif;">
<div style="background: #FF5E1A; padding: 30px; text-align: center;">
<h1 style="color: white; margin: 0;">Welcome to the Family!</h1>
</div>
<div style="padding: 30px; background: #fff;">
<p>Hi {{customer_name}},</p>
<p>Welcome to Tom\'s Java Jive! We\'re thrilled to have you join our community of coffee lovers.</p>
<h3>Here\'s what you can look forward to:</h3>
<ul style="color: #666;">
<li>Exclusive member discounts</li>
<li>Early access to new roasts</li>
<li>Reward points on every purchase</li>
<li>Birthday treats and special offers</li>
</ul>
<div style="text-align: center; margin: 30px 0;">
<a href="{{shop_url}}" style="display: inline-block; background: #FF5E1A; color: white; padding: 15px 30px; text-decoration: none; border-radius: 8px; font-weight: bold;">Start Shopping</a>
</div>
<p>Cheers,<br>The Tom\'s Java Jive Team</p>
</div>
<div style="background: #333; color: #fff; padding: 20px; text-align: center;">
<p style="margin: 0;">&copy; ' . date('Y') . ' Tom\'s Java Jive. All rights reserved.</p>
</div>
</div>
',
'abandoned_cart' => '
<div style="max-width: 600px; margin: 0 auto; font-family: Arial, sans-serif;">
<div style="background: #FF5E1A; padding: 20px; text-align: center;">
<h1 style="color: white; margin: 0;">Forget Something?</h1>
</div>
<div style="padding: 30px; background: #fff;">
<p>Hey there!</p>
<p>We noticed you left some amazing items in your cart. Don\'t let them get away!</p>
<div style="background: #f9f9f9; padding: 20px; border-radius: 8px; margin: 20px 0;">
{{items_html}}
<div style="padding: 15px 10px; font-weight: bold; text-align: right;">
Total: {{total}}
</div>
</div>
<div style="text-align: center; margin: 30px 0;">
<a href="{{cart_url}}" style="display: inline-block; background: #FF5E1A; color: white; padding: 15px 30px; text-decoration: none; border-radius: 8px; font-weight: bold;">Complete Your Order</a>
</div>
<p style="color: #666; font-size: 0.9em;">
Need help? Just reply to this email and we\'ll assist you!
</p>
</div>
<div style="background: #333; color: #fff; padding: 20px; text-align: center;">
<p style="margin: 0;">&copy; ' . date('Y') . ' Tom\'s Java Jive. All rights reserved.</p>
</div>
</div>
'
];
$template = $templates[$name] ?? '<p>Email template not found.</p>';
foreach ($vars as $key => $value) {
$template = str_replace('{{' . $key . '}}', $value, $template);
}
return $template;
}
}
// Helper function for easy access
function sendEmail(): SendGridEmail {
static $instance = null;
if ($instance === null) {
$instance = new SendGridEmail();
}
return $instance;
}
+77
View File
@@ -0,0 +1,77 @@
<?php
/**
* Tom's Java Jive - Footer Include
*/
?>
</main>
<footer class="footer">
<div class="container">
<div class="footer-grid">
<div class="footer-brand">
<a href="/" class="logo">
<img src="/assets/images/logo.png" alt="<?= SITE_NAME ?>" style="height: 50px;">
</a>
<p>Premium artisan coffee beans freshly roasted and delivered to your door. Experience the perfect cup, every time.</p>
</div>
<div>
<h4>Shop</h4>
<ul class="footer-links">
<li><a href="/shop.php">All Products</a></li>
<li><a href="/shop.php?category=single-origin">Single Origin</a></li>
<li><a href="/shop.php?category=blends">Blends</a></li>
<li><a href="/shop.php?category=equipment">Equipment</a></li>
</ul>
</div>
<div>
<h4>Company</h4>
<ul class="footer-links">
<li><a href="/#about">About Us</a></li>
<li><a href="/contact.php">Contact</a></li>
</ul>
</div>
<div>
<h4>Support</h4>
<ul class="footer-links">
<li><a href="/faq.php">FAQ</a></li>
<li><a href="/shipping.php">Shipping Info</a></li>
<li><a href="/returns.php">Returns</a></li>
<li><a href="/track-order.php">Track Order</a></li>
</ul>
</div>
</div>
<div class="footer-bottom">
<p>&copy; <?= date('Y') ?> Tom's Java Jive. All rights reserved.</p>
<div class="footer-social">
<a href="#" title="Facebook"><i class="fab fa-facebook-f"></i></a>
<a href="#" title="Instagram"><i class="fab fa-instagram"></i></a>
<a href="#" title="Twitter"><i class="fab fa-twitter"></i></a>
</div>
</div>
</div>
</footer>
<script src="/assets/js/main.js"></script>
<!-- PWA Service Worker Registration -->
<script>
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/sw.js')
.then(registration => {
console.log('SW registered:', registration.scope);
})
.catch(error => {
console.log('SW registration failed:', error);
});
});
}
</script>
<?php if (isset($extraScripts)) echo $extraScripts; ?>
</body>
</html>
+378
View File
@@ -0,0 +1,378 @@
<?php
/**
* Tom's Java Jive - Helper Functions
*/
require_once __DIR__ . '/../config/config.php';
require_once __DIR__ . '/db.php';
/**
* Generate a unique ID with prefix
*/
function generateId($prefix = '') {
return $prefix . bin2hex(random_bytes(6));
}
/**
* Generate order number
*/
function generateOrderNumber() {
return 'JJ' . strtoupper(bin2hex(random_bytes(4)));
}
/**
* Hash password using bcrypt
*/
function hashPassword($password) {
return password_hash($password, PASSWORD_BCRYPT, ['cost' => HASH_COST]);
}
/**
* Verify password
*/
function verifyPassword($password, $hash) {
return password_verify($password, $hash);
}
/**
* Sanitize input
*/
function sanitize($input) {
if (is_array($input)) {
return array_map('sanitize', $input);
}
return htmlspecialchars(trim($input), ENT_QUOTES, 'UTF-8');
}
/**
* Format currency
*/
function formatCurrency($amount) {
return TJJ_CURRENCY_SYMBOL . number_format((float)$amount, 2);
}
/**
* Format date
*/
function formatDate($date, $format = 'M j, Y') {
return date($format, strtotime($date));
}
/**
* Format datetime
*/
function formatDateTime($date, $format = 'M j, Y g:i A') {
return date($format, strtotime($date));
}
/**
* JSON response helper
*/
function jsonResponse($data, $statusCode = 200) {
http_response_code($statusCode);
header('Content-Type: application/json');
echo json_encode($data);
exit;
}
/**
* Redirect helper
*/
function redirect($url) {
header("Location: $url");
exit;
}
/**
* Get current URL
*/
function currentUrl() {
$protocol = isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? 'https' : 'http';
return $protocol . '://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'];
}
/**
* Check if request is AJAX
*/
function isAjax() {
return !empty($_SERVER['HTTP_X_REQUESTED_WITH']) &&
strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) === 'xmlhttprequest';
}
/**
* Get client IP address
*/
function getClientIp() {
$ip = $_SERVER['REMOTE_ADDR'];
if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
$ip = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR'])[0];
} elseif (!empty($_SERVER['HTTP_CLIENT_IP'])) {
$ip = $_SERVER['HTTP_CLIENT_IP'];
}
return trim($ip);
}
/**
* Generate CSRF token
*/
function generateCsrfToken() {
if (empty($_SESSION[CSRF_TOKEN_NAME])) {
$_SESSION[CSRF_TOKEN_NAME] = bin2hex(random_bytes(32));
}
return $_SESSION[CSRF_TOKEN_NAME];
}
/**
* Verify CSRF token
*/
function verifyCsrfToken($token) {
return isset($_SESSION[CSRF_TOKEN_NAME]) && hash_equals($_SESSION[CSRF_TOKEN_NAME], $token);
}
/**
* Get setting value
*/
function getSetting($key, $default = null) {
$result = db()->fetch(
"SELECT setting_value FROM settings WHERE setting_key = :key",
['key' => $key]
);
if ($result) {
return json_decode($result['setting_value'], true) ?? $result['setting_value'];
}
return $default;
}
/**
* Update setting value
*/
function setSetting($key, $value) {
$jsonValue = json_encode($value);
$existing = db()->fetch(
"SELECT id FROM settings WHERE setting_key = :key",
['key' => $key]
);
if ($existing) {
db()->update('settings', ['setting_value' => $jsonValue], 'setting_key = :key', ['key' => $key]);
} else {
db()->insert('settings', ['setting_key' => $key, 'setting_value' => $jsonValue]);
}
}
/**
* Flash message helpers
*/
function setFlash($type, $message) {
$_SESSION['flash'][$type] = $message;
}
function getFlash($type) {
if (isset($_SESSION['flash'][$type])) {
$message = $_SESSION['flash'][$type];
unset($_SESSION['flash'][$type]);
return $message;
}
return null;
}
function hasFlash($type) {
return isset($_SESSION['flash'][$type]);
}
/**
* Pagination helper
*/
function paginate($totalItems, $currentPage, $perPage = ITEMS_PER_PAGE) {
$totalPages = ceil($totalItems / $perPage);
$currentPage = max(1, min($currentPage, $totalPages));
$offset = ($currentPage - 1) * $perPage;
return [
'total_items' => $totalItems,
'total_pages' => $totalPages,
'current_page' => $currentPage,
'per_page' => $perPage,
'offset' => $offset,
'has_prev' => $currentPage > 1,
'has_next' => $currentPage < $totalPages
];
}
/**
* Render pagination HTML
*/
function renderPagination($pagination, $baseUrl) {
if ($pagination['total_pages'] <= 1) return '';
$html = '<nav class="pagination"><ul>';
// Previous
if ($pagination['has_prev']) {
$html .= '<li><a href="' . $baseUrl . '?page=' . ($pagination['current_page'] - 1) . '">&laquo; Previous</a></li>';
}
// Page numbers
for ($i = 1; $i <= $pagination['total_pages']; $i++) {
if ($i == $pagination['current_page']) {
$html .= '<li class="active"><span>' . $i . '</span></li>';
} else {
$html .= '<li><a href="' . $baseUrl . '?page=' . $i . '">' . $i . '</a></li>';
}
}
// Next
if ($pagination['has_next']) {
$html .= '<li><a href="' . $baseUrl . '?page=' . ($pagination['current_page'] + 1) . '">Next &raquo;</a></li>';
}
$html .= '</ul></nav>';
return $html;
}
/**
* Truncate text
*/
function truncate($text, $length = 100, $suffix = '...') {
if (strlen($text) <= $length) return $text;
return substr($text, 0, $length) . $suffix;
}
/**
* Slugify text
*/
function slugify($text) {
$text = preg_replace('~[^\pL\d]+~u', '-', $text);
$text = iconv('utf-8', 'us-ascii//TRANSLIT', $text);
$text = preg_replace('~[^-\w]+~', '', $text);
$text = trim($text, '-');
$text = preg_replace('~-+~', '-', $text);
return strtolower($text);
}
/**
* Get cart from session
*/
function getCart() {
return $_SESSION['cart'] ?? [];
}
/**
* Add item to cart
*/
function addToCart($productId, $quantity = 1) {
if (!isset($_SESSION['cart'])) {
$_SESSION['cart'] = [];
}
if (isset($_SESSION['cart'][$productId])) {
$_SESSION['cart'][$productId] += $quantity;
} else {
$_SESSION['cart'][$productId] = $quantity;
}
}
/**
* Update cart item quantity
*/
function updateCartItem($productId, $quantity) {
if ($quantity <= 0) {
removeFromCart($productId);
} else {
$_SESSION['cart'][$productId] = $quantity;
}
}
/**
* Remove item from cart
*/
function removeFromCart($productId) {
unset($_SESSION['cart'][$productId]);
}
/**
* Clear cart
*/
function clearCart() {
$_SESSION['cart'] = [];
}
/**
* Get cart count
*/
function getCartCount() {
return array_sum($_SESSION['cart'] ?? []);
}
/**
* Get cart total
*/
function getCartTotal() {
$total = 0;
$cart = getCart();
foreach ($cart as $productId => $quantity) {
$product = db()->fetch(
"SELECT price, sale_price FROM products WHERE product_id = :id AND is_active = 1",
['id' => $productId]
);
if ($product) {
$price = $product['sale_price'] ?? $product['price'];
$total += $price * $quantity;
}
}
return $total;
}
/**
* Send email using SendGrid
*/
function sendEmail($to, $subject, $htmlContent, $textContent = '') {
if (empty(SENDGRID_API_KEY)) {
return false;
}
$data = [
'personalizations' => [
[
'to' => [['email' => $to]],
'subject' => $subject
]
],
'from' => [
'email' => SENDER_EMAIL,
'name' => SENDER_NAME
],
'content' => [
['type' => 'text/html', 'value' => $htmlContent]
]
];
if ($textContent) {
array_unshift($data['content'], ['type' => 'text/plain', 'value' => $textContent]);
}
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, 'https://api.sendgrid.com/v3/mail/send');
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Authorization: Bearer ' . SENDGRID_API_KEY,
'Content-Type: application/json'
]);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
return $httpCode >= 200 && $httpCode < 300;
}
/**
* Log activity
*/
function logActivity($action, $details = [], $userId = null) {
// Implement activity logging if needed
}
+116
View File
@@ -0,0 +1,116 @@
<?php
/**
* Tom's Java Jive - Header Include
*/
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
require_once __DIR__ . '/db.php';
require_once __DIR__ . '/../config/config.php';
require_once __DIR__ . '/auth.php';
$cartCount = 0;
$cart = $_SESSION['cart'] ?? [];
foreach ($cart as $qty) {
$cartCount += $qty;
}
$isLoggedIn = CustomerAuth::isLoggedIn();
$customerUser = $isLoggedIn ? CustomerAuth::getUser() : null;
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?= $pageTitle ?? SITE_NAME ?></title>
<!-- SEO Meta Tags -->
<meta name="description" content="<?= htmlspecialchars($metaDescription ?? "Premium artisan coffee beans freshly roasted and delivered to your door. Shop single origin, blends, and specialty coffee from Tom's Java Jive.") ?>">
<meta name="keywords" content="<?= htmlspecialchars($metaKeywords ?? "artisan coffee, coffee beans, single origin coffee, fresh roasted coffee, Weatherford Texas coffee, specialty coffee") ?>">
<meta name="robots" content="<?= $metaRobots ?? 'index, follow' ?>">
<link rel="canonical" href="<?= $canonicalUrl ?? 'https://tomsjavajive.com' . strtok($_SERVER['REQUEST_URI'], '?') ?>">
<meta property="og:type" content="<?= $ogType ?? 'website' ?>">
<meta property="og:site_name" content="Tom's Java Jive">
<meta property="og:title" content="<?= htmlspecialchars($metaTitle ?? ($pageTitle ?? "Tom's Java Jive")) ?>">
<meta property="og:description" content="<?= htmlspecialchars($metaDescription ?? "Premium artisan coffee beans freshly roasted and delivered to your door.") ?>">
<meta property="og:url" content="<?= $canonicalUrl ?? 'https://tomsjavajive.com' . $_SERVER['REQUEST_URI'] ?>">
<meta property="og:image" content="<?= $ogImage ?? 'https://tomsjavajive.com/assets/images/og-image.jpg' ?>">
<meta property="og:locale" content="en_US">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="<?= htmlspecialchars($metaTitle ?? ($pageTitle ?? "Tom's Java Jive")) ?>">
<meta name="twitter:description" content="<?= htmlspecialchars($metaDescription ?? "Premium artisan coffee beans freshly roasted and delivered to your door.") ?>">
<meta name="twitter:image" content="<?= $ogImage ?? 'https://tomsjavajive.com/assets/images/og-image.jpg' ?>">
<?php if (empty($suppressSchema)): ?>
<script type="application/ld+json">
{"@context":"https://schema.org","@graph":[{"@type":["LocalBusiness","Store","FoodEstablishment"],"@id":"https://tomsjavajive.com/#business","name":"Tom's Java Jive","description":"Premium artisan coffee beans freshly roasted and shipped to your door. Specializing in single origin, signature blends, and specialty roasts from Weatherford, Texas.","url":"https://tomsjavajive.com","telephone":"+18175550120","email":"hello@tomsjavajive.com","priceRange":"$$","currenciesAccepted":"USD","paymentAccepted":"Credit Card, PayPal","servesCuisine":"Coffee","address":{"@type":"PostalAddress","addressLocality":"Weatherford","addressRegion":"TX","postalCode":"76086","addressCountry":"US"},"geo":{"@type":"GeoCoordinates","latitude":32.7554,"longitude":-97.7981},"hasOfferCatalog":{"@type":"OfferCatalog","name":"Coffee Products","itemListElement":[{"@type":"Offer","itemOffered":{"@type":"Product","name":"Single Origin Coffee Beans","description":"Hand-selected single origin beans from around the world, freshly roasted to order."}},{"@type":"Offer","itemOffered":{"@type":"Product","name":"Signature Blend Coffee","description":"Tom's signature house blends crafted for balanced, full-flavored cups."}}]}},{"@type":"WebSite","@id":"https://tomsjavajive.com/#website","url":"https://tomsjavajive.com","name":"Tom's Java Jive","publisher":{"@id":"https://tomsjavajive.com/#business"},"potentialAction":{"@type":"SearchAction","target":"https://tomsjavajive.com/shop.php?search={search_term_string}","query-input":"required name=search_term_string"}}]}
</script>
<?php endif; ?>
<?php if (!empty($breadcrumbs)): ?>
<script type="application/ld+json">
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":<?= json_encode(array_map(function($b,$i){return["@type"=>"ListItem","position"=>$i+1,"name"=>$b['name'],"item"=>$b['url']];},$breadcrumbs,array_keys($breadcrumbs)),JSON_UNESCAPED_SLASHES) ?>}
</script>
<?php endif; ?>
<?php if (!empty($productSchema)): ?>
<script type="application/ld+json"><?= json_encode($productSchema,JSON_UNESCAPED_SLASHES) ?></script>
<?php endif; ?>
<!-- PWA Meta Tags -->
<meta name="theme-color" content="#FF5E1A">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="default">
<meta name="apple-mobile-web-app-title" content="Java Jive">
<link rel="manifest" href="/manifest.json">
<link rel="apple-touch-icon" href="/assets/icons/icon-192.png">
<!-- Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Playfair+Display:wght@600;700&display=swap" rel="stylesheet">
<!-- Icons -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<!-- Styles -->
<link rel="stylesheet" href="/assets/css/style.css">
<!-- Favicon -->
<link rel="icon" href="/assets/images/logo.png" type="image/png">
<?php if (strpos($_SERVER['REQUEST_URI'], 'payment') !== false || strpos($_SERVER['REQUEST_URI'], 'checkout') !== false): ?>
<script src="https://js.stripe.com/v3/"></script>
<?php endif; ?>
<?php if (isset($extraHead)) echo $extraHead; ?>
</head>
<body>
<header class="header">
<nav class="nav container">
<a href="/" class="logo">
<img src="/assets/images/logo.png" alt="<?= SITE_NAME ?>" class="logo-img">
</a>
<ul class="nav-links">
<li><a href="/" class="<?= $_SERVER['REQUEST_URI'] === '/' ? 'active' : '' ?>">Home</a></li>
<li><a href="/shop.php" class="<?= strpos($_SERVER['REQUEST_URI'], 'shop') !== false ? 'active' : '' ?>">Shop</a></li>
<li><a href="/#about">About</a></li>
<?php if ($isLoggedIn): ?>
<li><a href="/account/" class="<?= strpos($_SERVER['REQUEST_URI'], 'account') !== false ? 'active' : '' ?>">Account</a></li>
<?php else: ?>
<li><a href="/login.php">Account</a></li>
<?php endif; ?>
</ul>
<div class="nav-actions">
<a href="/cart.php" class="cart-btn" title="Shopping Cart">
<i class="fas fa-shopping-bag"></i>
<?php if ($cartCount > 0): ?>
<span class="cart-count"><?= $cartCount ?></span>
<?php endif; ?>
</a>
</div>
</nav>
</header>
<main>
+438
View File
@@ -0,0 +1,438 @@
<?php
/**
* Tom's Java Jive - Customer Loyalty Program
*
* Handles loyalty tiers, points, and rewards
*/
class LoyaltyProgram {
// Loyalty tier definitions
private array $tiers = [
'bronze' => [
'name' => 'Bronze Bean',
'min_points' => 0,
'multiplier' => 1.0,
'benefits' => [
'Earn 1 point per $1 spent',
'Birthday reward',
'Member-only offers'
],
'color' => '#CD7F32',
'icon' => 'fa-coffee'
],
'silver' => [
'name' => 'Silver Roast',
'min_points' => 500,
'multiplier' => 1.25,
'benefits' => [
'Earn 1.25 points per $1 spent',
'Free shipping on orders $25+',
'Early access to new products',
'Double points weekends'
],
'color' => '#C0C0C0',
'icon' => 'fa-mug-hot'
],
'gold' => [
'name' => 'Gold Blend',
'min_points' => 1500,
'multiplier' => 1.5,
'benefits' => [
'Earn 1.5 points per $1 spent',
'Free shipping on all orders',
'Exclusive Gold-only products',
'Priority customer support',
'Quarterly free coffee sample'
],
'color' => '#FFD700',
'icon' => 'fa-crown'
],
'platinum' => [
'name' => 'Platinum Reserve',
'min_points' => 5000,
'multiplier' => 2.0,
'benefits' => [
'Earn 2 points per $1 spent',
'Free express shipping',
'VIP early access to everything',
'Annual free bag of premium coffee',
'Dedicated account manager',
'Exclusive tasting events'
],
'color' => '#E5E4E2',
'icon' => 'fa-gem'
]
];
// Points redemption rates
private float $pointsToValue = 0.01; // 1 point = $0.01 (100 points = $1)
/**
* Get all tier definitions
*/
public function getTiers(): array {
return $this->tiers;
}
/**
* Get a specific tier
*/
public function getTier(string $tierKey): ?array {
return $this->tiers[$tierKey] ?? null;
}
/**
* Determine customer's tier based on total points earned (lifetime)
*/
public function calculateTier(int $lifetimePoints): string {
$currentTier = 'bronze';
foreach ($this->tiers as $key => $tier) {
if ($lifetimePoints >= $tier['min_points']) {
$currentTier = $key;
}
}
return $currentTier;
}
/**
* Get customer's current tier info
*/
public function getCustomerTier(string $customerId): array {
$customer = db()->fetch(
"SELECT reward_points, lifetime_points, loyalty_tier FROM customers WHERE customer_id = :id",
['id' => $customerId]
);
if (!$customer) {
return ['tier' => 'bronze', 'info' => $this->tiers['bronze'], 'points' => 0];
}
$lifetimePoints = $customer['lifetime_points'] ?? $customer['reward_points'] ?? 0;
$tierKey = $this->calculateTier($lifetimePoints);
$tier = $this->tiers[$tierKey];
// Calculate progress to next tier
$nextTierKey = $this->getNextTier($tierKey);
$nextTier = $nextTierKey ? $this->tiers[$nextTierKey] : null;
$progress = 100;
$pointsToNext = 0;
if ($nextTier) {
$currentMin = $tier['min_points'];
$nextMin = $nextTier['min_points'];
$pointsToNext = $nextMin - $lifetimePoints;
$progress = min(100, (($lifetimePoints - $currentMin) / ($nextMin - $currentMin)) * 100);
}
return [
'tier' => $tierKey,
'info' => $tier,
'points' => $customer['reward_points'] ?? 0,
'lifetime_points' => $lifetimePoints,
'next_tier' => $nextTierKey,
'next_tier_info' => $nextTier,
'points_to_next' => $pointsToNext,
'progress_percent' => round($progress, 1)
];
}
/**
* Get next tier key
*/
private function getNextTier(string $currentTier): ?string {
$keys = array_keys($this->tiers);
$index = array_search($currentTier, $keys);
return isset($keys[$index + 1]) ? $keys[$index + 1] : null;
}
/**
* Award points for a purchase
*/
public function awardPoints(string $customerId, float $amount, string $description = 'Purchase'): array {
$customerTier = $this->getCustomerTier($customerId);
$multiplier = $customerTier['info']['multiplier'];
// Calculate points (base: 1 point per dollar)
$basePoints = floor($amount);
$bonusPoints = floor($basePoints * ($multiplier - 1));
$totalPoints = $basePoints + $bonusPoints;
// Update customer points
db()->query(
"UPDATE customers SET
reward_points = reward_points + :points,
lifetime_points = COALESCE(lifetime_points, 0) + :points,
updated_at = NOW()
WHERE customer_id = :id",
['points' => $totalPoints, 'id' => $customerId]
);
// Log the transaction
db()->insert('loyalty_transactions', [
'transaction_id' => generateId('lt_'),
'customer_id' => $customerId,
'points' => $totalPoints,
'type' => 'earn',
'description' => $description . ($bonusPoints > 0 ? " (+{$bonusPoints} bonus)" : ''),
'reference_amount' => $amount
]);
// Check for tier upgrade
$newTier = $this->checkTierUpgrade($customerId, $customerTier['tier']);
return [
'points_earned' => $totalPoints,
'base_points' => $basePoints,
'bonus_points' => $bonusPoints,
'multiplier' => $multiplier,
'tier_upgraded' => $newTier !== null,
'new_tier' => $newTier
];
}
/**
* Redeem points for credit
*/
public function redeemPoints(string $customerId, int $points): array {
$customer = db()->fetch(
"SELECT reward_points FROM customers WHERE customer_id = :id",
['id' => $customerId]
);
if (!$customer || $customer['reward_points'] < $points) {
return ['success' => false, 'error' => 'Insufficient points'];
}
$creditValue = $points * $this->pointsToValue;
// Deduct points
db()->query(
"UPDATE customers SET reward_points = reward_points - :points, updated_at = NOW() WHERE customer_id = :id",
['points' => $points, 'id' => $customerId]
);
// Log the redemption
db()->insert('loyalty_transactions', [
'transaction_id' => generateId('lt_'),
'customer_id' => $customerId,
'points' => -$points,
'type' => 'redeem',
'description' => "Redeemed for " . formatCurrency($creditValue) . " credit",
'reference_amount' => $creditValue
]);
// Add to wallet
$newBalance = db()->fetch(
"SELECT wallet_balance FROM customers WHERE customer_id = :id",
['id' => $customerId]
)['wallet_balance'] ?? 0;
$newBalance += $creditValue;
db()->query(
"UPDATE customers SET wallet_balance = :balance WHERE customer_id = :id",
['balance' => $newBalance, 'id' => $customerId]
);
// Log wallet transaction
db()->insert('wallet_transactions', [
'transaction_id' => generateId('wt_'),
'customer_id' => $customerId,
'amount' => $creditValue,
'balance_after' => $newBalance,
'type' => 'loyalty_redemption',
'description' => "Redeemed {$points} loyalty points"
]);
return [
'success' => true,
'points_redeemed' => $points,
'credit_value' => $creditValue,
'new_points_balance' => $customer['reward_points'] - $points,
'new_wallet_balance' => $newBalance
];
}
/**
* Check and process tier upgrade
*/
public function checkTierUpgrade(string $customerId, string $currentTier): ?string {
$customer = db()->fetch(
"SELECT lifetime_points, loyalty_tier FROM customers WHERE customer_id = :id",
['id' => $customerId]
);
if (!$customer) {
return null;
}
$calculatedTier = $this->calculateTier($customer['lifetime_points'] ?? 0);
$storedTier = $customer['loyalty_tier'] ?? 'bronze';
// Compare tier levels
$tierOrder = ['bronze', 'silver', 'gold', 'platinum'];
$calculatedIndex = array_search($calculatedTier, $tierOrder);
$storedIndex = array_search($storedTier, $tierOrder);
if ($calculatedIndex > $storedIndex) {
// Upgrade!
db()->query(
"UPDATE customers SET loyalty_tier = :tier, updated_at = NOW() WHERE customer_id = :id",
['tier' => $calculatedTier, 'id' => $customerId]
);
// Log the upgrade
db()->insert('loyalty_transactions', [
'transaction_id' => generateId('lt_'),
'customer_id' => $customerId,
'points' => 0,
'type' => 'tier_upgrade',
'description' => "Upgraded from {$this->tiers[$storedTier]['name']} to {$this->tiers[$calculatedTier]['name']}"
]);
// Send notifications
$this->sendTierUpgradeNotifications($customerId, $calculatedTier);
return $calculatedTier;
}
return null;
}
/**
* Send tier upgrade notifications
*/
private function sendTierUpgradeNotifications(string $customerId, string $newTier): void {
$customer = db()->fetch(
"SELECT email, phone, name FROM customers WHERE customer_id = :id",
['id' => $customerId]
);
if (!$customer) return;
$tierInfo = $this->tiers[$newTier];
// Send email notification
if (!empty($customer['email'])) {
require_once __DIR__ . '/email.php';
// Custom email for tier upgrade would go here
}
// Send SMS notification
if (!empty($customer['phone'])) {
require_once __DIR__ . '/sms.php';
sendSMS()->sendTierUpgrade($customer['phone'], $tierInfo['name']);
}
// Send push notification
require_once __DIR__ . '/push.php';
pushNotify()->sendTierNotification($customerId, $tierInfo['name'], $tierInfo['benefits']);
}
/**
* Get points conversion info
*/
public function getConversionInfo(): array {
return [
'points_per_dollar' => 1,
'points_value' => $this->pointsToValue,
'points_for_one_dollar' => intval(1 / $this->pointsToValue),
'description' => 'Earn 1 point for every $1 spent. Redeem 100 points for $1 credit.'
];
}
/**
* Get customer's loyalty history
*/
public function getHistory(string $customerId, int $limit = 20): array {
return db()->fetchAll(
"SELECT * FROM loyalty_transactions WHERE customer_id = :id ORDER BY created_at DESC LIMIT :limit",
['id' => $customerId, 'limit' => $limit]
);
}
/**
* Award birthday bonus
*/
public function awardBirthdayBonus(string $customerId): array {
$customerTier = $this->getCustomerTier($customerId);
// Bonus points based on tier
$bonusPoints = match($customerTier['tier']) {
'platinum' => 500,
'gold' => 300,
'silver' => 200,
default => 100
};
db()->query(
"UPDATE customers SET reward_points = reward_points + :points WHERE customer_id = :id",
['points' => $bonusPoints, 'id' => $customerId]
);
db()->insert('loyalty_transactions', [
'transaction_id' => generateId('lt_'),
'customer_id' => $customerId,
'points' => $bonusPoints,
'type' => 'birthday_bonus',
'description' => "Birthday reward - Happy Birthday!"
]);
return ['success' => true, 'points' => $bonusPoints];
}
/**
* Award referral bonus
*/
public function awardReferralBonus(string $referrerId, string $newCustomerId): array {
$referrerBonus = 100;
$newCustomerBonus = 50;
// Award to referrer
db()->query(
"UPDATE customers SET reward_points = reward_points + :points WHERE customer_id = :id",
['points' => $referrerBonus, 'id' => $referrerId]
);
db()->insert('loyalty_transactions', [
'transaction_id' => generateId('lt_'),
'customer_id' => $referrerId,
'points' => $referrerBonus,
'type' => 'referral_bonus',
'description' => "Referral bonus - Thank you for spreading the word!"
]);
// Award to new customer
db()->query(
"UPDATE customers SET reward_points = reward_points + :points WHERE customer_id = :id",
['points' => $newCustomerBonus, 'id' => $newCustomerId]
);
db()->insert('loyalty_transactions', [
'transaction_id' => generateId('lt_'),
'customer_id' => $newCustomerId,
'points' => $newCustomerBonus,
'type' => 'referral_welcome',
'description' => "Welcome bonus from referral"
]);
return [
'referrer_bonus' => $referrerBonus,
'new_customer_bonus' => $newCustomerBonus
];
}
}
// Helper function
function loyalty(): LoyaltyProgram {
static $instance = null;
if ($instance === null) {
$instance = new LoyaltyProgram();
}
return $instance;
}
+181
View File
@@ -0,0 +1,181 @@
<?php
/**
* Tom's Java Jive - Push Notification Service
*
* Handles web push notifications using VAPID
*/
class PushNotification {
private string $publicKey;
private string $privateKey;
private string $subject;
public function __construct() {
// VAPID keys - generate your own at: https://web-push-codelab.glitch.me/
$this->publicKey = getSetting('vapid_public_key', 'YOUR_VAPID_PUBLIC_KEY');
$this->privateKey = getSetting('vapid_private_key', 'YOUR_VAPID_PRIVATE_KEY');
$this->subject = 'mailto:' . getSetting('admin_email', 'admin@tomsjavajive.com');
}
/**
* Get VAPID public key for client
*/
public function getPublicKey(): string {
return $this->publicKey;
}
/**
* Send push notification to a subscription
*/
public function send(array $subscription, string $title, string $body, array $options = []): array {
$payload = json_encode([
'title' => $title,
'body' => $body,
'icon' => $options['icon'] ?? '/assets/icons/icon-192.png',
'badge' => $options['badge'] ?? '/assets/icons/badge-72.png',
'url' => $options['url'] ?? '/',
'tag' => $options['tag'] ?? null,
'data' => $options['data'] ?? []
]);
// For now, we'll store notifications for when the user comes online
// Full web push requires a library like minishlink/web-push
// This is a simplified version that works with the service worker
try {
// Store notification for retrieval
$notificationId = generateId('notif_');
db()->insert('push_notifications', [
'notification_id' => $notificationId,
'subscription_endpoint' => $subscription['endpoint'],
'payload' => $payload,
'status' => 'pending',
'created_at' => date('Y-m-d H:i:s')
]);
return ['success' => true, 'notification_id' => $notificationId];
} catch (Exception $e) {
return ['success' => false, 'error' => $e->getMessage()];
}
}
/**
* Send notification to all subscribed users
*/
public function broadcast(string $title, string $body, array $options = []): array {
$subscriptions = db()->fetchAll("SELECT * FROM push_subscriptions WHERE is_active = 1");
$results = ['sent' => 0, 'failed' => 0];
foreach ($subscriptions as $sub) {
$subscription = [
'endpoint' => $sub['endpoint'],
'keys' => [
'p256dh' => $sub['p256dh_key'],
'auth' => $sub['auth_key']
]
];
$result = $this->send($subscription, $title, $body, $options);
if ($result['success']) {
$results['sent']++;
} else {
$results['failed']++;
}
}
return $results;
}
/**
* Send order status update notification
*/
public function sendOrderUpdate(string $customerId, array $order, string $status): array {
$subscription = $this->getCustomerSubscription($customerId);
if (!$subscription) {
return ['success' => false, 'error' => 'No subscription found'];
}
$messages = [
'confirmed' => "Your order #{$order['order_number']} has been confirmed!",
'processing' => "We're preparing your order #{$order['order_number']}",
'shipped' => "Your order #{$order['order_number']} is on its way!",
'delivered' => "Your order #{$order['order_number']} has been delivered!",
'ready' => "Your order #{$order['order_number']} is ready for pickup!"
];
return $this->send(
$subscription,
"Order Update",
$messages[$status] ?? "Order #{$order['order_number']} status: {$status}",
[
'url' => "/account/order.php?id={$order['order_id']}",
'tag' => "order-{$order['order_id']}"
]
);
}
/**
* Send promotional notification
*/
public function sendPromotion(string $customerId, string $title, string $message, string $url = '/shop.php'): array {
$subscription = $this->getCustomerSubscription($customerId);
if (!$subscription) {
return ['success' => false, 'error' => 'No subscription found'];
}
return $this->send($subscription, $title, $message, ['url' => $url]);
}
/**
* Send loyalty tier notification
*/
public function sendTierNotification(string $customerId, string $tierName, array $benefits): array {
$subscription = $this->getCustomerSubscription($customerId);
if (!$subscription) {
return ['success' => false, 'error' => 'No subscription found'];
}
return $this->send(
$subscription,
"Congratulations! You're now {$tierName}!",
"Enjoy new benefits: " . implode(', ', array_slice($benefits, 0, 2)),
['url' => '/account/']
);
}
/**
* Get customer's push subscription
*/
private function getCustomerSubscription(string $customerId): ?array {
$sub = db()->fetch(
"SELECT * FROM push_subscriptions WHERE customer_id = :id AND is_active = 1 ORDER BY created_at DESC LIMIT 1",
['id' => $customerId]
);
if (!$sub) {
return null;
}
return [
'endpoint' => $sub['endpoint'],
'keys' => [
'p256dh' => $sub['p256dh_key'],
'auth' => $sub['auth_key']
]
];
}
}
// Helper function for easy access
function pushNotify(): PushNotification {
static $instance = null;
if ($instance === null) {
$instance = new PushNotification();
}
return $instance;
}
+195
View File
@@ -0,0 +1,195 @@
<?php
/**
* Tom's Java Jive - Twilio SMS Service
*
* Handles all SMS notifications using Twilio API
*/
class TwilioSMS {
private string $accountSid;
private string $authToken;
private string $fromNumber;
public function __construct() {
// Load from settings or config
$this->accountSid = getSetting('twilio_account_sid', 'YOUR_TWILIO_ACCOUNT_SID');
$this->authToken = getSetting('twilio_auth_token', 'YOUR_TWILIO_AUTH_TOKEN');
$this->fromNumber = getSetting('twilio_phone_number', '+1234567890');
}
/**
* Send SMS via Twilio API
*/
public function send(string $to, string $message): array {
// Ensure phone number is in E.164 format
$to = $this->formatPhoneNumber($to);
if (!$to) {
return ['success' => false, 'error' => 'Invalid phone number'];
}
$url = "https://api.twilio.com/2010-04-01/Accounts/{$this->accountSid}/Messages.json";
$data = [
'To' => $to,
'From' => $this->fromNumber,
'Body' => $message
];
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => http_build_query($data),
CURLOPT_USERPWD => "{$this->accountSid}:{$this->authToken}",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 30
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$error = curl_error($ch);
curl_close($ch);
if ($error) {
return ['success' => false, 'error' => $error];
}
$result = json_decode($response, true);
if ($httpCode >= 200 && $httpCode < 300) {
return [
'success' => true,
'sid' => $result['sid'] ?? null,
'status' => $result['status'] ?? 'queued'
];
}
return [
'success' => false,
'error' => $result['message'] ?? 'Failed to send SMS',
'code' => $result['code'] ?? $httpCode
];
}
/**
* Format phone number to E.164 format
*/
private function formatPhoneNumber(string $phone): ?string {
// Remove all non-numeric characters except +
$phone = preg_replace('/[^0-9+]/', '', $phone);
// If already in E.164 format
if (preg_match('/^\+[1-9]\d{1,14}$/', $phone)) {
return $phone;
}
// US number without country code
if (preg_match('/^1?\d{10}$/', $phone)) {
$phone = preg_replace('/^1?/', '', $phone);
return '+1' . $phone;
}
return null;
}
/**
* Send order confirmation SMS
*/
public function sendOrderConfirmation(array $order, string $phone): array {
$message = "Tom's Java Jive: Your order #{$order['order_number']} has been confirmed! " .
"Total: " . formatCurrency($order['total']) . ". " .
"Thank you for your purchase!";
return $this->send($phone, $message);
}
/**
* Send shipping notification SMS
*/
public function sendShippingNotification(array $order, string $phone): array {
$message = "Tom's Java Jive: Your order #{$order['order_number']} has shipped! " .
"Tracking: {$order['tracking_number']}. " .
"Track at: " . ($order['tracking_url'] ?? SITE_URL);
return $this->send($phone, $message);
}
/**
* Send delivery notification SMS
*/
public function sendDeliveryNotification(array $order, string $phone): array {
$message = "Tom's Java Jive: Great news! Your order #{$order['order_number']} " .
"has been delivered. Enjoy your coffee!";
return $this->send($phone, $message);
}
/**
* Send password reset SMS
*/
public function sendPasswordResetCode(string $phone, string $code): array {
$message = "Tom's Java Jive: Your password reset code is {$code}. " .
"This code expires in 15 minutes. Don't share it with anyone.";
return $this->send($phone, $message);
}
/**
* Send OTP verification SMS
*/
public function sendVerificationCode(string $phone, string $code): array {
$message = "Tom's Java Jive: Your verification code is {$code}. " .
"Valid for 10 minutes.";
return $this->send($phone, $message);
}
/**
* Send promotional SMS (with opt-out info)
*/
public function sendPromotion(string $phone, string $promoMessage): array {
$message = "Tom's Java Jive: {$promoMessage} " .
"Reply STOP to unsubscribe.";
return $this->send($phone, $message);
}
/**
* Send order ready for pickup SMS
*/
public function sendReadyForPickup(array $order, string $phone): array {
$message = "Tom's Java Jive: Your order #{$order['order_number']} is ready for pickup! " .
"Show this message at the counter.";
return $this->send($phone, $message);
}
/**
* Send low wallet balance alert
*/
public function sendLowBalanceAlert(string $phone, float $balance): array {
$message = "Tom's Java Jive: Your wallet balance is " . formatCurrency($balance) . ". " .
"Top up now to continue enjoying fast checkout!";
return $this->send($phone, $message);
}
/**
* Send loyalty tier upgrade notification
*/
public function sendTierUpgrade(string $phone, string $tierName): array {
$message = "Tom's Java Jive: Congratulations! You've reached {$tierName} status! " .
"Enjoy your new benefits and rewards. Thank you for being a loyal customer!";
return $this->send($phone, $message);
}
}
// Helper function for easy access
function sendSMS(): TwilioSMS {
static $instance = null;
if ($instance === null) {
$instance = new TwilioSMS();
}
return $instance;
}
+214
View File
@@ -0,0 +1,214 @@
<?php
/**
* Tom's Java Jive - Stripe Integration (cURL-based for vanilla PHP)
* Works without Composer - uses direct API calls
*/
class StripeAPI {
private $secretKey;
private $apiBase = 'https://api.stripe.com/v1';
public function __construct($secretKey = null) {
$this->secretKey = $secretKey ?: STRIPE_SECRET_KEY;
}
/**
* Make a cURL request to Stripe API
*/
private function request($method, $endpoint, $data = []) {
$url = $this->apiBase . $endpoint;
$ch = curl_init();
$headers = [
'Authorization: Bearer ' . $this->secretKey,
'Content-Type: application/x-www-form-urlencoded'
];
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_TIMEOUT, 30);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
if ($method === 'POST') {
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($data));
} elseif ($method === 'GET' && !empty($data)) {
$url .= '?' . http_build_query($data);
curl_setopt($ch, CURLOPT_URL, $url);
}
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$error = curl_error($ch);
curl_close($ch);
if ($error) {
throw new Exception('Stripe API Error: ' . $error);
}
$decoded = json_decode($response, true);
if ($httpCode >= 400) {
$errorMsg = $decoded['error']['message'] ?? 'Unknown Stripe error';
throw new Exception($errorMsg);
}
return $decoded;
}
/**
* Create a Payment Intent
*/
public function createPaymentIntent($amount, $currency = 'usd', $options = []) {
$data = [
'amount' => (int)($amount * 100), // Convert to cents
'currency' => strtolower($currency),
'automatic_payment_methods' => ['enabled' => 'true']
];
if (!empty($options['metadata'])) {
foreach ($options['metadata'] as $key => $value) {
$data["metadata[$key]"] = $value;
}
}
if (!empty($options['receipt_email'])) {
$data['receipt_email'] = $options['receipt_email'];
}
if (!empty($options['description'])) {
$data['description'] = $options['description'];
}
return $this->request('POST', '/payment_intents', $data);
}
/**
* Retrieve a Payment Intent
*/
public function getPaymentIntent($paymentIntentId) {
return $this->request('GET', '/payment_intents/' . $paymentIntentId);
}
/**
* Create a Checkout Session (hosted payment page)
*/
public function createCheckoutSession($lineItems, $successUrl, $cancelUrl, $options = []) {
$data = [
'mode' => $options['mode'] ?? 'payment',
'success_url' => $successUrl,
'cancel_url' => $cancelUrl
];
// Add line items
foreach ($lineItems as $i => $item) {
$data["line_items[$i][price_data][currency]"] = $item['currency'] ?? 'usd';
$data["line_items[$i][price_data][product_data][name]"] = $item['name'];
$data["line_items[$i][price_data][unit_amount]"] = (int)($item['price'] * 100);
$data["line_items[$i][quantity]"] = $item['quantity'] ?? 1;
if (!empty($item['description'])) {
$data["line_items[$i][price_data][product_data][description]"] = $item['description'];
}
}
if (!empty($options['customer_email'])) {
$data['customer_email'] = $options['customer_email'];
}
if (!empty($options['metadata'])) {
foreach ($options['metadata'] as $key => $value) {
$data["metadata[$key]"] = $value;
}
}
return $this->request('POST', '/checkout/sessions', $data);
}
/**
* Retrieve a Checkout Session
*/
public function getCheckoutSession($sessionId) {
return $this->request('GET', '/checkout/sessions/' . $sessionId);
}
/**
* Verify webhook signature
*/
public function verifyWebhookSignature($payload, $sigHeader, $webhookSecret) {
if (empty($webhookSecret)) {
return true; // Skip verification if secret not configured
}
$elements = explode(',', $sigHeader);
$timestamp = null;
$signatures = [];
foreach ($elements as $element) {
$parts = explode('=', $element, 2);
if (count($parts) === 2) {
if ($parts[0] === 't') {
$timestamp = $parts[1];
} elseif ($parts[0] === 'v1') {
$signatures[] = $parts[1];
}
}
}
if (empty($timestamp) || empty($signatures)) {
throw new Exception('Invalid signature format');
}
// Check timestamp tolerance (5 minutes)
if (abs(time() - $timestamp) > 300) {
throw new Exception('Timestamp outside tolerance');
}
$signedPayload = $timestamp . '.' . $payload;
$expectedSignature = hash_hmac('sha256', $signedPayload, $webhookSecret);
foreach ($signatures as $sig) {
if (hash_equals($expectedSignature, $sig)) {
return true;
}
}
throw new Exception('Signature verification failed');
}
/**
* Create a refund
*/
public function createRefund($paymentIntentId, $amount = null) {
$data = ['payment_intent' => $paymentIntentId];
if ($amount !== null) {
$data['amount'] = (int)($amount * 100);
}
return $this->request('POST', '/refunds', $data);
}
}
/**
* Get Stripe instance
*/
function stripe() {
static $stripe = null;
if ($stripe === null) {
$stripe = new StripeAPI();
}
return $stripe;
}
/**
* Check if Stripe is properly configured
*/
function isStripeConfigured() {
return !empty(STRIPE_SECRET_KEY) &&
STRIPE_SECRET_KEY !== 'sk_test_your_stripe_key' &&
!empty(STRIPE_PUBLISHABLE_KEY) &&
STRIPE_PUBLISHABLE_KEY !== 'pk_test_your_stripe_key';
}