[orbis] Weekly backup 2026-07-07 — 318 files changed, 48597 insertions(+), 7 deletions(-)

This commit is contained in:
DO Server Backup
2026-07-07 15:58:55 +00:00
parent 5fda5a1536
commit fe18800d18
318 changed files with 48597 additions and 7 deletions
@@ -0,0 +1,58 @@
<?php
/**
* Tom's Java Jive - Apply Wallet Credit / Gift Card at Checkout
*
* Validates and quotes an amount to apply toward the current order total.
* Does NOT deduct wallet balance here - that only happens once the Square
* payment actually succeeds (see markSquarePaymentResult() in
* includes/square.php), so an abandoned checkout never loses a customer's
* real wallet money. A gift card code, if given, is redeemed into wallet
* balance immediately (same as the Wallet page does) since that step is
* safe/reversible-in-spirit - only the *spending* of wallet balance at
* checkout is deferred to payment success.
*/
require_once __DIR__ . '/../includes/functions.php';
require_once __DIR__ . '/../includes/auth.php';
require_once __DIR__ . '/../includes/loyalty.php';
header('Content-Type: application/json');
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
jsonResponse(['error' => 'Method not allowed'], 405);
}
if (!CustomerAuth::isLoggedIn()) {
jsonResponse(['error' => 'Please log in to use wallet balance or a gift card'], 401);
}
$customer = CustomerAuth::getFullUser();
$input = json_decode(file_get_contents('php://input'), true);
$orderTotal = (float) ($input['order_total'] ?? 0);
if ($orderTotal <= 0) {
jsonResponse(['error' => 'Invalid order total'], 400);
}
$giftCardCode = trim($input['gift_card_code'] ?? '');
if (!empty($giftCardCode)) {
$redeemResult = loyalty()->redeemGiftCardToWallet($customer['customer_id'], $giftCardCode);
if (!$redeemResult['success']) {
jsonResponse(['error' => $redeemResult['error']], 400);
}
$walletBalance = $redeemResult['new_balance'];
} else {
$fresh = db()->fetch("SELECT wallet_balance FROM customers WHERE customer_id = :id", ['id' => $customer['customer_id']]);
$walletBalance = (float) ($fresh['wallet_balance'] ?? 0);
}
$requested = isset($input['amount']) ? (float) $input['amount'] : $walletBalance;
$applyAmount = max(0, round(min($requested, $walletBalance, $orderTotal), 2));
jsonResponse([
'success' => true,
'apply_amount' => $applyAmount,
'wallet_balance' => $walletBalance,
'new_total' => round($orderTotal - $applyAmount, 2)
]);
@@ -0,0 +1,121 @@
<?php
/**
* Tom's Java Jive - Cart API
*/
require_once __DIR__ . '/../includes/functions.php';
header('Content-Type: application/json');
$input = json_decode(file_get_contents('php://input'), true);
$action = $input['action'] ?? $_POST['action'] ?? '';
switch ($action) {
case 'add':
$productId = $input['product_id'] ?? '';
$quantity = intval($input['quantity'] ?? 1);
if (!$productId) {
jsonResponse(['error' => 'Product ID required'], 400);
}
// Verify product exists and is active
$product = db()->fetch(
"SELECT product_id, stock FROM products WHERE product_id = :id AND is_active = 1",
['id' => $productId]
);
if (!$product) {
jsonResponse(['error' => 'Product not found'], 404);
}
if ($product['stock'] < $quantity) {
jsonResponse(['error' => 'Not enough stock'], 400);
}
addToCart($productId, $quantity);
jsonResponse([
'success' => true,
'cart_count' => getCartCount(),
'message' => 'Item added to cart'
]);
break;
case 'update':
$productId = $input['product_id'] ?? '';
$quantity = intval($input['quantity'] ?? 0);
if (!$productId) {
jsonResponse(['error' => 'Product ID required'], 400);
}
updateCartItem($productId, $quantity);
jsonResponse([
'success' => true,
'cart_count' => getCartCount(),
'subtotal' => getCartTotal()
]);
break;
case 'remove':
$productId = $input['product_id'] ?? '';
if (!$productId) {
jsonResponse(['error' => 'Product ID required'], 400);
}
removeFromCart($productId);
jsonResponse([
'success' => true,
'cart_count' => getCartCount(),
'subtotal' => getCartTotal()
]);
break;
case 'clear':
clearCart();
jsonResponse(['success' => true, 'cart_count' => 0]);
break;
case 'get':
$cart = getCart();
$items = [];
$subtotal = 0;
foreach ($cart as $productId => $quantity) {
$product = db()->fetch(
"SELECT product_id, name, price, sale_price, stock, images FROM products WHERE product_id = :id",
['id' => $productId]
);
if ($product) {
$images = json_decode($product['images'] ?? '[]', true);
$unitPrice = $product['sale_price'] ?? $product['price'];
$total = $unitPrice * $quantity;
$subtotal += $total;
$items[] = [
'product_id' => $product['product_id'],
'name' => $product['name'],
'price' => $unitPrice,
'quantity' => $quantity,
'total' => $total,
'image' => !empty($images) ? $images[0] : null,
'stock' => $product['stock']
];
}
}
jsonResponse([
'items' => $items,
'count' => getCartCount(),
'subtotal' => $subtotal
]);
break;
default:
jsonResponse(['error' => 'Invalid action'], 400);
}
@@ -0,0 +1,94 @@
<?php
/**
* Tom's Java Jive - Create Square Payment API
* Accepts a Web Payments SDK card nonce (source_id) and charges it synchronously.
*/
require_once __DIR__ . '/../includes/functions.php';
require_once __DIR__ . '/../includes/square.php';
require_once __DIR__ . '/../includes/loyalty.php';
require_once __DIR__ . '/../includes/email.php';
header('Content-Type: application/json');
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
jsonResponse(['error' => 'Method not allowed'], 405);
}
$input = json_decode(file_get_contents('php://input'), true);
$orderId = $input['order_id'] ?? '';
$sourceId = $input['source_id'] ?? '';
if (empty($orderId)) {
jsonResponse(['error' => 'Order ID required'], 400);
}
$order = db()->fetch(
"SELECT * FROM orders WHERE order_id = :id",
['id' => $orderId]
);
if (!$order) {
jsonResponse(['error' => 'Order not found'], 404);
}
if ($order['payment_status'] === 'paid') {
jsonResponse(['error' => 'Order already paid'], 400);
}
// Demo mode - Square not configured
if (!isSquareConfigured()) {
db()->update('orders',
[
'payment_status' => 'paid',
'order_status' => 'confirmed',
'square_payment_id' => 'demo_' . bin2hex(random_bytes(8)),
'payment_method' => 'square',
],
'order_id = :id',
['id' => $orderId]
);
jsonResponse([
'demo_mode' => true,
'message' => 'Payment simulated (Square not configured)',
'redirect' => '/order-confirmation.php?order=' . $orderId
]);
}
if (empty($sourceId)) {
jsonResponse(['error' => 'Card details required'], 400);
}
try {
$result = squareCreatePayment(
$sourceId,
(float) $order['total'],
$orderId,
['note' => 'Order #' . $order['order_number']]
);
$payment = $result['payment'] ?? [];
$status = $payment['status'] ?? '';
if ($status === 'COMPLETED') {
markSquarePaymentResult($payment['id'], $orderId, 'COMPLETED');
jsonResponse([
'success' => true,
'redirect' => '/order-confirmation.php?order=' . $orderId
]);
}
// Store the payment id even if not yet completed, so the reconciliation
// poll and webhook can find this order by square_payment_id.
db()->update('orders',
['square_payment_id' => $payment['id'] ?? null],
'order_id = :id',
['id' => $orderId]
);
jsonResponse(['error' => 'Payment was not completed (status: ' . $status . ')'], 400);
} catch (Exception $e) {
error_log('Square payment error: ' . $e->getMessage());
jsonResponse(['error' => 'Payment failed: ' . $e->getMessage()], 500);
}
@@ -0,0 +1,53 @@
<?php
/**
* Tom's Java Jive - Delete Account API
*/
require_once __DIR__ . '/../includes/functions.php';
require_once __DIR__ . '/../includes/auth.php';
if (!CustomerAuth::isLoggedIn()) {
redirect('/login.php');
}
$customer = CustomerAuth::getFullUser();
try {
// Start transaction
db()->query("START TRANSACTION");
// Delete wallet transactions
db()->query("DELETE FROM wallet_transactions WHERE customer_id = :id", ['id' => $customer['customer_id']]);
// Delete reviews
db()->query("DELETE FROM reviews WHERE customer_id = :id", ['id' => $customer['customer_id']]);
// Delete wishlist
db()->query("DELETE FROM wishlist WHERE customer_id = :id", ['id' => $customer['customer_id']]);
// Anonymize orders (keep for records but remove personal info)
db()->query(
"UPDATE orders SET customer_name = 'Deleted User', customer_email = 'deleted@example.com',
shipping_address = NULL, billing_address = NULL WHERE customer_id = :id",
['id' => $customer['customer_id']]
);
// Remove from email subscribers
db()->query("DELETE FROM email_subscribers WHERE email = :email", ['email' => $customer['email']]);
// Delete customer
db()->query("DELETE FROM customers WHERE customer_id = :id", ['id' => $customer['customer_id']]);
db()->query("COMMIT");
// Logout
CustomerAuth::logout();
setFlash('success', 'Your account has been deleted. We\'re sorry to see you go!');
redirect('/');
} catch (Exception $e) {
db()->query("ROLLBACK");
setFlash('error', 'Failed to delete account. Please contact support.');
redirect('/account/profile.php');
}
@@ -0,0 +1,94 @@
<?php
/**
* Tom's Java Jive - Loyalty Points API
*/
header('Content-Type: application/json');
require_once __DIR__ . '/../includes/functions.php';
require_once __DIR__ . '/../includes/auth.php';
require_once __DIR__ . '/../includes/loyalty.php';
if (!CustomerAuth::isLoggedIn()) {
jsonResponse(['error' => 'Authentication required'], 401);
}
$customer = CustomerAuth::getUser();
$method = $_SERVER['REQUEST_METHOD'];
$input = json_decode(file_get_contents('php://input'), true);
$action = $input['action'] ?? $_GET['action'] ?? '';
switch ($action) {
case 'status':
// Get customer's loyalty status
$status = loyalty()->getCustomerTier($customer['customer_id']);
$conversion = loyalty()->getConversionInfo();
jsonResponse([
'tier' => $status['tier'],
'tier_name' => $status['info']['name'],
'tier_color' => $status['info']['color'],
'tier_icon' => $status['info']['icon'],
'benefits' => $status['info']['benefits'],
'multiplier' => $status['info']['multiplier'],
'points' => $status['points'],
'lifetime_points' => $status['lifetime_points'],
'points_value' => $status['points'] * $conversion['points_value'],
'next_tier' => $status['next_tier'],
'next_tier_name' => $status['next_tier_info']['name'] ?? null,
'points_to_next' => $status['points_to_next'],
'progress_percent' => $status['progress_percent'],
'conversion' => $conversion
]);
break;
case 'history':
// Get loyalty transaction history
$limit = min(50, intval($_GET['limit'] ?? 20));
$history = loyalty()->getHistory($customer['customer_id'], $limit);
jsonResponse(['transactions' => $history]);
break;
case 'redeem':
// Redeem points for credit
if ($method !== 'POST') {
jsonResponse(['error' => 'POST required'], 405);
}
$points = intval($input['points'] ?? 0);
if ($points < 100) {
jsonResponse(['error' => 'Minimum 100 points required for redemption'], 400);
}
$result = loyalty()->redeemPoints($customer['customer_id'], $points);
if ($result['success']) {
jsonResponse([
'success' => true,
'points_redeemed' => $result['points_redeemed'],
'credit_value' => $result['credit_value'],
'new_points_balance' => $result['new_points_balance'],
'new_wallet_balance' => $result['new_wallet_balance'],
'message' => 'Successfully redeemed ' . $points . ' points for ' . formatCurrency($result['credit_value'])
]);
} else {
jsonResponse(['error' => $result['error']], 400);
}
break;
case 'tiers':
// Get all tier information
$tiers = loyalty()->getTiers();
$conversion = loyalty()->getConversionInfo();
jsonResponse([
'tiers' => $tiers,
'conversion' => $conversion
]);
break;
default:
jsonResponse(['error' => 'Invalid action'], 400);
}
@@ -0,0 +1,183 @@
<?php
/**
* Tom's Java Jive - Orders API
*/
require_once __DIR__ . '/../includes/functions.php';
require_once __DIR__ . '/../includes/auth.php';
header('Content-Type: application/json');
$method = $_SERVER['REQUEST_METHOD'];
$input = json_decode(file_get_contents('php://input'), true);
$action = $input['action'] ?? $_POST['action'] ?? '';
switch ($method) {
case 'GET':
// Get order(s)
$orderId = $_GET['id'] ?? '';
$orderNumber = $_GET['number'] ?? '';
if ($orderId) {
$order = db()->fetch(
"SELECT * FROM orders WHERE order_id = :id",
['id' => $orderId]
);
if (!$order) {
jsonResponse(['error' => 'Order not found'], 404);
}
// Only the admin or the customer who placed this order may view it by raw id.
$customer = CustomerAuth::getUser();
$isOwner = $customer && (string)$customer['customer_id'] === (string)$order['customer_id'];
if (!AdminAuth::isLoggedIn() && !$isOwner) {
jsonResponse(['error' => 'Unauthorized'], 401);
}
$order['items'] = json_decode($order['items'], true);
$order['shipping_address'] = json_decode($order['shipping_address'], true);
unset($order['id']);
jsonResponse($order);
} elseif ($orderNumber) {
$email = $_GET['email'] ?? '';
$order = db()->fetch(
"SELECT * FROM orders WHERE order_number = :num AND customer_email = :email",
['num' => $orderNumber, 'email' => strtolower($email)]
);
if (!$order) {
jsonResponse(['error' => 'Order not found'], 404);
}
$order['items'] = json_decode($order['items'], true);
$order['shipping_address'] = json_decode($order['shipping_address'], true);
unset($order['id']);
jsonResponse($order);
} else {
// List orders (admin only or customer's own)
$customer = CustomerAuth::getUser();
if ($customer) {
$orders = db()->fetchAll(
"SELECT order_id, order_number, total, payment_status, order_status, created_at
FROM orders WHERE customer_id = :cid ORDER BY created_at DESC LIMIT 50",
['cid' => $customer['customer_id']]
);
} else {
jsonResponse(['error' => 'Authentication required'], 401);
}
jsonResponse(['orders' => $orders]);
}
break;
case 'POST':
// Update order status (admin)
if ($action === 'update_status') {
if (!AdminAuth::isLoggedIn()) {
jsonResponse(['error' => 'Unauthorized'], 401);
}
$orderId = $input['order_id'] ?? '';
$status = $input['status'] ?? '';
$trackingNumber = $input['tracking_number'] ?? null;
if (empty($orderId) || empty($status)) {
jsonResponse(['error' => 'Order ID and status required'], 400);
}
$validStatuses = ['pending', 'confirmed', 'processing', 'shipped', 'delivered', 'cancelled', 'refunded'];
if (!in_array($status, $validStatuses)) {
jsonResponse(['error' => 'Invalid status'], 400);
}
$updateData = ['order_status' => $status];
if ($trackingNumber) {
$updateData['tracking_number'] = $trackingNumber;
}
db()->update('orders', $updateData, 'order_id = :id', ['id' => $orderId]);
// If status is shipped or delivered, send email
$order = db()->fetch("SELECT * FROM orders WHERE order_id = :id", ['id' => $orderId]);
if ($order && in_array($status, ['shipped', 'delivered'])) {
sendStatusUpdateEmail($order, $status, $trackingNumber);
}
jsonResponse(['success' => true, 'status' => $status]);
}
// Cancel order
if ($action === 'cancel') {
$orderId = $input['order_id'] ?? '';
$customer = CustomerAuth::getUser();
if (!$customer) {
jsonResponse(['error' => 'Authentication required'], 401);
}
$order = db()->fetch(
"SELECT * FROM orders WHERE order_id = :id AND customer_id = :cid",
['id' => $orderId, 'cid' => $customer['customer_id']]
);
if (!$order) {
jsonResponse(['error' => 'Order not found'], 404);
}
if (!in_array($order['order_status'], ['pending', 'confirmed'])) {
jsonResponse(['error' => 'This order cannot be cancelled'], 400);
}
db()->update('orders',
['order_status' => 'cancelled'],
'order_id = :id',
['id' => $orderId]
);
// Restore stock
$items = json_decode($order['items'], true) ?? [];
foreach ($items as $item) {
db()->query(
"UPDATE products SET stock = stock + :qty WHERE product_id = :id",
['qty' => $item['quantity'], 'id' => $item['product_id']]
);
}
jsonResponse(['success' => true]);
}
jsonResponse(['error' => 'Invalid action'], 400);
break;
default:
jsonResponse(['error' => 'Method not allowed'], 405);
}
function sendStatusUpdateEmail($order, $status, $trackingNumber = null) {
$statusMessages = [
'shipped' => 'Your order has been shipped!',
'delivered' => 'Your order has been delivered!'
];
$tracking = $trackingNumber ? "<p><strong>Tracking #:</strong> {$trackingNumber}</p>" : '';
$html = <<<HTML
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
<div style="background: #8B4513; color: white; padding: 20px; text-align: center;">
<h1 style="margin: 0;">Tom's Java Jive</h1>
</div>
<div style="padding: 30px; background: #FDFBF7;">
<h2>{$statusMessages[$status]}</h2>
<p>Hi {$order['customer_name']},</p>
<p>Order <strong>#{$order['order_number']}</strong> has been updated to: <strong>{$status}</strong></p>
{$tracking}
</div>
</div>
HTML;
sendEmail($order['customer_email'], "Order Update - #{$order['order_number']}", $html);
}
@@ -0,0 +1,93 @@
<?php
/**
* Tom's Java Jive - Check Payment Status API
* Reconciliation-only fallback: Square payments complete synchronously in
* create-square-payment.php, so this endpoint only matters if the browser
* lost that response after Square had already accepted the charge. It shares
* markSquarePaymentResult() with the webhook and the synchronous path so
* loyalty points/emails are never duplicated no matter which path resolves first.
*/
require_once __DIR__ . '/../includes/functions.php';
require_once __DIR__ . '/../includes/square.php';
require_once __DIR__ . '/../includes/loyalty.php';
require_once __DIR__ . '/../includes/email.php';
header('Content-Type: application/json');
if ($_SERVER['REQUEST_METHOD'] !== 'GET') {
jsonResponse(['error' => 'Method not allowed'], 405);
}
$orderId = $_GET['order_id'] ?? '';
if (empty($orderId)) {
jsonResponse(['error' => 'Order ID required'], 400);
}
$order = db()->fetch(
"SELECT * FROM orders WHERE order_id = :id",
['id' => $orderId]
);
if (!$order) {
jsonResponse(['error' => 'Order not found'], 404);
}
if ($order['payment_status'] === 'paid') {
jsonResponse([
'status' => 'complete',
'payment_status' => 'paid',
'order_id' => $order['order_id'],
'order_number' => $order['order_number'],
'redirect' => '/order-confirmation.php?order=' . $order['order_id']
]);
}
if (!isSquareConfigured()) {
jsonResponse([
'status' => 'demo_mode',
'payment_status' => $order['payment_status'],
'message' => 'Square not configured - running in demo mode'
]);
}
if (empty($order['square_payment_id'])) {
jsonResponse([
'status' => 'pending',
'payment_status' => $order['payment_status']
]);
}
try {
$result = squareGetPayment($order['square_payment_id']);
$payment = $result['payment'] ?? [];
$status = $payment['status'] ?? '';
markSquarePaymentResult($order['square_payment_id'], $order['order_id'], $status);
$updated = db()->fetch("SELECT * FROM orders WHERE order_id = :id", ['id' => $order['order_id']]);
if ($updated['payment_status'] === 'paid') {
jsonResponse([
'status' => 'complete',
'payment_status' => 'paid',
'order_id' => $updated['order_id'],
'order_number' => $updated['order_number'],
'redirect' => '/order-confirmation.php?order=' . $updated['order_id']
]);
}
jsonResponse([
'status' => $status,
'payment_status' => $updated['payment_status']
]);
} catch (Exception $e) {
error_log('Square payment status check error: ' . $e->getMessage());
jsonResponse([
'status' => 'error',
'payment_status' => $order['payment_status'],
'error' => 'Failed to check payment status'
]);
}
@@ -0,0 +1,191 @@
<?php
/**
* Tom's Java Jive - POS Order API
* Creates orders from the POS system
*/
header('Content-Type: application/json');
require_once __DIR__ . '/../includes/functions.php';
require_once __DIR__ . '/../includes/auth.php';
// Only accept POST
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
jsonResponse(['error' => 'Method not allowed'], 405);
}
$input = json_decode(file_get_contents('php://input'), true);
if (empty($input['items']) || !is_array($input['items'])) {
jsonResponse(['error' => 'No items provided'], 400);
}
$items = $input['items'];
$paymentMethod = $input['payment_method'] ?? 'cash';
$notes = $input['notes'] ?? '';
$customerId = $input['customer_id'] ?? null;
$customerEmail = $input['customer_email'] ?? null;
$discountAmount = floatval($input['discount'] ?? 0);
$couponCode = $input['coupon_code'] ?? null;
// Calculate totals
$subtotal = 0;
$orderItems = [];
foreach ($items as $item) {
// Verify product exists and has stock
$product = db()->fetch(
"SELECT product_id, name, price, sale_price, stock FROM products WHERE product_id = :id AND is_active = 1",
['id' => $item['product_id']]
);
if (!$product) {
jsonResponse(['error' => 'Product not found: ' . $item['name']], 400);
}
if ($product['stock'] < $item['quantity']) {
jsonResponse(['error' => 'Insufficient stock for: ' . $product['name']], 400);
}
$price = $product['sale_price'] ?? $product['price'];
$lineTotal = $price * $item['quantity'];
$subtotal += $lineTotal;
$orderItems[] = [
'product_id' => $product['product_id'],
'name' => $product['name'],
'price' => $price,
'quantity' => $item['quantity'],
'total' => $lineTotal
];
}
// Apply coupon if provided
$couponDiscount = 0;
if ($couponCode) {
$coupon = db()->fetch(
"SELECT * FROM coupons WHERE code = :code AND is_active = 1
AND (starts_at IS NULL OR starts_at <= NOW())
AND (expires_at IS NULL OR expires_at > NOW())
AND (max_uses IS NULL OR times_used < max_uses)",
['code' => strtoupper($couponCode)]
);
if ($coupon) {
if ($coupon['min_order_amount'] && $subtotal < $coupon['min_order_amount']) {
// Coupon minimum not met, ignore
} else {
if ($coupon['discount_type'] === 'percentage') {
$couponDiscount = $subtotal * ($coupon['discount_value'] / 100);
} else {
$couponDiscount = min($coupon['discount_value'], $subtotal);
}
// Update coupon usage
db()->query("UPDATE coupons SET times_used = times_used + 1 WHERE coupon_id = :id",
['id' => $coupon['coupon_id']]);
}
}
}
// Calculate final total
$discount = $discountAmount + $couponDiscount;
$taxRate = 0; // Adjust based on settings
$tax = ($subtotal - $discount) * $taxRate;
$total = $subtotal - $discount + $tax;
// Handle wallet payment
$walletUsed = 0;
if ($paymentMethod === 'wallet' && $customerId) {
$customer = db()->fetch(
"SELECT wallet_balance FROM customers WHERE customer_id = :id",
['id' => $customerId]
);
if (!$customer || $customer['wallet_balance'] < $total) {
jsonResponse(['error' => 'Insufficient wallet balance'], 400);
}
$walletUsed = $total;
// Deduct from wallet
db()->query(
"UPDATE customers SET wallet_balance = wallet_balance - :amount WHERE customer_id = :id",
['amount' => $walletUsed, 'id' => $customerId]
);
// Log wallet transaction
$newBalance = $customer['wallet_balance'] - $walletUsed;
db()->insert('wallet_transactions', [
'transaction_id' => generateId('wt_'),
'customer_id' => $customerId,
'amount' => -$walletUsed,
'balance_after' => $newBalance,
'type' => 'purchase',
'description' => 'POS Purchase'
]);
}
// Generate order
$orderId = generateId('ord_');
$orderNumber = generateOrderNumber();
try {
// Create order
db()->insert('orders', [
'order_id' => $orderId,
'order_number' => $orderNumber,
'customer_id' => $customerId,
'customer_email' => $customerEmail ?? 'pos@store.local',
'customer_name' => $input['customer_name'] ?? 'POS Customer',
'items' => json_encode($orderItems),
'subtotal' => $subtotal,
'tax' => $tax,
'discount' => $discount,
'wallet_amount_used' => $walletUsed,
'total' => $total,
'payment_method' => $paymentMethod,
'payment_status' => 'paid',
'order_status' => 'confirmed',
'notes' => $notes,
'is_pos_order' => 1
]);
// Insert order items
foreach ($orderItems as $item) {
db()->insert('order_items', [
'order_id' => $orderId,
'product_id' => $item['product_id'],
'name' => $item['name'],
'price' => $item['price'],
'quantity' => $item['quantity'],
'total' => $item['total']
]);
// Update stock
db()->query(
"UPDATE products SET stock = stock - :qty WHERE product_id = :id",
['qty' => $item['quantity'], 'id' => $item['product_id']]
);
}
// Award reward points if customer
if ($customerId) {
$pointsEarned = floor($total); // 1 point per dollar
db()->query(
"UPDATE customers SET reward_points = reward_points + :points WHERE customer_id = :id",
['points' => $pointsEarned, 'id' => $customerId]
);
}
jsonResponse([
'success' => true,
'order_id' => $orderId,
'order_number' => $orderNumber,
'total' => $total,
'items' => $orderItems
]);
} catch (Exception $e) {
jsonResponse(['error' => 'Failed to create order: ' . $e->getMessage()], 500);
}
@@ -0,0 +1,93 @@
<?php
/**
* Tom's Java Jive - Products API
*/
require_once __DIR__ . '/../includes/functions.php';
header('Content-Type: application/json');
$method = $_SERVER['REQUEST_METHOD'];
if ($method === 'GET') {
$productId = $_GET['id'] ?? null;
if ($productId) {
// Get single product
$product = db()->fetch(
"SELECT * FROM products WHERE product_id = :id AND is_active = 1",
['id' => $productId]
);
if (!$product) {
jsonResponse(['error' => 'Product not found'], 404);
}
$product['images'] = json_decode($product['images'] ?? '[]', true);
$product['tags'] = json_decode($product['tags'] ?? '[]', true);
unset($product['id']);
// Get reviews
$reviews = db()->fetchAll(
"SELECT review_id, customer_name, rating, title, comment, is_verified_purchase, created_at
FROM reviews WHERE product_id = :id AND is_approved = 1 ORDER BY created_at DESC",
['id' => $productId]
);
$product['reviews'] = $reviews;
$product['average_rating'] = !empty($reviews)
? round(array_sum(array_column($reviews, 'rating')) / count($reviews), 1)
: 0;
jsonResponse($product);
} else {
// Get products list
$category = $_GET['category'] ?? '';
$search = $_GET['search'] ?? '';
$featured = $_GET['featured'] ?? '';
$limit = min(100, intval($_GET['limit'] ?? 20));
$offset = intval($_GET['offset'] ?? 0);
$where = ['is_active = 1'];
$params = [];
if ($category) {
$where[] = 'category = :category';
$params['category'] = $category;
}
if ($search) {
$where[] = '(name LIKE :search OR description LIKE :search)';
$params['search'] = '%' . $search . '%';
}
if ($featured === '1') {
$where[] = 'is_featured = 1';
}
$whereClause = implode(' AND ', $where);
$products = db()->fetchAll(
"SELECT product_id, name, description, price, sale_price, category, images, stock, is_featured
FROM products WHERE {$whereClause}
ORDER BY is_featured DESC, created_at DESC
LIMIT :limit OFFSET :offset",
array_merge($params, ['limit' => $limit, 'offset' => $offset])
);
foreach ($products as &$p) {
$p['images'] = json_decode($p['images'] ?? '[]', true);
}
$total = db()->count('products', $whereClause, $params);
jsonResponse([
'products' => $products,
'total' => $total,
'limit' => $limit,
'offset' => $offset
]);
}
}
jsonResponse(['error' => 'Method not allowed'], 405);
@@ -0,0 +1,83 @@
<?php
/**
* Tom's Java Jive - Push Subscription API
*/
header('Content-Type: application/json');
require_once __DIR__ . '/../includes/functions.php';
require_once __DIR__ . '/../includes/auth.php';
$method = $_SERVER['REQUEST_METHOD'];
$input = json_decode(file_get_contents('php://input'), true);
switch ($method) {
case 'POST':
// Subscribe to push notifications
$endpoint = $input['endpoint'] ?? '';
$p256dh = $input['keys']['p256dh'] ?? '';
$auth = $input['keys']['auth'] ?? '';
if (empty($endpoint) || empty($p256dh) || empty($auth)) {
jsonResponse(['error' => 'Invalid subscription data'], 400);
}
$customerId = null;
if (CustomerAuth::isLoggedIn()) {
$customerId = CustomerAuth::getUser()['customer_id'];
}
// Check if subscription already exists
$existing = db()->fetch(
"SELECT id FROM push_subscriptions WHERE endpoint = :endpoint",
['endpoint' => $endpoint]
);
if ($existing) {
// Update existing
db()->query(
"UPDATE push_subscriptions SET
customer_id = :cid, p256dh_key = :p256dh, auth_key = :auth,
is_active = 1, updated_at = NOW()
WHERE endpoint = :endpoint",
['cid' => $customerId, 'p256dh' => $p256dh, 'auth' => $auth, 'endpoint' => $endpoint]
);
} else {
// Create new
db()->insert('push_subscriptions', [
'customer_id' => $customerId,
'endpoint' => $endpoint,
'p256dh_key' => $p256dh,
'auth_key' => $auth,
'is_active' => 1
]);
}
jsonResponse(['success' => true, 'message' => 'Subscribed to notifications']);
break;
case 'DELETE':
// Unsubscribe
$endpoint = $input['endpoint'] ?? '';
if (empty($endpoint)) {
jsonResponse(['error' => 'Endpoint required'], 400);
}
db()->query(
"UPDATE push_subscriptions SET is_active = 0 WHERE endpoint = :endpoint",
['endpoint' => $endpoint]
);
jsonResponse(['success' => true, 'message' => 'Unsubscribed from notifications']);
break;
case 'GET':
// Get VAPID public key
require_once __DIR__ . '/../includes/push.php';
jsonResponse(['publicKey' => pushNotify()->getPublicKey()]);
break;
default:
jsonResponse(['error' => 'Method not allowed'], 405);
}
@@ -0,0 +1,34 @@
<?php
/**
* Tom's Java Jive - Redeem Gift Card API
*/
header('Content-Type: application/json');
require_once __DIR__ . '/../includes/functions.php';
require_once __DIR__ . '/../includes/auth.php';
require_once __DIR__ . '/../includes/loyalty.php';
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
jsonResponse(['error' => 'Method not allowed'], 405);
}
if (!CustomerAuth::isLoggedIn()) {
jsonResponse(['error' => 'Please log in to redeem a gift card'], 401);
}
$customer = CustomerAuth::getFullUser();
$input = json_decode(file_get_contents('php://input'), true);
$result = loyalty()->redeemGiftCardToWallet($customer['customer_id'], $input['code'] ?? '');
if (!$result['success']) {
jsonResponse(['error' => $result['error']], 400);
}
jsonResponse([
'success' => true,
'amount' => $result['amount'],
'new_balance' => $result['new_balance'],
'message' => formatCurrency($result['amount']) . ' has been added to your wallet!'
]);
@@ -0,0 +1,30 @@
<?php
/**
* Tom's Java Jive - Customer Search API
*/
header('Content-Type: application/json');
require_once __DIR__ . '/../includes/functions.php';
require_once __DIR__ . '/../includes/auth.php';
if (!AdminAuth::isLoggedIn()) {
jsonResponse(['error' => 'Unauthorized'], 401);
}
$query = $_GET['q'] ?? '';
if (strlen($query) < 2) {
jsonResponse([]);
}
$customers = db()->fetchAll(
"SELECT customer_id, email, name, phone, wallet_balance, reward_points
FROM customers
WHERE (email LIKE :q1 OR name LIKE :q2 OR phone LIKE :q3) AND is_active = 1
ORDER BY name ASC
LIMIT 20",
['q1' => '%' . $query . '%', 'q2' => '%' . $query . '%', 'q3' => '%' . $query . '%']
);
jsonResponse($customers);
@@ -0,0 +1,66 @@
<?php
/**
* Tom's Java Jive - Submit Review API
*/
header('Content-Type: application/json');
require_once __DIR__ . '/../includes/functions.php';
require_once __DIR__ . '/../includes/auth.php';
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
jsonResponse(['error' => 'Method not allowed'], 405);
}
if (!CustomerAuth::isLoggedIn()) {
jsonResponse(['error' => 'Please log in to submit a review'], 401);
}
$customer = CustomerAuth::getFullUser();
$input = json_decode(file_get_contents('php://input'), true);
$productId = $input['product_id'] ?? '';
$rating = intval($input['rating'] ?? 0);
$title = trim($input['title'] ?? '');
$content = trim($input['content'] ?? '');
if (empty($productId) || $rating < 1 || $rating > 5 || empty($content)) {
jsonResponse(['error' => 'Invalid input. Rating and review content are required.'], 400);
}
// Check if product exists
$product = db()->fetch("SELECT product_id FROM products WHERE product_id = :id", ['id' => $productId]);
if (!$product) {
jsonResponse(['error' => 'Product not found'], 404);
}
// Check if already reviewed
$existingReview = db()->fetch(
"SELECT review_id FROM reviews WHERE customer_id = :cid AND product_id = :pid",
['cid' => $customer['customer_id'], 'pid' => $productId]
);
if ($existingReview) {
jsonResponse(['error' => 'You have already reviewed this product'], 400);
}
// Create review
$reviewId = generateId('rev_');
db()->insert('reviews', [
'review_id' => $reviewId,
'product_id' => $productId,
'customer_id' => $customer['customer_id'],
'customer_name' => $customer['name'] ?? explode('@', $customer['email'])[0],
'customer_email' => $customer['email'],
'rating' => $rating,
'title' => $title,
'comment' => $content,
'is_approved' => 0 // Reviews require admin approval
]);
jsonResponse([
'success' => true,
'message' => 'Review submitted successfully. It will be visible after approval.',
'review_id' => $reviewId
]);
@@ -0,0 +1,75 @@
<?php
/**
* Tom's Java Jive - Newsletter Subscribe API
*/
require_once __DIR__ . '/../includes/functions.php';
header('Content-Type: application/json');
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
jsonResponse(['error' => 'Method not allowed'], 405);
}
$input = json_decode(file_get_contents('php://input'), true);
$email = trim($input['email'] ?? $_POST['email'] ?? '');
if (empty($email)) {
jsonResponse(['error' => 'Email is required'], 400);
}
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
jsonResponse(['error' => 'Please enter a valid email address'], 400);
}
// Check if already subscribed
$existing = db()->fetch(
"SELECT id FROM email_subscribers WHERE email = :email",
['email' => strtolower($email)]
);
if ($existing) {
jsonResponse(['error' => 'This email is already subscribed'], 400);
}
// Add subscriber
try {
db()->insert('email_subscribers', [
'email' => strtolower($email),
'source' => 'website',
'is_active' => 1
]);
// Send welcome email
$html = <<<HTML
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
<div style="background: #8B4513; color: white; padding: 20px; text-align: center;">
<h1 style="margin: 0;">Tom's Java Jive</h1>
</div>
<div style="padding: 30px; background: #FDFBF7;">
<h2>Welcome to the Java Jive Family!</h2>
<p>Thanks for subscribing to our newsletter. You'll be the first to know about:</p>
<ul>
<li>New coffee releases</li>
<li>Exclusive discounts and promotions</li>
<li>Brewing tips and recipes</li>
<li>Behind-the-scenes at our roastery</li>
</ul>
<p>As a thank you, enjoy <strong>10% off</strong> your first order with code: <strong>WELCOME10</strong></p>
<p style="text-align: center; margin-top: 20px;">
<a href="https://tomsjavajive.com/shop.php" style="background: #E86A33; color: white; padding: 12px 24px; text-decoration: none; border-radius: 6px;">Shop Now</a>
</p>
</div>
<div style="padding: 20px; text-align: center; color: #666; font-size: 12px;">
<p>Tom's Java Jive | Premium Coffee</p>
</div>
</div>
HTML;
sendEmail($email, 'Welcome to Tom\'s Java Jive!', $html);
jsonResponse(['success' => true, 'message' => 'Successfully subscribed!']);
} catch (Exception $e) {
jsonResponse(['error' => 'Subscription failed. Please try again.'], 500);
}
@@ -0,0 +1,65 @@
<?php
/**
* Tom's Java Jive - Test Notification API
*/
header('Content-Type: application/json');
require_once __DIR__ . '/../includes/functions.php';
require_once __DIR__ . '/../includes/email.php';
require_once __DIR__ . '/../includes/sms.php';
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
jsonResponse(['error' => 'Method not allowed'], 405);
}
$input = json_decode(file_get_contents('php://input'), true);
$type = $input['type'] ?? '';
$recipient = $input['recipient'] ?? '';
if (empty($recipient)) {
jsonResponse(['error' => 'Recipient is required'], 400);
}
switch ($type) {
case 'email':
if (!filter_var($recipient, FILTER_VALIDATE_EMAIL)) {
jsonResponse(['error' => 'Invalid email address'], 400);
}
$result = emailService()->send(
$recipient,
"Test Email from Tom's Java Jive",
"<div style='font-family: Arial, sans-serif; max-width: 500px; margin: 0 auto; padding: 20px;'>
<h2 style='color: #FF5E1A;'>Test Email</h2>
<p>This is a test email from your Tom's Java Jive store.</p>
<p>If you received this, your SendGrid integration is working correctly!</p>
<p style='color: #666; font-size: 0.9em; margin-top: 30px;'>
Sent at: " . date('Y-m-d H:i:s') . "
</p>
</div>"
);
if ($result['success']) {
jsonResponse(['success' => true, 'message' => 'Test email sent to ' . $recipient]);
} else {
jsonResponse(['success' => false, 'error' => $result['error'] ?? 'Failed to send email']);
}
break;
case 'sms':
$result = sendSMS()->send(
$recipient,
"Tom's Java Jive: This is a test message. If you received this, your Twilio integration is working! Sent at " . date('g:i A')
);
if ($result['success']) {
jsonResponse(['success' => true, 'message' => 'Test SMS sent to ' . $recipient]);
} else {
jsonResponse(['success' => false, 'error' => $result['error'] ?? 'Failed to send SMS']);
}
break;
default:
jsonResponse(['error' => 'Invalid notification type'], 400);
}
@@ -0,0 +1,59 @@
<?php
/**
* Tom's Java Jive - Coupon Validation API
*/
header('Content-Type: application/json');
require_once __DIR__ . '/../includes/functions.php';
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
jsonResponse(['error' => 'Method not allowed'], 405);
}
$input = json_decode(file_get_contents('php://input'), true);
$code = strtoupper(trim($input['code'] ?? ''));
$subtotal = floatval($input['subtotal'] ?? 0);
if (empty($code)) {
jsonResponse(['error' => 'Coupon code required'], 400);
}
$coupon = db()->fetch(
"SELECT * FROM coupons WHERE code = :code AND is_active = 1",
['code' => $code]
);
if (!$coupon) {
jsonResponse(['error' => 'Invalid coupon code']);
}
// Check if expired
if ($coupon['expires_at'] && strtotime($coupon['expires_at']) < time()) {
jsonResponse(['error' => 'Coupon has expired']);
}
// Check if not started yet
if ($coupon['starts_at'] && strtotime($coupon['starts_at']) > time()) {
jsonResponse(['error' => 'Coupon is not yet active']);
}
// Check usage limit
if ($coupon['max_uses'] && $coupon['times_used'] >= $coupon['max_uses']) {
jsonResponse(['error' => 'Coupon usage limit reached']);
}
// Check minimum order
if ($coupon['min_order_amount'] && $subtotal < $coupon['min_order_amount']) {
jsonResponse(['error' => 'Minimum order of ' . formatCurrency($coupon['min_order_amount']) . ' required']);
}
jsonResponse([
'valid' => true,
'code' => $coupon['code'],
'type' => $coupon['discount_type'],
'value' => floatval($coupon['discount_value']),
'description' => $coupon['discount_type'] === 'percentage'
? $coupon['discount_value'] . '% off'
: formatCurrency($coupon['discount_value']) . ' off'
]);
@@ -0,0 +1,69 @@
<?php
/**
* Tom's Java Jive - Square Webhook Handler
*/
require_once __DIR__ . '/../includes/functions.php';
require_once __DIR__ . '/../includes/square.php';
require_once __DIR__ . '/../includes/loyalty.php';
require_once __DIR__ . '/../includes/email.php';
header('Content-Type: application/json');
$payload = file_get_contents('php://input');
$sigHeader = $_SERVER['HTTP_X_SQUARE_HMACSHA256_SIGNATURE'] ?? '';
$notificationUrl = SITE_URL . '/api/webhook.php';
if (empty(SQUARE_WEBHOOK_SIGNATURE_KEY) || SQUARE_WEBHOOK_SIGNATURE_KEY === 'REPLACE_ME') {
error_log('Square webhook signature key not configured - rejecting webhook');
http_response_code(400);
exit();
}
if (!verifySquareWebhookSignature($payload, $sigHeader, $notificationUrl, SQUARE_WEBHOOK_SIGNATURE_KEY)) {
error_log('Square webhook signature verification failed');
http_response_code(400);
exit();
}
$event = json_decode($payload, true);
if (!$event) {
http_response_code(400);
exit();
}
$eventType = $event['type'] ?? '';
switch ($eventType) {
case 'payment.updated':
case 'payment.created':
$payment = $event['data']['object']['payment'] ?? [];
$paymentId = $payment['id'] ?? '';
$status = $payment['status'] ?? '';
$orderId = $payment['reference_id'] ?? null;
if ($paymentId && $status) {
markSquarePaymentResult($paymentId, $orderId, $status);
}
break;
case 'refund.updated':
case 'refund.created':
$refund = $event['data']['object']['refund'] ?? [];
$paymentId = $refund['payment_id'] ?? '';
$status = $refund['status'] ?? '';
if ($status === 'COMPLETED' && $paymentId) {
db()->update('orders',
[
'payment_status' => 'refunded',
'order_status' => 'refunded',
],
'square_payment_id = :pid',
['pid' => $paymentId]
);
}
break;
}
http_response_code(200);
echo json_encode(['received' => true]);
@@ -0,0 +1,95 @@
<?php
/**
* Tom's Java Jive - Wishlist API
*/
header('Content-Type: application/json');
require_once __DIR__ . '/../includes/functions.php';
require_once __DIR__ . '/../includes/auth.php';
if (!CustomerAuth::isLoggedIn()) {
jsonResponse(['error' => 'Please log in to manage your wishlist'], 401);
}
$customer = CustomerAuth::getFullUser();
$input = json_decode(file_get_contents('php://input'), true);
$action = $input['action'] ?? $_GET['action'] ?? '';
$productId = $input['product_id'] ?? $_GET['product_id'] ?? '';
switch ($action) {
case 'add':
if (empty($productId)) {
jsonResponse(['error' => 'Product ID required'], 400);
}
// Check if product exists
$product = db()->fetch("SELECT product_id FROM products WHERE product_id = :id", ['id' => $productId]);
if (!$product) {
jsonResponse(['error' => 'Product not found'], 404);
}
// Check if already in wishlist
$existing = db()->fetch(
"SELECT id FROM wishlist WHERE customer_id = :cid AND product_id = :pid",
['cid' => $customer['customer_id'], 'pid' => $productId]
);
if ($existing) {
jsonResponse(['success' => true, 'message' => 'Already in wishlist']);
}
db()->insert('wishlist', [
'customer_id' => $customer['customer_id'],
'product_id' => $productId
]);
jsonResponse(['success' => true, 'message' => 'Added to wishlist']);
break;
case 'remove':
if (empty($productId)) {
jsonResponse(['error' => 'Product ID required'], 400);
}
db()->query(
"DELETE FROM wishlist WHERE customer_id = :cid AND product_id = :pid",
['cid' => $customer['customer_id'], 'pid' => $productId]
);
jsonResponse(['success' => true, 'message' => 'Removed from wishlist']);
break;
case 'check':
if (empty($productId)) {
jsonResponse(['error' => 'Product ID required'], 400);
}
$exists = db()->fetch(
"SELECT id FROM wishlist WHERE customer_id = :cid AND product_id = :pid",
['cid' => $customer['customer_id'], 'pid' => $productId]
);
jsonResponse(['in_wishlist' => (bool)$exists]);
break;
case 'list':
$items = db()->fetchAll(
"SELECT p.product_id, p.name, p.slug, p.price, p.sale_price, p.images, p.stock
FROM wishlist w
JOIN products p ON w.product_id = p.product_id
WHERE w.customer_id = :id
ORDER BY w.created_at DESC",
['id' => $customer['customer_id']]
);
foreach ($items as &$item) {
$item['images'] = json_decode($item['images'] ?? '[]', true);
}
jsonResponse(['items' => $items]);
break;
default:
jsonResponse(['error' => 'Invalid action'], 400);
}