mirror of
https://github.com/myronblair/tomsjavajive
synced 2026-07-29 09:42:29 -05:00
Compare commits
10 Commits
a4ebf26a48
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| c6a382a378 | |||
| f84a754370 | |||
| 3c8e4d1dbc | |||
| bdd0bd6afa | |||
| 9771d53b19 | |||
| e02436618c | |||
| cfbae6e945 | |||
| 30daef5f74 | |||
| 11edf3394f | |||
| a0b8bf2a09 |
@@ -1,105 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
import subprocess, os
|
|
||||||
|
|
||||||
BASE = '/home/tomsjavajive.com/public_html'
|
|
||||||
|
|
||||||
# ── Fix 1: Remove wholesale and careers from footer ──────────
|
|
||||||
print("[1] Fixing footer...")
|
|
||||||
f = BASE + '/includes/footer.php'
|
|
||||||
with open(f) as fh:
|
|
||||||
c = fh.read()
|
|
||||||
|
|
||||||
before = len(c)
|
|
||||||
c = c.replace(' <li><a href="/wholesale.php">Wholesale</a></li>\n', '')
|
|
||||||
c = c.replace(' <li><a href="/careers.php">Careers</a></li>\n', '')
|
|
||||||
# Also try without leading spaces
|
|
||||||
c = c.replace('<li><a href="/wholesale.php">Wholesale</a></li>\n', '')
|
|
||||||
c = c.replace('<li><a href="/careers.php">Careers</a></li>\n', '')
|
|
||||||
|
|
||||||
with open(f, 'w') as fh:
|
|
||||||
fh.write(c)
|
|
||||||
removed = before - len(c)
|
|
||||||
print(f" Footer: removed {removed} chars (wholesale + careers)")
|
|
||||||
|
|
||||||
# ── Fix 2: Check account pages for errors ───────────────────
|
|
||||||
print("\n[2] Checking account pages...")
|
|
||||||
for page in ['rewards', 'wishlist', 'reviews']:
|
|
||||||
path = f'{BASE}/account/{page}.php'
|
|
||||||
r = subprocess.run(
|
|
||||||
['/usr/local/lsws/lsphp85/bin/php', '-d', 'display_errors=1',
|
|
||||||
'-d', 'error_reporting=32767', path],
|
|
||||||
capture_output=True, text=True
|
|
||||||
)
|
|
||||||
output = r.stdout + r.stderr
|
|
||||||
errors = [l for l in output.split('\n')
|
|
||||||
if any(x in l for x in ['Fatal','Error','Undefined','undefined','Call to'])]
|
|
||||||
if errors:
|
|
||||||
print(f" {page}.php ERRORS:")
|
|
||||||
for e in errors[:3]:
|
|
||||||
print(f" {e[:120]}")
|
|
||||||
else:
|
|
||||||
# Check if page has actual HTML content
|
|
||||||
has_html = '<' in r.stdout and len(r.stdout) > 100
|
|
||||||
print(f" {page}.php: {'OK - has content' if has_html else 'WARNING - minimal output'}")
|
|
||||||
|
|
||||||
# ── Fix 3: Check admin customers Modal ──────────────────────
|
|
||||||
print("\n[3] Checking admin customers...")
|
|
||||||
f2 = BASE + '/admin/customers.php'
|
|
||||||
with open(f2) as fh:
|
|
||||||
c2 = fh.read()
|
|
||||||
|
|
||||||
print(f" Modal.open present: {'Modal.open' in c2}")
|
|
||||||
print(f" openCustomerModal present: {'openCustomerModal' in c2}")
|
|
||||||
print(f" admin.js exists: {os.path.exists(BASE+'/admin/assets/admin.js')}")
|
|
||||||
|
|
||||||
# Check if admin.js has Modal object
|
|
||||||
if os.path.exists(BASE+'/admin/assets/admin.js'):
|
|
||||||
with open(BASE+'/admin/assets/admin.js') as fh:
|
|
||||||
js = fh.read()
|
|
||||||
print(f" Modal object in admin.js: {'Modal' in js}")
|
|
||||||
print(f" admin.js size: {len(js)} chars")
|
|
||||||
else:
|
|
||||||
print(" admin.js MISSING - this is why customer modal doesn't work!")
|
|
||||||
# Create a basic Modal object
|
|
||||||
modal_js = """
|
|
||||||
// Modal helper
|
|
||||||
const Modal = {
|
|
||||||
open: function(id) {
|
|
||||||
const el = document.getElementById(id);
|
|
||||||
if (el) { el.style.display = 'flex'; el.classList.add('active'); }
|
|
||||||
},
|
|
||||||
close: function(id) {
|
|
||||||
const el = document.getElementById(id);
|
|
||||||
if (el) { el.style.display = 'none'; el.classList.remove('active'); }
|
|
||||||
}
|
|
||||||
};
|
|
||||||
document.addEventListener('keydown', function(e) {
|
|
||||||
if (e.key === 'Escape') {
|
|
||||||
document.querySelectorAll('.modal-overlay.active').forEach(m => {
|
|
||||||
m.style.display = 'none'; m.classList.remove('active');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
document.addEventListener('click', function(e) {
|
|
||||||
if (e.target.classList.contains('modal-overlay')) {
|
|
||||||
e.target.style.display = 'none'; e.target.classList.remove('active');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
"""
|
|
||||||
os.makedirs(BASE+'/admin/assets', exist_ok=True)
|
|
||||||
with open(BASE+'/admin/assets/admin.js', 'w') as fh:
|
|
||||||
fh.write(modal_js)
|
|
||||||
print(" Created admin.js with Modal object")
|
|
||||||
|
|
||||||
# ── Fix 4: Check account page includes ─────────────────────
|
|
||||||
print("\n[4] Checking account page structure...")
|
|
||||||
for page in ['rewards', 'wishlist', 'reviews']:
|
|
||||||
path = f'{BASE}/account/{page}.php'
|
|
||||||
with open(path) as fh:
|
|
||||||
content = fh.read()
|
|
||||||
has_auth = 'CustomerAuth::require' in content
|
|
||||||
has_header = "require_once" in content and 'header' in content
|
|
||||||
has_footer = 'footer' in content
|
|
||||||
print(f" {page}.php: auth={has_auth}, header={has_header}, footer={has_footer}, size={len(content)}")
|
|
||||||
|
|
||||||
print("\nDone!")
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
-- Migration: Add wishlist table and addresses column to customers
|
|
||||||
-- Run this in phpMyAdmin to add the new features
|
|
||||||
|
|
||||||
-- Add addresses column to customers table
|
|
||||||
ALTER TABLE `customers` ADD COLUMN `addresses` JSON DEFAULT NULL AFTER `billing_address`;
|
|
||||||
|
|
||||||
-- Add preferences column to customers table
|
|
||||||
ALTER TABLE `customers` ADD COLUMN `preferences` JSON DEFAULT NULL AFTER `addresses`;
|
|
||||||
|
|
||||||
-- Add is_active column to customers table if not exists
|
|
||||||
ALTER TABLE `customers` ADD COLUMN `is_active` TINYINT(1) DEFAULT 1 AFTER `preferences`;
|
|
||||||
|
|
||||||
-- Create wishlist table
|
|
||||||
CREATE TABLE IF NOT EXISTS `wishlist` (
|
|
||||||
`id` INT AUTO_INCREMENT PRIMARY KEY,
|
|
||||||
`customer_id` VARCHAR(50) NOT NULL,
|
|
||||||
`product_id` VARCHAR(50) NOT NULL,
|
|
||||||
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
UNIQUE KEY `unique_wishlist` (`customer_id`, `product_id`),
|
|
||||||
INDEX `idx_customer` (`customer_id`),
|
|
||||||
INDEX `idx_product` (`product_id`),
|
|
||||||
FOREIGN KEY (`customer_id`) REFERENCES `customers`(`customer_id`) ON DELETE CASCADE,
|
|
||||||
FOREIGN KEY (`product_id`) REFERENCES `products`(`product_id`) ON DELETE CASCADE
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
||||||
|
|
||||||
-- Add reorder_level column to products if not exists
|
|
||||||
ALTER TABLE `products` ADD COLUMN `reorder_level` INT DEFAULT 10 AFTER `low_stock_threshold`;
|
|
||||||
|
|
||||||
-- Add slug column to products if not exists
|
|
||||||
ALTER TABLE `products` ADD COLUMN `slug` VARCHAR(255) DEFAULT NULL AFTER `name`;
|
|
||||||
ALTER TABLE `products` ADD INDEX `idx_slug` (`slug`);
|
|
||||||
|
|
||||||
-- Update existing products to have slugs based on name
|
|
||||||
UPDATE `products` SET `slug` = LOWER(REPLACE(REPLACE(REPLACE(`name`, ' ', '-'), "'", ''), '"', '')) WHERE `slug` IS NULL;
|
|
||||||
|
|
||||||
-- Add is_pos_order to orders table
|
|
||||||
ALTER TABLE `orders` ADD COLUMN `is_pos_order` TINYINT(1) DEFAULT 0 AFTER `notes`;
|
|
||||||
@@ -1,96 +0,0 @@
|
|||||||
-- Migration: Add tables for push notifications, loyalty program, and integration settings
|
|
||||||
-- Run this in phpMyAdmin
|
|
||||||
|
|
||||||
-- Push notification subscriptions
|
|
||||||
CREATE TABLE IF NOT EXISTS `push_subscriptions` (
|
|
||||||
`id` INT AUTO_INCREMENT PRIMARY KEY,
|
|
||||||
`customer_id` VARCHAR(50) DEFAULT NULL,
|
|
||||||
`endpoint` TEXT NOT NULL,
|
|
||||||
`p256dh_key` VARCHAR(255) NOT NULL,
|
|
||||||
`auth_key` VARCHAR(255) NOT NULL,
|
|
||||||
`is_active` TINYINT(1) DEFAULT 1,
|
|
||||||
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
|
||||||
INDEX `idx_customer` (`customer_id`),
|
|
||||||
INDEX `idx_active` (`is_active`)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
||||||
|
|
||||||
-- Push notifications queue
|
|
||||||
CREATE TABLE IF NOT EXISTS `push_notifications` (
|
|
||||||
`id` INT AUTO_INCREMENT PRIMARY KEY,
|
|
||||||
`notification_id` VARCHAR(50) NOT NULL UNIQUE,
|
|
||||||
`subscription_endpoint` TEXT NOT NULL,
|
|
||||||
`payload` TEXT NOT NULL,
|
|
||||||
`status` ENUM('pending', 'sent', 'failed') DEFAULT 'pending',
|
|
||||||
`error_message` TEXT DEFAULT NULL,
|
|
||||||
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
`sent_at` TIMESTAMP NULL DEFAULT NULL,
|
|
||||||
INDEX `idx_status` (`status`)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
||||||
|
|
||||||
-- Loyalty transactions history
|
|
||||||
CREATE TABLE IF NOT EXISTS `loyalty_transactions` (
|
|
||||||
`id` INT AUTO_INCREMENT PRIMARY KEY,
|
|
||||||
`transaction_id` VARCHAR(50) NOT NULL UNIQUE,
|
|
||||||
`customer_id` VARCHAR(50) NOT NULL,
|
|
||||||
`points` INT NOT NULL,
|
|
||||||
`type` ENUM('earn', 'redeem', 'tier_upgrade', 'birthday_bonus', 'referral_bonus', 'referral_welcome', 'adjustment', 'expiry') NOT NULL,
|
|
||||||
`description` VARCHAR(255) DEFAULT NULL,
|
|
||||||
`reference_amount` DECIMAL(10,2) DEFAULT NULL,
|
|
||||||
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
INDEX `idx_customer` (`customer_id`),
|
|
||||||
INDEX `idx_type` (`type`),
|
|
||||||
INDEX `idx_created` (`created_at`),
|
|
||||||
FOREIGN KEY (`customer_id`) REFERENCES `customers`(`customer_id`) ON DELETE CASCADE
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
||||||
|
|
||||||
-- Add lifetime_points and loyalty_tier to customers
|
|
||||||
ALTER TABLE `customers`
|
|
||||||
ADD COLUMN IF NOT EXISTS `lifetime_points` INT DEFAULT 0 AFTER `reward_points`,
|
|
||||||
ADD COLUMN IF NOT EXISTS `loyalty_tier` ENUM('bronze', 'silver', 'gold', 'platinum') DEFAULT 'bronze' AFTER `lifetime_points`,
|
|
||||||
ADD COLUMN IF NOT EXISTS `birthday` DATE DEFAULT NULL AFTER `loyalty_tier`,
|
|
||||||
ADD COLUMN IF NOT EXISTS `referral_code` VARCHAR(20) DEFAULT NULL AFTER `birthday`,
|
|
||||||
ADD COLUMN IF NOT EXISTS `referred_by` VARCHAR(50) DEFAULT NULL AFTER `referral_code`;
|
|
||||||
|
|
||||||
-- Add unique index on referral_code
|
|
||||||
ALTER TABLE `customers` ADD UNIQUE INDEX IF NOT EXISTS `idx_referral_code` (`referral_code`);
|
|
||||||
|
|
||||||
-- Update settings table with integration keys (INSERT IGNORE to not overwrite existing)
|
|
||||||
INSERT IGNORE INTO `settings` (`setting_key`, `setting_value`, `updated_at`) VALUES
|
|
||||||
('sendgrid_api_key', '', NOW()),
|
|
||||||
('sendgrid_from_email', 'noreply@tomsjavajive.com', NOW()),
|
|
||||||
('sendgrid_from_name', 'Tom''s Java Jive', NOW()),
|
|
||||||
('twilio_account_sid', '', NOW()),
|
|
||||||
('twilio_auth_token', '', NOW()),
|
|
||||||
('twilio_phone_number', '', NOW()),
|
|
||||||
('vapid_public_key', '', NOW()),
|
|
||||||
('vapid_private_key', '', NOW()),
|
|
||||||
('loyalty_enabled', '1', NOW()),
|
|
||||||
('email_notifications_enabled', '1', NOW()),
|
|
||||||
('sms_notifications_enabled', '0', NOW()),
|
|
||||||
('push_notifications_enabled', '1', NOW());
|
|
||||||
|
|
||||||
-- Add Stripe checkout session column to orders
|
|
||||||
ALTER TABLE `orders` ADD COLUMN IF NOT EXISTS `stripe_checkout_session` VARCHAR(255) DEFAULT NULL AFTER `stripe_payment_intent`;
|
|
||||||
|
|
||||||
-- Payment transactions table for tracking payment attempts
|
|
||||||
CREATE TABLE IF NOT EXISTS `payment_transactions` (
|
|
||||||
`id` INT AUTO_INCREMENT PRIMARY KEY,
|
|
||||||
`transaction_id` VARCHAR(50) NOT NULL UNIQUE,
|
|
||||||
`order_id` VARCHAR(50) NOT NULL,
|
|
||||||
`customer_id` VARCHAR(50) DEFAULT NULL,
|
|
||||||
`amount` DECIMAL(10,2) NOT NULL,
|
|
||||||
`currency` VARCHAR(3) DEFAULT 'USD',
|
|
||||||
`payment_method` VARCHAR(50) DEFAULT 'stripe',
|
|
||||||
`stripe_session_id` VARCHAR(255) DEFAULT NULL,
|
|
||||||
`stripe_payment_intent` VARCHAR(255) DEFAULT NULL,
|
|
||||||
`status` ENUM('initiated', 'pending', 'processing', 'succeeded', 'failed', 'cancelled', 'refunded') DEFAULT 'initiated',
|
|
||||||
`metadata` JSON DEFAULT NULL,
|
|
||||||
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
|
||||||
INDEX `idx_order` (`order_id`),
|
|
||||||
INDEX `idx_customer` (`customer_id`),
|
|
||||||
INDEX `idx_status` (`status`),
|
|
||||||
INDEX `idx_stripe_session` (`stripe_session_id`)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
||||||
|
|
||||||
@@ -1,402 +0,0 @@
|
|||||||
-- Tom's Java Jive - MySQL Database Schema
|
|
||||||
-- Version: 1.0
|
|
||||||
-- Compatible with MySQL 8.0+
|
|
||||||
|
|
||||||
SET SQL_MODE = "NO_AUTO_VALUE_ON_ZERO";
|
|
||||||
SET AUTOCOMMIT = 0;
|
|
||||||
START TRANSACTION;
|
|
||||||
SET time_zone = "+00:00";
|
|
||||||
|
|
||||||
-- --------------------------------------------------------
|
|
||||||
-- Database Schema for Tom's Java Jive
|
|
||||||
-- NOTE: Database must already exist in cPanel
|
|
||||||
-- Select your database in phpMyAdmin before importing
|
|
||||||
-- --------------------------------------------------------
|
|
||||||
|
|
||||||
-- --------------------------------------------------------
|
|
||||||
-- Table: settings
|
|
||||||
-- --------------------------------------------------------
|
|
||||||
CREATE TABLE `settings` (
|
|
||||||
`id` INT AUTO_INCREMENT PRIMARY KEY,
|
|
||||||
`setting_key` VARCHAR(100) NOT NULL UNIQUE,
|
|
||||||
`setting_value` JSON,
|
|
||||||
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
||||||
|
|
||||||
-- --------------------------------------------------------
|
|
||||||
-- Table: admin_users
|
|
||||||
-- --------------------------------------------------------
|
|
||||||
CREATE TABLE `admin_users` (
|
|
||||||
`id` INT AUTO_INCREMENT PRIMARY KEY,
|
|
||||||
`user_id` VARCHAR(50) NOT NULL UNIQUE,
|
|
||||||
`email` VARCHAR(255) NOT NULL UNIQUE,
|
|
||||||
`password_hash` VARCHAR(255) DEFAULT NULL,
|
|
||||||
`name` VARCHAR(255) DEFAULT NULL,
|
|
||||||
`picture` VARCHAR(500) DEFAULT NULL,
|
|
||||||
`is_admin` TINYINT(1) DEFAULT 1,
|
|
||||||
`is_master` TINYINT(1) DEFAULT 0,
|
|
||||||
`permissions` JSON DEFAULT NULL,
|
|
||||||
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
`last_login` TIMESTAMP NULL,
|
|
||||||
INDEX `idx_email` (`email`)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
||||||
|
|
||||||
-- --------------------------------------------------------
|
|
||||||
-- Table: customers
|
|
||||||
-- --------------------------------------------------------
|
|
||||||
CREATE TABLE `customers` (
|
|
||||||
`id` INT AUTO_INCREMENT PRIMARY KEY,
|
|
||||||
`customer_id` VARCHAR(50) NOT NULL UNIQUE,
|
|
||||||
`email` VARCHAR(255) NOT NULL UNIQUE,
|
|
||||||
`password_hash` VARCHAR(255) DEFAULT NULL,
|
|
||||||
`name` VARCHAR(255) DEFAULT NULL,
|
|
||||||
`phone` VARCHAR(50) DEFAULT NULL,
|
|
||||||
`shipping_address` JSON DEFAULT NULL,
|
|
||||||
`billing_address` JSON DEFAULT NULL,
|
|
||||||
`wallet_balance` DECIMAL(10,2) DEFAULT 0.00,
|
|
||||||
`reward_points` INT DEFAULT 0,
|
|
||||||
`is_guest` TINYINT(1) DEFAULT 0,
|
|
||||||
`created_via` VARCHAR(50) DEFAULT 'web',
|
|
||||||
`email_verified` TINYINT(1) DEFAULT 0,
|
|
||||||
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
|
||||||
INDEX `idx_email` (`email`),
|
|
||||||
INDEX `idx_customer_id` (`customer_id`)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
||||||
|
|
||||||
-- --------------------------------------------------------
|
|
||||||
-- Table: products
|
|
||||||
-- --------------------------------------------------------
|
|
||||||
CREATE TABLE `products` (
|
|
||||||
`id` INT AUTO_INCREMENT PRIMARY KEY,
|
|
||||||
`product_id` VARCHAR(50) NOT NULL UNIQUE,
|
|
||||||
`name` VARCHAR(255) NOT NULL,
|
|
||||||
`description` TEXT,
|
|
||||||
`price` DECIMAL(10,2) NOT NULL,
|
|
||||||
`sale_price` DECIMAL(10,2) DEFAULT NULL,
|
|
||||||
`cost_price` DECIMAL(10,2) DEFAULT NULL,
|
|
||||||
`sku` VARCHAR(100) DEFAULT NULL,
|
|
||||||
`barcode` VARCHAR(100) DEFAULT NULL,
|
|
||||||
`category` VARCHAR(100) DEFAULT NULL,
|
|
||||||
`tags` JSON DEFAULT NULL,
|
|
||||||
`images` JSON DEFAULT NULL,
|
|
||||||
`stock` INT DEFAULT 0,
|
|
||||||
`low_stock_threshold` INT DEFAULT 10,
|
|
||||||
`weight` DECIMAL(10,2) DEFAULT NULL,
|
|
||||||
`dimensions` JSON DEFAULT NULL,
|
|
||||||
`is_active` TINYINT(1) DEFAULT 1,
|
|
||||||
`is_featured` TINYINT(1) DEFAULT 0,
|
|
||||||
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
|
||||||
INDEX `idx_product_id` (`product_id`),
|
|
||||||
INDEX `idx_category` (`category`),
|
|
||||||
INDEX `idx_is_active` (`is_active`),
|
|
||||||
FULLTEXT INDEX `idx_search` (`name`, `description`)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
||||||
|
|
||||||
-- --------------------------------------------------------
|
|
||||||
-- Table: orders
|
|
||||||
-- --------------------------------------------------------
|
|
||||||
CREATE TABLE `orders` (
|
|
||||||
`id` INT AUTO_INCREMENT PRIMARY KEY,
|
|
||||||
`order_id` VARCHAR(50) NOT NULL UNIQUE,
|
|
||||||
`order_number` VARCHAR(20) NOT NULL UNIQUE,
|
|
||||||
`customer_id` VARCHAR(50) DEFAULT NULL,
|
|
||||||
`customer_email` VARCHAR(255) NOT NULL,
|
|
||||||
`customer_name` VARCHAR(255) DEFAULT NULL,
|
|
||||||
`customer_phone` VARCHAR(50) DEFAULT NULL,
|
|
||||||
`items` JSON NOT NULL,
|
|
||||||
`subtotal` DECIMAL(10,2) NOT NULL,
|
|
||||||
`shipping_cost` DECIMAL(10,2) DEFAULT 0.00,
|
|
||||||
`tax` DECIMAL(10,2) DEFAULT 0.00,
|
|
||||||
`discount` DECIMAL(10,2) DEFAULT 0.00,
|
|
||||||
`gift_card_discount` DECIMAL(10,2) DEFAULT 0.00,
|
|
||||||
`wallet_amount_used` DECIMAL(10,2) DEFAULT 0.00,
|
|
||||||
`total` DECIMAL(10,2) NOT NULL,
|
|
||||||
`shipping_address` JSON DEFAULT NULL,
|
|
||||||
`billing_address` JSON DEFAULT NULL,
|
|
||||||
`shipping_method` VARCHAR(50) DEFAULT NULL,
|
|
||||||
`payment_method` VARCHAR(50) DEFAULT NULL,
|
|
||||||
`payment_status` ENUM('pending', 'paid', 'failed', 'refunded', 'partially_refunded') DEFAULT 'pending',
|
|
||||||
`order_status` ENUM('pending', 'confirmed', 'processing', 'shipped', 'delivered', 'cancelled', 'refunded') DEFAULT 'pending',
|
|
||||||
`stripe_session_id` VARCHAR(255) DEFAULT NULL,
|
|
||||||
`stripe_payment_intent` VARCHAR(255) DEFAULT NULL,
|
|
||||||
`tracking_number` VARCHAR(100) DEFAULT NULL,
|
|
||||||
`tracking_url` VARCHAR(500) DEFAULT NULL,
|
|
||||||
`notes` TEXT DEFAULT NULL,
|
|
||||||
`is_pos_order` TINYINT(1) DEFAULT 0,
|
|
||||||
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
|
||||||
INDEX `idx_order_id` (`order_id`),
|
|
||||||
INDEX `idx_customer_id` (`customer_id`),
|
|
||||||
INDEX `idx_customer_email` (`customer_email`),
|
|
||||||
INDEX `idx_order_status` (`order_status`),
|
|
||||||
INDEX `idx_payment_status` (`payment_status`),
|
|
||||||
INDEX `idx_created_at` (`created_at`)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
||||||
|
|
||||||
-- --------------------------------------------------------
|
|
||||||
-- Table: order_items (normalized for reporting)
|
|
||||||
-- --------------------------------------------------------
|
|
||||||
CREATE TABLE `order_items` (
|
|
||||||
`id` INT AUTO_INCREMENT PRIMARY KEY,
|
|
||||||
`order_id` VARCHAR(50) NOT NULL,
|
|
||||||
`product_id` VARCHAR(50) NOT NULL,
|
|
||||||
`name` VARCHAR(255) NOT NULL,
|
|
||||||
`price` DECIMAL(10,2) NOT NULL,
|
|
||||||
`quantity` INT NOT NULL,
|
|
||||||
`total` DECIMAL(10,2) NOT NULL,
|
|
||||||
INDEX `idx_order_id` (`order_id`),
|
|
||||||
INDEX `idx_product_id` (`product_id`),
|
|
||||||
FOREIGN KEY (`order_id`) REFERENCES `orders`(`order_id`) ON DELETE CASCADE
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
||||||
|
|
||||||
-- --------------------------------------------------------
|
|
||||||
-- Table: gift_cards
|
|
||||||
-- --------------------------------------------------------
|
|
||||||
CREATE TABLE `gift_cards` (
|
|
||||||
`id` INT AUTO_INCREMENT PRIMARY KEY,
|
|
||||||
`gift_card_id` VARCHAR(50) NOT NULL UNIQUE,
|
|
||||||
`code` VARCHAR(20) NOT NULL UNIQUE,
|
|
||||||
`initial_balance` DECIMAL(10,2) NOT NULL,
|
|
||||||
`current_balance` DECIMAL(10,2) NOT NULL,
|
|
||||||
`purchaser_email` VARCHAR(255) DEFAULT NULL,
|
|
||||||
`recipient_email` VARCHAR(255) DEFAULT NULL,
|
|
||||||
`recipient_name` VARCHAR(255) DEFAULT NULL,
|
|
||||||
`message` TEXT DEFAULT NULL,
|
|
||||||
`is_active` TINYINT(1) DEFAULT 1,
|
|
||||||
`expires_at` TIMESTAMP NULL,
|
|
||||||
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
INDEX `idx_code` (`code`),
|
|
||||||
INDEX `idx_is_active` (`is_active`)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
||||||
|
|
||||||
-- --------------------------------------------------------
|
|
||||||
-- Table: gift_card_transactions
|
|
||||||
-- --------------------------------------------------------
|
|
||||||
CREATE TABLE `gift_card_transactions` (
|
|
||||||
`id` INT AUTO_INCREMENT PRIMARY KEY,
|
|
||||||
`gift_card_id` VARCHAR(50) NOT NULL,
|
|
||||||
`order_id` VARCHAR(50) DEFAULT NULL,
|
|
||||||
`amount` DECIMAL(10,2) NOT NULL,
|
|
||||||
`balance_after` DECIMAL(10,2) NOT NULL,
|
|
||||||
`type` ENUM('purchase', 'redemption', 'refund') NOT NULL,
|
|
||||||
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
INDEX `idx_gift_card_id` (`gift_card_id`)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
||||||
|
|
||||||
-- --------------------------------------------------------
|
|
||||||
-- Table: wallet_transactions
|
|
||||||
-- --------------------------------------------------------
|
|
||||||
CREATE TABLE `wallet_transactions` (
|
|
||||||
`id` INT AUTO_INCREMENT PRIMARY KEY,
|
|
||||||
`transaction_id` VARCHAR(50) NOT NULL UNIQUE,
|
|
||||||
`customer_id` VARCHAR(50) NOT NULL,
|
|
||||||
`amount` DECIMAL(10,2) NOT NULL,
|
|
||||||
`balance_after` DECIMAL(10,2) NOT NULL,
|
|
||||||
`type` ENUM('deposit', 'withdrawal', 'purchase', 'refund', 'reward') NOT NULL,
|
|
||||||
`description` VARCHAR(255) DEFAULT NULL,
|
|
||||||
`order_id` VARCHAR(50) DEFAULT NULL,
|
|
||||||
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
INDEX `idx_customer_id` (`customer_id`),
|
|
||||||
INDEX `idx_type` (`type`)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
||||||
|
|
||||||
-- --------------------------------------------------------
|
|
||||||
-- Table: reviews
|
|
||||||
-- --------------------------------------------------------
|
|
||||||
CREATE TABLE `reviews` (
|
|
||||||
`id` INT AUTO_INCREMENT PRIMARY KEY,
|
|
||||||
`review_id` VARCHAR(50) NOT NULL UNIQUE,
|
|
||||||
`product_id` VARCHAR(50) NOT NULL,
|
|
||||||
`customer_id` VARCHAR(50) DEFAULT NULL,
|
|
||||||
`customer_name` VARCHAR(255) NOT NULL,
|
|
||||||
`customer_email` VARCHAR(255) NOT NULL,
|
|
||||||
`rating` INT NOT NULL CHECK (rating >= 1 AND rating <= 5),
|
|
||||||
`title` VARCHAR(255) DEFAULT NULL,
|
|
||||||
`comment` TEXT,
|
|
||||||
`is_verified_purchase` TINYINT(1) DEFAULT 0,
|
|
||||||
`is_approved` TINYINT(1) DEFAULT 0,
|
|
||||||
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
INDEX `idx_product_id` (`product_id`),
|
|
||||||
INDEX `idx_is_approved` (`is_approved`)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
||||||
|
|
||||||
-- --------------------------------------------------------
|
|
||||||
-- Table: email_campaigns
|
|
||||||
-- --------------------------------------------------------
|
|
||||||
CREATE TABLE `email_campaigns` (
|
|
||||||
`id` INT AUTO_INCREMENT PRIMARY KEY,
|
|
||||||
`campaign_id` VARCHAR(50) NOT NULL UNIQUE,
|
|
||||||
`name` VARCHAR(255) NOT NULL,
|
|
||||||
`subject` VARCHAR(255) NOT NULL,
|
|
||||||
`content` TEXT NOT NULL,
|
|
||||||
`recipient_type` ENUM('all', 'customers_only', 'subscribers_only') DEFAULT 'all',
|
|
||||||
`status` ENUM('draft', 'scheduled', 'sent', 'cancelled') DEFAULT 'draft',
|
|
||||||
`scheduled_at` TIMESTAMP NULL,
|
|
||||||
`sent_at` TIMESTAMP NULL,
|
|
||||||
`recipients_count` INT DEFAULT 0,
|
|
||||||
`opened_count` INT DEFAULT 0,
|
|
||||||
`clicked_count` INT DEFAULT 0,
|
|
||||||
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
INDEX `idx_status` (`status`)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
||||||
|
|
||||||
-- --------------------------------------------------------
|
|
||||||
-- Table: email_subscribers
|
|
||||||
-- --------------------------------------------------------
|
|
||||||
CREATE TABLE `email_subscribers` (
|
|
||||||
`id` INT AUTO_INCREMENT PRIMARY KEY,
|
|
||||||
`email` VARCHAR(255) NOT NULL UNIQUE,
|
|
||||||
`name` VARCHAR(255) DEFAULT NULL,
|
|
||||||
`is_active` TINYINT(1) DEFAULT 1,
|
|
||||||
`source` VARCHAR(50) DEFAULT 'website',
|
|
||||||
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
INDEX `idx_is_active` (`is_active`)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
||||||
|
|
||||||
-- --------------------------------------------------------
|
|
||||||
-- Table: abandoned_carts
|
|
||||||
-- --------------------------------------------------------
|
|
||||||
CREATE TABLE `abandoned_carts` (
|
|
||||||
`id` INT AUTO_INCREMENT PRIMARY KEY,
|
|
||||||
`cart_id` VARCHAR(50) NOT NULL UNIQUE,
|
|
||||||
`customer_id` VARCHAR(50) DEFAULT NULL,
|
|
||||||
`customer_email` VARCHAR(255) DEFAULT NULL,
|
|
||||||
`items` JSON NOT NULL,
|
|
||||||
`subtotal` DECIMAL(10,2) NOT NULL,
|
|
||||||
`recovery_email_sent` TINYINT(1) DEFAULT 0,
|
|
||||||
`recovered` TINYINT(1) DEFAULT 0,
|
|
||||||
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
|
||||||
INDEX `idx_customer_email` (`customer_email`),
|
|
||||||
INDEX `idx_recovered` (`recovered`)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
||||||
|
|
||||||
-- --------------------------------------------------------
|
|
||||||
-- Table: referrals
|
|
||||||
-- --------------------------------------------------------
|
|
||||||
CREATE TABLE `referrals` (
|
|
||||||
`id` INT AUTO_INCREMENT PRIMARY KEY,
|
|
||||||
`referral_id` VARCHAR(50) NOT NULL UNIQUE,
|
|
||||||
`referrer_customer_id` VARCHAR(50) NOT NULL,
|
|
||||||
`referral_code` VARCHAR(20) NOT NULL UNIQUE,
|
|
||||||
`referred_customer_id` VARCHAR(50) DEFAULT NULL,
|
|
||||||
`referred_email` VARCHAR(255) DEFAULT NULL,
|
|
||||||
`status` ENUM('pending', 'completed', 'expired') DEFAULT 'pending',
|
|
||||||
`reward_amount` DECIMAL(10,2) DEFAULT 5.00,
|
|
||||||
`reward_given` TINYINT(1) DEFAULT 0,
|
|
||||||
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
INDEX `idx_referral_code` (`referral_code`),
|
|
||||||
INDEX `idx_referrer` (`referrer_customer_id`)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
||||||
|
|
||||||
-- --------------------------------------------------------
|
|
||||||
-- Table: visitor_sessions
|
|
||||||
-- --------------------------------------------------------
|
|
||||||
CREATE TABLE `visitor_sessions` (
|
|
||||||
`id` INT AUTO_INCREMENT PRIMARY KEY,
|
|
||||||
`session_id` VARCHAR(100) NOT NULL UNIQUE,
|
|
||||||
`visitor_id` VARCHAR(50) NOT NULL,
|
|
||||||
`ip_address` VARCHAR(45) DEFAULT NULL,
|
|
||||||
`user_agent` TEXT DEFAULT NULL,
|
|
||||||
`current_page` VARCHAR(500) DEFAULT NULL,
|
|
||||||
`referrer` VARCHAR(500) DEFAULT NULL,
|
|
||||||
`country` VARCHAR(100) DEFAULT NULL,
|
|
||||||
`city` VARCHAR(100) DEFAULT NULL,
|
|
||||||
`is_active` TINYINT(1) DEFAULT 1,
|
|
||||||
`page_views` INT DEFAULT 1,
|
|
||||||
`started_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
`last_activity` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
|
||||||
INDEX `idx_is_active` (`is_active`),
|
|
||||||
INDEX `idx_last_activity` (`last_activity`)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
||||||
|
|
||||||
-- --------------------------------------------------------
|
|
||||||
-- Table: categories
|
|
||||||
-- --------------------------------------------------------
|
|
||||||
CREATE TABLE `categories` (
|
|
||||||
`id` INT AUTO_INCREMENT PRIMARY KEY,
|
|
||||||
`category_id` VARCHAR(50) NOT NULL UNIQUE,
|
|
||||||
`name` VARCHAR(255) NOT NULL,
|
|
||||||
`slug` VARCHAR(255) NOT NULL UNIQUE,
|
|
||||||
`description` TEXT DEFAULT NULL,
|
|
||||||
`image` VARCHAR(500) DEFAULT NULL,
|
|
||||||
`parent_id` VARCHAR(50) DEFAULT NULL,
|
|
||||||
`sort_order` INT DEFAULT 0,
|
|
||||||
`is_active` TINYINT(1) DEFAULT 1,
|
|
||||||
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
|
||||||
INDEX `idx_slug` (`slug`),
|
|
||||||
INDEX `idx_is_active` (`is_active`)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
||||||
|
|
||||||
-- --------------------------------------------------------
|
|
||||||
-- Table: coupons
|
|
||||||
-- --------------------------------------------------------
|
|
||||||
CREATE TABLE `coupons` (
|
|
||||||
`id` INT AUTO_INCREMENT PRIMARY KEY,
|
|
||||||
`coupon_id` VARCHAR(50) NOT NULL UNIQUE,
|
|
||||||
`code` VARCHAR(50) NOT NULL UNIQUE,
|
|
||||||
`discount_type` ENUM('percentage', 'fixed') NOT NULL DEFAULT 'percentage',
|
|
||||||
`discount_value` DECIMAL(10,2) NOT NULL,
|
|
||||||
`min_order_amount` DECIMAL(10,2) DEFAULT NULL,
|
|
||||||
`max_uses` INT DEFAULT NULL,
|
|
||||||
`times_used` INT DEFAULT 0,
|
|
||||||
`is_active` TINYINT(1) DEFAULT 1,
|
|
||||||
`starts_at` TIMESTAMP NULL,
|
|
||||||
`expires_at` TIMESTAMP NULL,
|
|
||||||
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
INDEX `idx_code` (`code`),
|
|
||||||
INDEX `idx_is_active` (`is_active`)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
||||||
|
|
||||||
-- --------------------------------------------------------
|
|
||||||
-- Table: password_reset_tokens
|
|
||||||
-- --------------------------------------------------------
|
|
||||||
CREATE TABLE `password_reset_tokens` (
|
|
||||||
`id` INT AUTO_INCREMENT PRIMARY KEY,
|
|
||||||
`email` VARCHAR(255) NOT NULL,
|
|
||||||
`token` VARCHAR(255) NOT NULL,
|
|
||||||
`user_type` ENUM('admin', 'customer') NOT NULL,
|
|
||||||
`expires_at` TIMESTAMP NOT NULL,
|
|
||||||
`used` TINYINT(1) DEFAULT 0,
|
|
||||||
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
INDEX `idx_token` (`token`),
|
|
||||||
INDEX `idx_email` (`email`)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
||||||
|
|
||||||
-- --------------------------------------------------------
|
|
||||||
-- Table: sessions
|
|
||||||
-- --------------------------------------------------------
|
|
||||||
CREATE TABLE `sessions` (
|
|
||||||
`id` INT AUTO_INCREMENT PRIMARY KEY,
|
|
||||||
`session_id` VARCHAR(128) NOT NULL UNIQUE,
|
|
||||||
`user_id` VARCHAR(50) DEFAULT NULL,
|
|
||||||
`user_type` ENUM('admin', 'customer') DEFAULT NULL,
|
|
||||||
`data` TEXT,
|
|
||||||
`ip_address` VARCHAR(45) DEFAULT NULL,
|
|
||||||
`user_agent` VARCHAR(255) DEFAULT NULL,
|
|
||||||
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
`expires_at` TIMESTAMP NOT NULL,
|
|
||||||
INDEX `idx_session_id` (`session_id`),
|
|
||||||
INDEX `idx_expires_at` (`expires_at`)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
||||||
|
|
||||||
-- --------------------------------------------------------
|
|
||||||
-- Insert default settings
|
|
||||||
-- --------------------------------------------------------
|
|
||||||
INSERT INTO `settings` (`setting_key`, `setting_value`) VALUES
|
|
||||||
('store_name', '"Tom\'s Java Jive"'),
|
|
||||||
('store_email', '"support@tomsjavajive.com"'),
|
|
||||||
('store_phone', '""'),
|
|
||||||
('store_address', '""'),
|
|
||||||
('currency', '"USD"'),
|
|
||||||
('currency_symbol', '"$"'),
|
|
||||||
('tax_rate', '0'),
|
|
||||||
('shipping', '{"flat_rate_enabled": true, "flat_rate_amount": 5.99, "free_shipping_threshold": 50, "weight_based_enabled": false}'),
|
|
||||||
('payment', '{"stripe_enabled": true, "paypal_enabled": false, "cod_enabled": false}'),
|
|
||||||
('email', '{"sendgrid_api_key": "", "sender_email": "noreply@tomsjavajive.com", "sender_name": "Tom\'s Java Jive"}');
|
|
||||||
|
|
||||||
COMMIT;
|
|
||||||
@@ -8,9 +8,10 @@ RewriteEngine On
|
|||||||
# includes/*.php, and .git/ were all still reachable live despite the
|
# includes/*.php, and .git/ were all still reachable live despite the
|
||||||
# RedirectMatch rules further down) — only mod_rewrite matching on REQUEST_URI
|
# RedirectMatch rules further down) — only mod_rewrite matching on REQUEST_URI
|
||||||
# is confirmed to actually work here. This must come before other rewrites.
|
# is confirmed to actually work here. This must come before other rewrites.
|
||||||
RewriteCond %{REQUEST_URI} ^/(\.git|config/|includes/.*\.php|install/|db/)
|
RewriteCond %{REQUEST_URI} ^/(\.git|config/|includes/.*\.php|install/|!install!!/|db/)
|
||||||
RewriteRule .* - [F,L]
|
RewriteRule .* - [F,L]
|
||||||
RewriteCond %{REQUEST_URI} \.sql$
|
RewriteCond %{REQUEST_URI} \.sql$ [OR]
|
||||||
|
RewriteCond %{REQUEST_URI} \.py$
|
||||||
RewriteRule .* - [F,L]
|
RewriteRule .* - [F,L]
|
||||||
|
|
||||||
# Force HTTPS (uncomment in production)
|
# Force HTTPS (uncomment in production)
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['redeem_points'])) {
|
|||||||
redirect('/account/rewards.php');
|
redirect('/account/rewards.php');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$extraHead = '<link rel="stylesheet" href="/assets/css/account.css?v='. filemtime(__DIR__ . '/../assets/css/account.css') .'">';
|
||||||
require_once __DIR__ . '/../includes/header.php';
|
require_once __DIR__ . '/../includes/header.php';
|
||||||
require_once __DIR__ . '/includes/sidebar.php';
|
require_once __DIR__ . '/includes/sidebar.php';
|
||||||
?>
|
?>
|
||||||
|
|||||||
+119
-3
@@ -5,6 +5,7 @@
|
|||||||
ob_start();
|
ob_start();
|
||||||
$pageTitle = 'Order Details';
|
$pageTitle = 'Order Details';
|
||||||
require_once __DIR__ . '/includes/header.php';
|
require_once __DIR__ . '/includes/header.php';
|
||||||
|
require_once __DIR__ . '/../includes/square.php';
|
||||||
|
|
||||||
$orderId = $_GET['id'] ?? '';
|
$orderId = $_GET['id'] ?? '';
|
||||||
|
|
||||||
@@ -53,12 +54,87 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($action === 'process_refund') {
|
||||||
|
$refundAmount = (float) ($_POST['refund_amount'] ?? 0);
|
||||||
|
$reason = trim($_POST['refund_reason'] ?? '');
|
||||||
|
|
||||||
|
if ($order['payment_method'] !== 'square' || empty($order['square_payment_id'])) {
|
||||||
|
setFlash('error', 'This order has no Square payment to refund.');
|
||||||
|
header('Location: /admin/order.php?id=' . $orderId);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
if ($order['payment_status'] !== 'paid' && $order['payment_status'] !== 'partially_refunded') {
|
||||||
|
setFlash('error', 'Only paid orders can be refunded.');
|
||||||
|
header('Location: /admin/order.php?id=' . $orderId);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
if ($refundAmount <= 0 || $refundAmount > (float) $order['total']) {
|
||||||
|
setFlash('error', 'Invalid refund amount.');
|
||||||
|
header('Location: /admin/order.php?id=' . $orderId);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$result = squareRefundPayment($order['square_payment_id'], $refundAmount, $reason ?: ('Order #' . $order['order_number']));
|
||||||
|
$refund = $result['refund'] ?? [];
|
||||||
|
$refundStatus = $refund['status'] ?? '';
|
||||||
|
|
||||||
|
if (in_array($refundStatus, ['PENDING', 'COMPLETED'], true)) {
|
||||||
|
$isFullRefund = $refundAmount >= (float) $order['total'];
|
||||||
|
$newPaymentStatus = $isFullRefund ? 'refunded' : 'partially_refunded';
|
||||||
|
|
||||||
|
db()->update('orders',
|
||||||
|
['payment_status' => $newPaymentStatus, 'order_status' => $isFullRefund ? 'refunded' : $order['order_status']],
|
||||||
|
'order_id = :id',
|
||||||
|
['id' => $orderId]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Restore any wallet credit that was applied, proportionally on a full refund
|
||||||
|
if ($isFullRefund && !empty($order['wallet_amount_used']) && (float) $order['wallet_amount_used'] > 0 && !empty($order['customer_id'])) {
|
||||||
|
$walletAmount = (float) $order['wallet_amount_used'];
|
||||||
|
$cust = db()->fetch("SELECT wallet_balance FROM customers WHERE customer_id = :id", ['id' => $order['customer_id']]);
|
||||||
|
if ($cust) {
|
||||||
|
$newBalance = (float) $cust['wallet_balance'] + $walletAmount;
|
||||||
|
db()->update('customers', ['wallet_balance' => $newBalance], 'customer_id = :id', ['id' => $order['customer_id']]);
|
||||||
|
db()->insert('wallet_transactions', [
|
||||||
|
'transaction_id' => generateId('wt_'),
|
||||||
|
'customer_id' => $order['customer_id'],
|
||||||
|
'amount' => $walletAmount,
|
||||||
|
'balance_after' => $newBalance,
|
||||||
|
'type' => 'refund',
|
||||||
|
'description' => 'Refund for Order #' . $order['order_number'],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$existingNotes = $order['notes'] ?? '';
|
||||||
|
$newNote = '[' . date('M j, Y g:i A') . '] Refunded ' . formatCurrency($refundAmount)
|
||||||
|
. ' via Square (refund ID: ' . ($refund['id'] ?? 'unknown') . ')'
|
||||||
|
. ($reason ? ' — ' . $reason : '');
|
||||||
|
$allNotes = $existingNotes ? $existingNotes . "\n" . $newNote : $newNote;
|
||||||
|
db()->update('orders', ['notes' => $allNotes], 'order_id = :id', ['id' => $orderId]);
|
||||||
|
|
||||||
|
setFlash('success', formatCurrency($refundAmount) . ' refunded successfully.');
|
||||||
|
} else {
|
||||||
|
setFlash('error', 'Refund did not complete (status: ' . $refundStatus . ').');
|
||||||
|
}
|
||||||
|
} catch (Exception $e) {
|
||||||
|
setFlash('error', 'Refund failed: ' . $e->getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
header('Location: /admin/order.php?id=' . $orderId);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$items = json_decode($order['items'], true) ?? [];
|
$items = json_decode($order['items'], true) ?? [];
|
||||||
$shippingAddress = json_decode($order['shipping_address'], true) ?? [];
|
$shippingAddress = json_decode($order['shipping_address'], true) ?? [];
|
||||||
|
|
||||||
$statuses = ['pending', 'confirmed', 'processing', 'shipped', 'delivered', 'cancelled', 'refunded'];
|
$statuses = ['pending', 'confirmed', 'processing', 'shipped', 'delivered', 'cancelled', 'refunded'];
|
||||||
|
$canRefund = $order['payment_method'] === 'square'
|
||||||
|
&& !empty($order['square_payment_id'])
|
||||||
|
&& in_array($order['payment_status'], ['paid', 'partially_refunded'], true);
|
||||||
?>
|
?>
|
||||||
|
|
||||||
<div class="page-header">
|
<div class="page-header">
|
||||||
@@ -81,6 +157,9 @@ $statuses = ['pending', 'confirmed', 'processing', 'shipped', 'delivered', 'canc
|
|||||||
<?php if (hasFlash('success')): ?>
|
<?php if (hasFlash('success')): ?>
|
||||||
<div class="alert alert-success"><i class="fas fa-check-circle"></i> <?= getFlash('success') ?></div>
|
<div class="alert alert-success"><i class="fas fa-check-circle"></i> <?= getFlash('success') ?></div>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
<?php if (hasFlash('error')): ?>
|
||||||
|
<div class="alert alert-error"><i class="fas fa-exclamation-circle"></i> <?= getFlash('error') ?></div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
<div style="display: grid; grid-template-columns: 2fr 1fr; gap: 1.5rem;">
|
<div style="display: grid; grid-template-columns: 2fr 1fr; gap: 1.5rem;">
|
||||||
<!-- Main Column -->
|
<!-- Main Column -->
|
||||||
@@ -141,6 +220,12 @@ $statuses = ['pending', 'confirmed', 'processing', 'shipped', 'delivered', 'canc
|
|||||||
<td style="text-align: right; color: var(--admin-success);">-<?= formatCurrency($order['discount']) ?></td>
|
<td style="text-align: right; color: var(--admin-success);">-<?= formatCurrency($order['discount']) ?></td>
|
||||||
</tr>
|
</tr>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
<?php if (($order['wallet_amount_used'] ?? 0) > 0): ?>
|
||||||
|
<tr>
|
||||||
|
<td colspan="3" style="text-align: right;">Wallet / Gift Card</td>
|
||||||
|
<td style="text-align: right; color: var(--admin-success);">-<?= formatCurrency($order['wallet_amount_used']) ?></td>
|
||||||
|
</tr>
|
||||||
|
<?php endif; ?>
|
||||||
<tr style="font-size: 1.125rem; font-weight: 600;">
|
<tr style="font-size: 1.125rem; font-weight: 600;">
|
||||||
<td colspan="3" style="text-align: right;">Total</td>
|
<td colspan="3" style="text-align: right;">Total</td>
|
||||||
<td style="text-align: right;"><?= formatCurrency($order['total']) ?></td>
|
<td style="text-align: right;"><?= formatCurrency($order['total']) ?></td>
|
||||||
@@ -173,6 +258,32 @@ $statuses = ['pending', 'confirmed', 'processing', 'shipped', 'delivered', 'canc
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<?php if ($canRefund): ?>
|
||||||
|
<!-- Refund -->
|
||||||
|
<div class="admin-card">
|
||||||
|
<div class="admin-card-header">
|
||||||
|
<h3 class="admin-card-title">Process Refund</h3>
|
||||||
|
</div>
|
||||||
|
<div class="admin-card-body">
|
||||||
|
<form method="POST" onsubmit="return confirm('Refund ' + document.getElementById('refund_amount').value + ' via Square? This cannot be undone.');">
|
||||||
|
<input type="hidden" name="action" value="process_refund">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Amount</label>
|
||||||
|
<input type="number" step="0.01" min="0.01" max="<?= $order['total'] ?>" name="refund_amount" id="refund_amount" class="form-input" value="<?= $order['total'] ?>" required>
|
||||||
|
<small class="text-muted">Up to <?= formatCurrency($order['total']) ?> (full order total). Enter a lower amount for a partial refund.</small>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Reason (optional)</label>
|
||||||
|
<input type="text" name="refund_reason" class="form-input" placeholder="e.g. damaged item, customer request">
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-secondary btn-block">
|
||||||
|
<i class="fas fa-undo"></i> Refund via Square
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Sidebar -->
|
<!-- Sidebar -->
|
||||||
@@ -260,13 +371,18 @@ $statuses = ['pending', 'confirmed', 'processing', 'shipped', 'delivered', 'canc
|
|||||||
$paymentClass = match($order['payment_status']) {
|
$paymentClass = match($order['payment_status']) {
|
||||||
'paid' => 'success',
|
'paid' => 'success',
|
||||||
'failed' => 'error',
|
'failed' => 'error',
|
||||||
'refunded' => 'warning',
|
'refunded', 'partially_refunded' => 'warning',
|
||||||
default => 'primary'
|
default => 'primary'
|
||||||
};
|
};
|
||||||
?>
|
?>
|
||||||
<span class="badge badge-<?= $paymentClass ?>"><?= ucfirst($order['payment_status']) ?></span>
|
<span class="badge badge-<?= $paymentClass ?>"><?= ucfirst(str_replace('_', ' ', $order['payment_status'])) ?></span>
|
||||||
</div>
|
</div>
|
||||||
<?php if ($order['stripe_payment_intent']): ?>
|
<?php if (!empty($order['square_payment_id'])): ?>
|
||||||
|
<div style="display: flex; justify-content: space-between;">
|
||||||
|
<span>Square Payment ID</span>
|
||||||
|
<span class="text-muted" style="font-size: 0.8rem;"><?= htmlspecialchars(substr($order['square_payment_id'], 0, 20)) ?></span>
|
||||||
|
</div>
|
||||||
|
<?php elseif ($order['stripe_payment_intent']): ?>
|
||||||
<div style="display: flex; justify-content: space-between;">
|
<div style="display: flex; justify-content: space-between;">
|
||||||
<span>Stripe ID</span>
|
<span>Stripe ID</span>
|
||||||
<span class="text-muted" style="font-size: 0.8rem;"><?= htmlspecialchars(substr($order['stripe_payment_intent'], 0, 20)) ?>...</span>
|
<span class="text-muted" style="font-size: 0.8rem;"><?= htmlspecialchars(substr($order['stripe_payment_intent'], 0, 20)) ?>...</span>
|
||||||
|
|||||||
+4
-2
@@ -45,8 +45,10 @@ if ($status) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ($search) {
|
if ($search) {
|
||||||
$where[] = '(order_number LIKE :search OR customer_name LIKE :search OR customer_email LIKE :search)';
|
$where[] = '(order_number LIKE :search1 OR customer_name LIKE :search2 OR customer_email LIKE :search3)';
|
||||||
$params['search'] = '%' . $search . '%';
|
$params['search1'] = '%' . $search . '%';
|
||||||
|
$params['search2'] = '%' . $search . '%';
|
||||||
|
$params['search3'] = '%' . $search . '%';
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($dateFrom) {
|
if ($dateFrom) {
|
||||||
|
|||||||
+79
-2
@@ -21,6 +21,18 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||||||
setFlash('success', 'Stripe settings updated');
|
setFlash('success', 'Stripe settings updated');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($section === 'square') {
|
||||||
|
setSetting('payment_square', [
|
||||||
|
'enabled' => isset($_POST['square_enabled']),
|
||||||
|
'sandbox' => isset($_POST['square_sandbox']),
|
||||||
|
'app_id' => trim($_POST['square_app_id'] ?? ''),
|
||||||
|
'location_id' => trim($_POST['square_location_id'] ?? ''),
|
||||||
|
'access_token' => trim($_POST['square_access_token'] ?? ''),
|
||||||
|
'webhook_signature_key' => trim($_POST['square_webhook_signature_key'] ?? '')
|
||||||
|
]);
|
||||||
|
setFlash('success', 'Square settings updated');
|
||||||
|
}
|
||||||
|
|
||||||
if ($section === 'methods') {
|
if ($section === 'methods') {
|
||||||
setSetting('payment_methods', [
|
setSetting('payment_methods', [
|
||||||
'card' => isset($_POST['method_card']),
|
'card' => isset($_POST['method_card']),
|
||||||
@@ -43,6 +55,15 @@ $stripe = getSetting('payment_stripe', [
|
|||||||
'webhook_secret' => ''
|
'webhook_secret' => ''
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
$square = getSetting('payment_square', [
|
||||||
|
'enabled' => defined('PAYMENT_PROCESSOR') && PAYMENT_PROCESSOR === 'square',
|
||||||
|
'sandbox' => defined('SQUARE_ENV') && SQUARE_ENV === 'sandbox',
|
||||||
|
'app_id' => defined('SQUARE_APP_ID') ? SQUARE_APP_ID : '',
|
||||||
|
'location_id' => defined('SQUARE_LOCATION_ID') ? SQUARE_LOCATION_ID : '',
|
||||||
|
'access_token' => defined('SQUARE_ACCESS_TOKEN') ? SQUARE_ACCESS_TOKEN : '',
|
||||||
|
'webhook_signature_key' => defined('SQUARE_WEBHOOK_SIGNATURE_KEY') ? SQUARE_WEBHOOK_SIGNATURE_KEY : ''
|
||||||
|
]);
|
||||||
|
|
||||||
$methods = getSetting('payment_methods', [
|
$methods = getSetting('payment_methods', [
|
||||||
'card' => true,
|
'card' => true,
|
||||||
'cash' => true,
|
'cash' => true,
|
||||||
@@ -72,12 +93,68 @@ $methods = getSetting('payment_methods', [
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<!-- Stripe Settings -->
|
<!-- Square Settings -->
|
||||||
|
<form method="POST">
|
||||||
|
<input type="hidden" name="section" value="square">
|
||||||
|
<div class="admin-card">
|
||||||
|
<div class="admin-card-header">
|
||||||
|
<h3 class="admin-card-title"><i class="fas fa-square" style="color: #006AFF;"></i> Square</h3>
|
||||||
|
</div>
|
||||||
|
<div class="admin-card-body">
|
||||||
|
<div class="form-group">
|
||||||
|
<label style="display: flex; align-items: center; gap: 0.5rem; cursor: pointer;">
|
||||||
|
<input type="checkbox" name="square_enabled" <?= $square['enabled'] ? 'checked' : '' ?>>
|
||||||
|
Enable Square payments
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label style="display: flex; align-items: center; gap: 0.5rem; cursor: pointer;">
|
||||||
|
<input type="checkbox" name="square_sandbox" <?= $square['sandbox'] ? 'checked' : '' ?>>
|
||||||
|
Sandbox mode (use sandbox app/location/token)
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Application ID</label>
|
||||||
|
<input type="text" name="square_app_id" class="form-input"
|
||||||
|
value="<?= htmlspecialchars($square['app_id']) ?>"
|
||||||
|
placeholder="sq0idp-...">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Location ID</label>
|
||||||
|
<input type="text" name="square_location_id" class="form-input"
|
||||||
|
value="<?= htmlspecialchars($square['location_id']) ?>"
|
||||||
|
placeholder="L...">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Access Token</label>
|
||||||
|
<input type="password" name="square_access_token" class="form-input"
|
||||||
|
value="<?= htmlspecialchars($square['access_token']) ?>"
|
||||||
|
placeholder="EAAA...">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group mb-0">
|
||||||
|
<label class="form-label">Webhook Signature Key</label>
|
||||||
|
<input type="password" name="square_webhook_signature_key" class="form-input"
|
||||||
|
value="<?= htmlspecialchars($square['webhook_signature_key']) ?>"
|
||||||
|
placeholder="...">
|
||||||
|
<small class="text-muted">From the Square Developer Dashboard's webhook subscription for this site</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" class="btn btn-primary mt-2">Save Square Settings</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<!-- Stripe Settings (legacy) -->
|
||||||
<form method="POST">
|
<form method="POST">
|
||||||
<input type="hidden" name="section" value="stripe">
|
<input type="hidden" name="section" value="stripe">
|
||||||
<div class="admin-card">
|
<div class="admin-card">
|
||||||
<div class="admin-card-header">
|
<div class="admin-card-header">
|
||||||
<h3 class="admin-card-title"><i class="fab fa-stripe" style="color: #635BFF;"></i> Stripe</h3>
|
<h3 class="admin-card-title"><i class="fab fa-stripe" style="color: #635BFF;"></i> Stripe <small class="text-muted">(legacy)</small></h3>
|
||||||
</div>
|
</div>
|
||||||
<div class="admin-card-body">
|
<div class="admin-card-body">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
|
|||||||
@@ -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)
|
||||||
|
]);
|
||||||
@@ -1,129 +0,0 @@
|
|||||||
<?php
|
|
||||||
/**
|
|
||||||
* Tom's Java Jive - Create Stripe Checkout Session API
|
|
||||||
* Uses hosted checkout page (redirects to Stripe)
|
|
||||||
*/
|
|
||||||
|
|
||||||
require_once __DIR__ . '/../includes/functions.php';
|
|
||||||
require_once __DIR__ . '/../includes/stripe.php';
|
|
||||||
require_once __DIR__ . '/../includes/loyalty.php';
|
|
||||||
|
|
||||||
header('Content-Type: application/json');
|
|
||||||
|
|
||||||
// Only accept POST
|
|
||||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
|
||||||
jsonResponse(['error' => 'Method not allowed'], 405);
|
|
||||||
}
|
|
||||||
|
|
||||||
$input = json_decode(file_get_contents('php://input'), true);
|
|
||||||
$orderId = $input['order_id'] ?? '';
|
|
||||||
$originUrl = $input['origin_url'] ?? '';
|
|
||||||
|
|
||||||
if (empty($orderId)) {
|
|
||||||
jsonResponse(['error' => 'Order ID required'], 400);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (empty($originUrl)) {
|
|
||||||
$originUrl = SITE_URL;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get order
|
|
||||||
$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);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if Stripe is configured
|
|
||||||
if (!isStripeConfigured()) {
|
|
||||||
// Demo mode - simulate successful payment
|
|
||||||
db()->update('orders',
|
|
||||||
[
|
|
||||||
'payment_status' => 'paid',
|
|
||||||
'order_status' => 'confirmed',
|
|
||||||
'stripe_payment_intent' => 'demo_' . bin2hex(random_bytes(8))
|
|
||||||
],
|
|
||||||
'order_id = :id',
|
|
||||||
['id' => $orderId]
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!empty($order['customer_id'])) {
|
|
||||||
loyalty()->awardPoints(
|
|
||||||
$order['customer_id'],
|
|
||||||
(float) $order['total'],
|
|
||||||
'Order #' . $order['order_number'],
|
|
||||||
$orderId
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
jsonResponse([
|
|
||||||
'demo_mode' => true,
|
|
||||||
'message' => 'Payment simulated (Stripe not configured)',
|
|
||||||
'redirect' => '/order-confirmation.php?order=' . $orderId
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build line items from order
|
|
||||||
$items = json_decode($order['items'], true) ?? [];
|
|
||||||
$lineItems = [];
|
|
||||||
|
|
||||||
foreach ($items as $item) {
|
|
||||||
$lineItems[] = [
|
|
||||||
'name' => $item['name'],
|
|
||||||
'price' => floatval($item['price']),
|
|
||||||
'quantity' => intval($item['quantity']),
|
|
||||||
'currency' => 'usd'
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add shipping if applicable
|
|
||||||
if ($order['shipping_cost'] > 0) {
|
|
||||||
$lineItems[] = [
|
|
||||||
'name' => 'Shipping',
|
|
||||||
'price' => floatval($order['shipping_cost']),
|
|
||||||
'quantity' => 1,
|
|
||||||
'currency' => 'usd'
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build success/cancel URLs
|
|
||||||
$successUrl = rtrim($originUrl, '/') . '/order-confirmation.php?order=' . $orderId . '&session_id={CHECKOUT_SESSION_ID}';
|
|
||||||
$cancelUrl = rtrim($originUrl, '/') . '/payment.php?order=' . $orderId . '&cancelled=1';
|
|
||||||
|
|
||||||
try {
|
|
||||||
$session = stripe()->createCheckoutSession(
|
|
||||||
$lineItems,
|
|
||||||
$successUrl,
|
|
||||||
$cancelUrl,
|
|
||||||
[
|
|
||||||
'customer_email' => $order['customer_email'],
|
|
||||||
'metadata' => [
|
|
||||||
'order_id' => $orderId,
|
|
||||||
'order_number' => $order['order_number']
|
|
||||||
]
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
// Store checkout session ID
|
|
||||||
db()->update('orders',
|
|
||||||
['stripe_session_id' => $session['id']],
|
|
||||||
'order_id = :id',
|
|
||||||
['id' => $orderId]
|
|
||||||
);
|
|
||||||
|
|
||||||
jsonResponse([
|
|
||||||
'url' => $session['url'],
|
|
||||||
'session_id' => $session['id']
|
|
||||||
]);
|
|
||||||
|
|
||||||
} catch (Exception $e) {
|
|
||||||
error_log('Stripe Checkout error: ' . $e->getMessage());
|
|
||||||
jsonResponse(['error' => 'Failed to create checkout session: ' . $e->getMessage()], 500);
|
|
||||||
}
|
|
||||||
@@ -1,87 +0,0 @@
|
|||||||
<?php
|
|
||||||
/**
|
|
||||||
* Tom's Java Jive - Create Stripe Payment Intent API
|
|
||||||
* Uses cURL-based Stripe integration (no Composer required)
|
|
||||||
*/
|
|
||||||
|
|
||||||
require_once __DIR__ . '/../includes/functions.php';
|
|
||||||
require_once __DIR__ . '/../includes/stripe.php';
|
|
||||||
|
|
||||||
header('Content-Type: application/json');
|
|
||||||
|
|
||||||
// Only accept POST
|
|
||||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
|
||||||
jsonResponse(['error' => 'Method not allowed'], 405);
|
|
||||||
}
|
|
||||||
|
|
||||||
$input = json_decode(file_get_contents('php://input'), true);
|
|
||||||
$orderId = $input['order_id'] ?? '';
|
|
||||||
|
|
||||||
if (empty($orderId)) {
|
|
||||||
jsonResponse(['error' => 'Order ID required'], 400);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get order
|
|
||||||
$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);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if Stripe is configured
|
|
||||||
if (!isStripeConfigured()) {
|
|
||||||
// Demo mode - simulate successful payment
|
|
||||||
db()->update('orders',
|
|
||||||
[
|
|
||||||
'payment_status' => 'paid',
|
|
||||||
'order_status' => 'confirmed',
|
|
||||||
'stripe_payment_intent' => 'demo_' . bin2hex(random_bytes(8))
|
|
||||||
],
|
|
||||||
'order_id = :id',
|
|
||||||
['id' => $orderId]
|
|
||||||
);
|
|
||||||
|
|
||||||
jsonResponse([
|
|
||||||
'demo_mode' => true,
|
|
||||||
'message' => 'Payment simulated (Stripe not configured)',
|
|
||||||
'redirect' => '/order-confirmation.php?order=' . $orderId
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create Stripe Payment Intent using cURL-based API
|
|
||||||
try {
|
|
||||||
$paymentIntent = stripe()->createPaymentIntent(
|
|
||||||
$order['total'],
|
|
||||||
'usd',
|
|
||||||
[
|
|
||||||
'metadata' => [
|
|
||||||
'order_id' => $orderId,
|
|
||||||
'order_number' => $order['order_number']
|
|
||||||
],
|
|
||||||
'receipt_email' => $order['customer_email'],
|
|
||||||
'description' => 'Order #' . $order['order_number']
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
// Store payment intent ID
|
|
||||||
db()->update('orders',
|
|
||||||
['stripe_payment_intent' => $paymentIntent['id']],
|
|
||||||
'order_id = :id',
|
|
||||||
['id' => $orderId]
|
|
||||||
);
|
|
||||||
|
|
||||||
jsonResponse([
|
|
||||||
'client_secret' => $paymentIntent['client_secret']
|
|
||||||
]);
|
|
||||||
|
|
||||||
} catch (Exception $e) {
|
|
||||||
error_log('Stripe error: ' . $e->getMessage());
|
|
||||||
jsonResponse(['error' => 'Payment initialization failed: ' . $e->getMessage()], 500);
|
|
||||||
}
|
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
+10
-1
@@ -28,6 +28,13 @@ switch ($method) {
|
|||||||
jsonResponse(['error' => 'Order not found'], 404);
|
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['items'] = json_decode($order['items'], true);
|
||||||
$order['shipping_address'] = json_decode($order['shipping_address'], true);
|
$order['shipping_address'] = json_decode($order['shipping_address'], true);
|
||||||
unset($order['id']);
|
unset($order['id']);
|
||||||
@@ -71,7 +78,9 @@ switch ($method) {
|
|||||||
case 'POST':
|
case 'POST':
|
||||||
// Update order status (admin)
|
// Update order status (admin)
|
||||||
if ($action === 'update_status') {
|
if ($action === 'update_status') {
|
||||||
// Admin check would go here
|
if (!AdminAuth::isLoggedIn()) {
|
||||||
|
jsonResponse(['error' => 'Unauthorized'], 401);
|
||||||
|
}
|
||||||
$orderId = $input['order_id'] ?? '';
|
$orderId = $input['order_id'] ?? '';
|
||||||
$status = $input['status'] ?? '';
|
$status = $input['status'] ?? '';
|
||||||
$trackingNumber = $input['tracking_number'] ?? null;
|
$trackingNumber = $input['tracking_number'] ?? null;
|
||||||
|
|||||||
+38
-102
@@ -1,45 +1,39 @@
|
|||||||
<?php
|
<?php
|
||||||
/**
|
/**
|
||||||
* Tom's Java Jive - Check Payment Status API
|
* Tom's Java Jive - Check Payment Status API
|
||||||
* Polls Stripe for payment/checkout session status
|
* 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/functions.php';
|
||||||
require_once __DIR__ . '/../includes/stripe.php';
|
require_once __DIR__ . '/../includes/square.php';
|
||||||
require_once __DIR__ . '/../includes/loyalty.php';
|
require_once __DIR__ . '/../includes/loyalty.php';
|
||||||
|
require_once __DIR__ . '/../includes/email.php';
|
||||||
|
|
||||||
header('Content-Type: application/json');
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
// Only accept GET
|
|
||||||
if ($_SERVER['REQUEST_METHOD'] !== 'GET') {
|
if ($_SERVER['REQUEST_METHOD'] !== 'GET') {
|
||||||
jsonResponse(['error' => 'Method not allowed'], 405);
|
jsonResponse(['error' => 'Method not allowed'], 405);
|
||||||
}
|
}
|
||||||
|
|
||||||
$orderId = $_GET['order_id'] ?? '';
|
$orderId = $_GET['order_id'] ?? '';
|
||||||
$sessionId = $_GET['session_id'] ?? '';
|
|
||||||
|
|
||||||
if (empty($orderId) && empty($sessionId)) {
|
if (empty($orderId)) {
|
||||||
jsonResponse(['error' => 'Order ID or Session ID required'], 400);
|
jsonResponse(['error' => 'Order ID required'], 400);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get order by ID or session
|
|
||||||
if (!empty($orderId)) {
|
|
||||||
$order = db()->fetch(
|
$order = db()->fetch(
|
||||||
"SELECT * FROM orders WHERE order_id = :id",
|
"SELECT * FROM orders WHERE order_id = :id",
|
||||||
['id' => $orderId]
|
['id' => $orderId]
|
||||||
);
|
);
|
||||||
} else {
|
|
||||||
$order = db()->fetch(
|
|
||||||
"SELECT * FROM orders WHERE stripe_session_id = :session OR stripe_payment_intent = :session",
|
|
||||||
['session' => $sessionId]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!$order) {
|
if (!$order) {
|
||||||
jsonResponse(['error' => 'Order not found'], 404);
|
jsonResponse(['error' => 'Order not found'], 404);
|
||||||
}
|
}
|
||||||
|
|
||||||
// If already marked as paid, return success
|
|
||||||
if ($order['payment_status'] === 'paid') {
|
if ($order['payment_status'] === 'paid') {
|
||||||
jsonResponse([
|
jsonResponse([
|
||||||
'status' => 'complete',
|
'status' => 'complete',
|
||||||
@@ -50,105 +44,47 @@ if ($order['payment_status'] === 'paid') {
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if Stripe is configured
|
if (!isSquareConfigured()) {
|
||||||
if (!isStripeConfigured()) {
|
|
||||||
jsonResponse([
|
jsonResponse([
|
||||||
'status' => 'demo_mode',
|
'status' => 'demo_mode',
|
||||||
'payment_status' => $order['payment_status'],
|
'payment_status' => $order['payment_status'],
|
||||||
'message' => 'Stripe not configured - running in demo mode'
|
'message' => 'Square not configured - running in demo mode'
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
if (empty($order['square_payment_id'])) {
|
||||||
// Check with Stripe
|
|
||||||
if (!empty($order['stripe_session_id'])) {
|
|
||||||
// Check checkout session status
|
|
||||||
$session = stripe()->getCheckoutSession($order['stripe_session_id']);
|
|
||||||
|
|
||||||
if ($session['payment_status'] === 'paid') {
|
|
||||||
// Update order
|
|
||||||
db()->update('orders',
|
|
||||||
[
|
|
||||||
'payment_status' => 'paid',
|
|
||||||
'order_status' => 'confirmed',
|
|
||||||
'stripe_payment_intent' => $session['payment_intent'] ?? null
|
|
||||||
],
|
|
||||||
'order_id = :id',
|
|
||||||
['id' => $order['order_id']]
|
|
||||||
);
|
|
||||||
|
|
||||||
// Award loyalty points
|
|
||||||
if (!empty($order['customer_id'])) {
|
|
||||||
loyalty()->awardPoints(
|
|
||||||
$order['customer_id'],
|
|
||||||
(float) $order['total'],
|
|
||||||
'Order #' . $order['order_number'],
|
|
||||||
$order['order_id']
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
jsonResponse([
|
|
||||||
'status' => 'complete',
|
|
||||||
'payment_status' => 'paid',
|
|
||||||
'order_id' => $order['order_id'],
|
|
||||||
'order_number' => $order['order_number'],
|
|
||||||
'redirect' => '/order-confirmation.php?order=' . $order['order_id']
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
jsonResponse([
|
|
||||||
'status' => $session['status'],
|
|
||||||
'payment_status' => $session['payment_status']
|
|
||||||
]);
|
|
||||||
|
|
||||||
} elseif (!empty($order['stripe_payment_intent'])) {
|
|
||||||
// Check payment intent status
|
|
||||||
$paymentIntent = stripe()->getPaymentIntent($order['stripe_payment_intent']);
|
|
||||||
|
|
||||||
if ($paymentIntent['status'] === 'succeeded') {
|
|
||||||
// Update order
|
|
||||||
db()->update('orders',
|
|
||||||
[
|
|
||||||
'payment_status' => 'paid',
|
|
||||||
'order_status' => 'confirmed'
|
|
||||||
],
|
|
||||||
'order_id = :id',
|
|
||||||
['id' => $order['order_id']]
|
|
||||||
);
|
|
||||||
|
|
||||||
// Award loyalty points
|
|
||||||
if (!empty($order['customer_id'])) {
|
|
||||||
loyalty()->awardPoints(
|
|
||||||
$order['customer_id'],
|
|
||||||
(float) $order['total'],
|
|
||||||
'Order #' . $order['order_number'],
|
|
||||||
$order['order_id']
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
jsonResponse([
|
|
||||||
'status' => 'complete',
|
|
||||||
'payment_status' => 'paid',
|
|
||||||
'order_id' => $order['order_id'],
|
|
||||||
'order_number' => $order['order_number'],
|
|
||||||
'redirect' => '/order-confirmation.php?order=' . $order['order_id']
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
jsonResponse([
|
|
||||||
'status' => $paymentIntent['status'],
|
|
||||||
'payment_status' => 'pending'
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// No Stripe reference found
|
|
||||||
jsonResponse([
|
jsonResponse([
|
||||||
'status' => 'pending',
|
'status' => 'pending',
|
||||||
'payment_status' => $order['payment_status']
|
'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) {
|
} catch (Exception $e) {
|
||||||
error_log('Payment status check error: ' . $e->getMessage());
|
error_log('Square payment status check error: ' . $e->getMessage());
|
||||||
jsonResponse([
|
jsonResponse([
|
||||||
'status' => 'error',
|
'status' => 'error',
|
||||||
'payment_status' => $order['payment_status'],
|
'payment_status' => $order['payment_status'],
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ header('Content-Type: application/json');
|
|||||||
|
|
||||||
require_once __DIR__ . '/../includes/functions.php';
|
require_once __DIR__ . '/../includes/functions.php';
|
||||||
require_once __DIR__ . '/../includes/auth.php';
|
require_once __DIR__ . '/../includes/auth.php';
|
||||||
|
require_once __DIR__ . '/../includes/loyalty.php';
|
||||||
|
|
||||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||||
jsonResponse(['error' => 'Method not allowed'], 405);
|
jsonResponse(['error' => 'Method not allowed'], 405);
|
||||||
@@ -19,79 +20,15 @@ if (!CustomerAuth::isLoggedIn()) {
|
|||||||
$customer = CustomerAuth::getFullUser();
|
$customer = CustomerAuth::getFullUser();
|
||||||
$input = json_decode(file_get_contents('php://input'), true);
|
$input = json_decode(file_get_contents('php://input'), true);
|
||||||
|
|
||||||
$code = strtoupper(str_replace(['-', ' '], '', trim($input['code'] ?? '')));
|
$result = loyalty()->redeemGiftCardToWallet($customer['customer_id'], $input['code'] ?? '');
|
||||||
|
|
||||||
if (empty($code) || strlen($code) < 8) {
|
if (!$result['success']) {
|
||||||
jsonResponse(['error' => 'Invalid gift card code'], 400);
|
jsonResponse(['error' => $result['error']], 400);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find gift card
|
|
||||||
$giftCard = db()->fetch(
|
|
||||||
"SELECT * FROM gift_cards WHERE code = :code AND is_active = 1",
|
|
||||||
['code' => $code]
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!$giftCard) {
|
|
||||||
jsonResponse(['error' => 'Gift card not found or already used'], 404);
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($giftCard['current_balance'] <= 0) {
|
|
||||||
jsonResponse(['error' => 'This gift card has no remaining balance'], 400);
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($giftCard['expires_at'] && strtotime($giftCard['expires_at']) < time()) {
|
|
||||||
jsonResponse(['error' => 'This gift card has expired'], 400);
|
|
||||||
}
|
|
||||||
|
|
||||||
$amount = $giftCard['current_balance'];
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Start transaction
|
|
||||||
db()->query("START TRANSACTION");
|
|
||||||
|
|
||||||
// Update gift card balance to 0
|
|
||||||
db()->query(
|
|
||||||
"UPDATE gift_cards SET current_balance = 0, is_active = 0, updated_at = NOW() WHERE gift_card_id = :id",
|
|
||||||
['id' => $giftCard['gift_card_id']]
|
|
||||||
);
|
|
||||||
|
|
||||||
// Log gift card transaction
|
|
||||||
db()->insert('gift_card_transactions', [
|
|
||||||
'gift_card_id' => $giftCard['gift_card_id'],
|
|
||||||
'amount' => -$amount,
|
|
||||||
'balance_after' => 0,
|
|
||||||
'type' => 'redeem',
|
|
||||||
'description' => 'Redeemed by customer: ' . $customer['email']
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Add to customer wallet
|
|
||||||
$newWalletBalance = ($customer['wallet_balance'] ?? 0) + $amount;
|
|
||||||
|
|
||||||
db()->query(
|
|
||||||
"UPDATE customers SET wallet_balance = :balance, updated_at = NOW() WHERE customer_id = :id",
|
|
||||||
['balance' => $newWalletBalance, 'id' => $customer['customer_id']]
|
|
||||||
);
|
|
||||||
|
|
||||||
// Log wallet transaction
|
|
||||||
db()->insert('wallet_transactions', [
|
|
||||||
'transaction_id' => generateId('wt_'),
|
|
||||||
'customer_id' => $customer['customer_id'],
|
|
||||||
'amount' => $amount,
|
|
||||||
'balance_after' => $newWalletBalance,
|
|
||||||
'type' => 'gift_card',
|
|
||||||
'description' => 'Gift card redeemed: ' . $code
|
|
||||||
]);
|
|
||||||
|
|
||||||
db()->query("COMMIT");
|
|
||||||
|
|
||||||
jsonResponse([
|
jsonResponse([
|
||||||
'success' => true,
|
'success' => true,
|
||||||
'amount' => $amount,
|
'amount' => $result['amount'],
|
||||||
'new_balance' => $newWalletBalance,
|
'new_balance' => $result['new_balance'],
|
||||||
'message' => formatCurrency($amount) . ' has been added to your wallet!'
|
'message' => formatCurrency($result['amount']) . ' has been added to your wallet!'
|
||||||
]);
|
]);
|
||||||
|
|
||||||
} catch (Exception $e) {
|
|
||||||
db()->query("ROLLBACK");
|
|
||||||
jsonResponse(['error' => 'Failed to redeem gift card. Please try again.'], 500);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -6,6 +6,11 @@
|
|||||||
header('Content-Type: application/json');
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
require_once __DIR__ . '/../includes/functions.php';
|
require_once __DIR__ . '/../includes/functions.php';
|
||||||
|
require_once __DIR__ . '/../includes/auth.php';
|
||||||
|
|
||||||
|
if (!AdminAuth::isLoggedIn()) {
|
||||||
|
jsonResponse(['error' => 'Unauthorized'], 401);
|
||||||
|
}
|
||||||
|
|
||||||
$query = $_GET['q'] ?? '';
|
$query = $_GET['q'] ?? '';
|
||||||
|
|
||||||
@@ -16,10 +21,10 @@ if (strlen($query) < 2) {
|
|||||||
$customers = db()->fetchAll(
|
$customers = db()->fetchAll(
|
||||||
"SELECT customer_id, email, name, phone, wallet_balance, reward_points
|
"SELECT customer_id, email, name, phone, wallet_balance, reward_points
|
||||||
FROM customers
|
FROM customers
|
||||||
WHERE (email LIKE :q OR name LIKE :q OR phone LIKE :q) AND is_active = 1
|
WHERE (email LIKE :q1 OR name LIKE :q2 OR phone LIKE :q3) AND is_active = 1
|
||||||
ORDER BY name ASC
|
ORDER BY name ASC
|
||||||
LIMIT 20",
|
LIMIT 20",
|
||||||
['q' => '%' . $query . '%']
|
['q1' => '%' . $query . '%', 'q2' => '%' . $query . '%', 'q3' => '%' . $query . '%']
|
||||||
);
|
);
|
||||||
|
|
||||||
jsonResponse($customers);
|
jsonResponse($customers);
|
||||||
|
|||||||
+30
-73
@@ -1,107 +1,65 @@
|
|||||||
<?php
|
<?php
|
||||||
/**
|
/**
|
||||||
* Tom's Java Jive - Stripe Webhook Handler
|
* Tom's Java Jive - Square Webhook Handler
|
||||||
* Uses cURL-based Stripe integration (no Composer required)
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
require_once __DIR__ . '/../includes/functions.php';
|
require_once __DIR__ . '/../includes/functions.php';
|
||||||
require_once __DIR__ . '/../includes/stripe.php';
|
require_once __DIR__ . '/../includes/square.php';
|
||||||
|
require_once __DIR__ . '/../includes/loyalty.php';
|
||||||
require_once __DIR__ . '/../includes/email.php';
|
require_once __DIR__ . '/../includes/email.php';
|
||||||
|
|
||||||
header('Content-Type: application/json');
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
$payload = file_get_contents('php://input');
|
$payload = file_get_contents('php://input');
|
||||||
$sigHeader = $_SERVER['HTTP_STRIPE_SIGNATURE'] ?? '';
|
$sigHeader = $_SERVER['HTTP_X_SQUARE_HMACSHA256_SIGNATURE'] ?? '';
|
||||||
|
$notificationUrl = SITE_URL . '/api/webhook.php';
|
||||||
|
|
||||||
// Verify webhook signature (if secret is configured)
|
if (empty(SQUARE_WEBHOOK_SIGNATURE_KEY) || SQUARE_WEBHOOK_SIGNATURE_KEY === 'REPLACE_ME') {
|
||||||
if (!empty(STRIPE_WEBHOOK_SECRET) && STRIPE_WEBHOOK_SECRET !== 'whsec_your_webhook_secret') {
|
error_log('Square webhook signature key not configured - rejecting webhook');
|
||||||
try {
|
|
||||||
stripe()->verifyWebhookSignature($payload, $sigHeader, STRIPE_WEBHOOK_SECRET);
|
|
||||||
$event = json_decode($payload, true);
|
|
||||||
} catch (Exception $e) {
|
|
||||||
error_log('Stripe webhook signature verification failed: ' . $e->getMessage());
|
|
||||||
http_response_code(400);
|
http_response_code(400);
|
||||||
exit();
|
exit();
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
|
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);
|
$event = json_decode($payload, true);
|
||||||
if (!$event) {
|
if (!$event) {
|
||||||
http_response_code(400);
|
http_response_code(400);
|
||||||
exit();
|
exit();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
$eventType = $event['type'] ?? '';
|
$eventType = $event['type'] ?? '';
|
||||||
$data = $event['data']['object'] ?? [];
|
|
||||||
|
|
||||||
switch ($eventType) {
|
switch ($eventType) {
|
||||||
|
|
||||||
case 'checkout.session.completed':
|
case 'payment.updated':
|
||||||
// Stripe Checkout (hosted page) — metadata is on the session
|
case 'payment.created':
|
||||||
$orderId = $data['metadata']['order_id'] ?? '';
|
$payment = $event['data']['object']['payment'] ?? [];
|
||||||
$paymentIntentId = $data['payment_intent'] ?? '';
|
$paymentId = $payment['id'] ?? '';
|
||||||
if ($orderId && ($data['payment_status'] ?? '') === 'paid') {
|
$status = $payment['status'] ?? '';
|
||||||
db()->update('orders',
|
$orderId = $payment['reference_id'] ?? null;
|
||||||
[
|
if ($paymentId && $status) {
|
||||||
'payment_status' => 'paid',
|
markSquarePaymentResult($paymentId, $orderId, $status);
|
||||||
'order_status' => 'confirmed',
|
|
||||||
'stripe_payment_intent' => $paymentIntentId,
|
|
||||||
],
|
|
||||||
'order_id = :id',
|
|
||||||
['id' => $orderId]
|
|
||||||
);
|
|
||||||
$order = db()->fetch("SELECT * FROM orders WHERE order_id = :id", ['id' => $orderId]);
|
|
||||||
if ($order) {
|
|
||||||
emailService()->sendOrderConfirmation($order);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'payment_intent.succeeded':
|
case 'refund.updated':
|
||||||
// Payment Intent flow (embedded/direct) - skip if already confirmed by checkout.session.completed
|
case 'refund.created':
|
||||||
$paymentIntentId = $data['id'] ?? '';
|
$refund = $event['data']['object']['refund'] ?? [];
|
||||||
$orderId = $data['metadata']['order_id'] ?? '';
|
$paymentId = $refund['payment_id'] ?? '';
|
||||||
if ($orderId && ($data['status'] ?? '') === 'succeeded') {
|
$status = $refund['status'] ?? '';
|
||||||
$order = db()->fetch("SELECT * FROM orders WHERE order_id = :id", ['id' => $orderId]);
|
if ($status === 'COMPLETED' && $paymentId) {
|
||||||
if ($order && $order['order_status'] !== 'confirmed') {
|
|
||||||
db()->update('orders',
|
|
||||||
[
|
|
||||||
'payment_status' => 'paid',
|
|
||||||
'order_status' => 'confirmed',
|
|
||||||
'stripe_payment_intent' => $paymentIntentId,
|
|
||||||
],
|
|
||||||
'order_id = :id',
|
|
||||||
['id' => $orderId]
|
|
||||||
);
|
|
||||||
$order = db()->fetch("SELECT * FROM orders WHERE order_id = :id", ['id' => $orderId]);
|
|
||||||
if ($order) {
|
|
||||||
emailService()->sendOrderConfirmation($order);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'payment_intent.payment_failed':
|
|
||||||
$orderId = $data['metadata']['order_id'] ?? '';
|
|
||||||
if ($orderId) {
|
|
||||||
db()->update('orders',
|
|
||||||
['payment_status' => 'failed'],
|
|
||||||
'order_id = :id',
|
|
||||||
['id' => $orderId]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'charge.refunded':
|
|
||||||
$paymentIntentId = $data['payment_intent'] ?? '';
|
|
||||||
if ($paymentIntentId) {
|
|
||||||
db()->update('orders',
|
db()->update('orders',
|
||||||
[
|
[
|
||||||
'payment_status' => 'refunded',
|
'payment_status' => 'refunded',
|
||||||
'order_status' => 'refunded',
|
'order_status' => 'refunded',
|
||||||
],
|
],
|
||||||
'stripe_payment_intent = :pi',
|
'square_payment_id = :pid',
|
||||||
['pi' => $paymentIntentId]
|
['pid' => $paymentId]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@@ -109,4 +67,3 @@ switch ($eventType) {
|
|||||||
|
|
||||||
http_response_code(200);
|
http_response_code(200);
|
||||||
echo json_encode(['received' => true]);
|
echo json_encode(['received' => true]);
|
||||||
|
|
||||||
|
|||||||
+132
-7
@@ -6,6 +6,7 @@
|
|||||||
$pageTitle = "Checkout - Tom's Java Jive";
|
$pageTitle = "Checkout - Tom's Java Jive";
|
||||||
require_once __DIR__ . '/includes/functions.php';
|
require_once __DIR__ . '/includes/functions.php';
|
||||||
require_once __DIR__ . '/includes/auth.php';
|
require_once __DIR__ . '/includes/auth.php';
|
||||||
|
require_once __DIR__ . '/includes/loyalty.php';
|
||||||
|
|
||||||
$cart = getCart();
|
$cart = getCart();
|
||||||
if (empty($cart)) {
|
if (empty($cart)) {
|
||||||
@@ -50,10 +51,8 @@ if ($shippingSettings['flat_rate_enabled'] ?? true) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$total = $subtotal + $shippingCost;
|
$preDiscountTotal = $subtotal + $shippingCost;
|
||||||
|
$processor = defined('PAYMENT_PROCESSOR') ? PAYMENT_PROCESSOR : 'stripe';
|
||||||
// Get Stripe publishable key
|
|
||||||
$stripeKey = STRIPE_PUBLISHABLE_KEY;
|
|
||||||
|
|
||||||
$errors = [];
|
$errors = [];
|
||||||
|
|
||||||
@@ -75,6 +74,18 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||||||
if (empty($state)) $errors['state'] = 'State is required';
|
if (empty($state)) $errors['state'] = 'State is required';
|
||||||
if (empty($zip)) $errors['zip'] = 'ZIP code is required';
|
if (empty($zip)) $errors['zip'] = 'ZIP code is required';
|
||||||
|
|
||||||
|
// Re-validate any requested wallet credit server-side against the
|
||||||
|
// customer's current balance - never trust the client-submitted amount.
|
||||||
|
$walletAmountUsed = 0;
|
||||||
|
if ($customer && !empty($_POST['wallet_amount_used'])) {
|
||||||
|
$requestedWallet = (float) $_POST['wallet_amount_used'];
|
||||||
|
$freshCustomer = db()->fetch("SELECT wallet_balance FROM customers WHERE customer_id = :id", ['id' => $customer['customer_id']]);
|
||||||
|
$currentBalance = (float) ($freshCustomer['wallet_balance'] ?? 0);
|
||||||
|
$walletAmountUsed = max(0, round(min($requestedWallet, $currentBalance, $preDiscountTotal), 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
$total = round($preDiscountTotal - $walletAmountUsed, 2);
|
||||||
|
|
||||||
if (empty($errors)) {
|
if (empty($errors)) {
|
||||||
// Create order
|
// Create order
|
||||||
$orderId = generateId('ord_');
|
$orderId = generateId('ord_');
|
||||||
@@ -111,6 +122,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||||||
'items' => json_encode($orderItems),
|
'items' => json_encode($orderItems),
|
||||||
'subtotal' => $subtotal,
|
'subtotal' => $subtotal,
|
||||||
'shipping_cost' => $shippingCost,
|
'shipping_cost' => $shippingCost,
|
||||||
|
'wallet_amount_used' => $walletAmountUsed,
|
||||||
'total' => $total,
|
'total' => $total,
|
||||||
'shipping_address' => json_encode([
|
'shipping_address' => json_encode([
|
||||||
'address' => $address,
|
'address' => $address,
|
||||||
@@ -120,7 +132,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||||||
'country' => 'USA'
|
'country' => 'USA'
|
||||||
]),
|
]),
|
||||||
'shipping_method' => 'standard',
|
'shipping_method' => 'standard',
|
||||||
'payment_method' => 'stripe',
|
'payment_method' => $processor,
|
||||||
'payment_status' => 'pending',
|
'payment_status' => 'pending',
|
||||||
'order_status' => 'pending'
|
'order_status' => 'pending'
|
||||||
]);
|
]);
|
||||||
@@ -167,6 +179,7 @@ require_once __DIR__ . '/includes/header.php';
|
|||||||
<h1 style="margin-bottom: 2rem;">Checkout</h1>
|
<h1 style="margin-bottom: 2rem;">Checkout</h1>
|
||||||
|
|
||||||
<form method="POST" action="" id="checkout-form">
|
<form method="POST" action="" id="checkout-form">
|
||||||
|
<input type="hidden" name="wallet_amount_used" id="wallet_amount_used" value="0">
|
||||||
<div class="checkout-layout">
|
<div class="checkout-layout">
|
||||||
|
|
||||||
<!-- Customer & Shipping Info -->
|
<!-- Customer & Shipping Info -->
|
||||||
@@ -263,6 +276,38 @@ require_once __DIR__ . '/includes/header.php';
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<?php if ($customer): ?>
|
||||||
|
<!-- Wallet / Gift Card -->
|
||||||
|
<div class="card mb-2">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3 style="margin: 0;">Wallet & Gift Cards</h3>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<p class="text-muted" style="font-size: 0.875rem; margin-bottom: 1rem;">
|
||||||
|
Wallet balance: <strong id="wallet-balance-display"><?= formatCurrency($customer['wallet_balance'] ?? 0) ?></strong>
|
||||||
|
</p>
|
||||||
|
<?php if (($customer['wallet_balance'] ?? 0) > 0): ?>
|
||||||
|
<button type="button" id="apply-wallet-btn" class="btn btn-secondary mb-1">
|
||||||
|
<i class="fas fa-wallet"></i> Apply Wallet Balance
|
||||||
|
</button>
|
||||||
|
<?php endif; ?>
|
||||||
|
<div class="form-group" style="display: flex; gap: 0.5rem; align-items: flex-end;">
|
||||||
|
<div style="flex: 1;">
|
||||||
|
<label class="form-label">Gift Card Code</label>
|
||||||
|
<input type="text" id="gift-card-code" class="form-input"
|
||||||
|
placeholder="XXXX-XXXX-XXXX" style="text-transform: uppercase;">
|
||||||
|
</div>
|
||||||
|
<button type="button" id="apply-gift-card-btn" class="btn btn-secondary">Apply</button>
|
||||||
|
</div>
|
||||||
|
<div id="wallet-message" style="margin-top: 0.5rem;"></div>
|
||||||
|
<div id="wallet-applied-row" style="display: none; margin-top: 0.5rem; font-weight: 600;">
|
||||||
|
Applied: <span id="wallet-applied-amount"></span>
|
||||||
|
<button type="button" id="remove-wallet-btn" class="btn btn-secondary" style="margin-left: 0.5rem; padding: 0.15rem 0.6rem; font-size: 0.8rem;">Remove</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
<!-- Order Notes -->
|
<!-- Order Notes -->
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
@@ -313,12 +358,16 @@ require_once __DIR__ . '/includes/header.php';
|
|||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="checkout-summary-row" id="wallet-discount-row" style="display: none;">
|
||||||
|
<span>Wallet / Gift Card</span>
|
||||||
|
<span id="wallet-discount-amount" class="text-success"></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<hr style="margin: 1rem 0;">
|
<hr style="margin: 1rem 0;">
|
||||||
|
|
||||||
<div class="checkout-summary-total">
|
<div class="checkout-summary-total">
|
||||||
<span>Total</span>
|
<span>Total</span>
|
||||||
<span><?= formatCurrency($total) ?></span>
|
<span id="checkout-total-display"><?= formatCurrency($preDiscountTotal) ?></span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button type="submit" class="btn btn-primary btn-lg btn-block mt-2">
|
<button type="submit" class="btn btn-primary btn-lg btn-block mt-2">
|
||||||
@@ -330,7 +379,7 @@ require_once __DIR__ . '/includes/header.php';
|
|||||||
</a>
|
</a>
|
||||||
|
|
||||||
<p class="secure-badge">
|
<p class="secure-badge">
|
||||||
<i class="fas fa-lock"></i> Secure checkout powered by Stripe
|
<i class="fas fa-lock"></i> Secure checkout powered by <?= $processor === 'square' ? 'Square' : 'Stripe' ?>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -339,4 +388,80 @@ require_once __DIR__ . '/includes/header.php';
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<?php if ($customer): ?>
|
||||||
|
<script>
|
||||||
|
const ORDER_TOTAL_PRE_DISCOUNT = <?= json_encode($preDiscountTotal) ?>;
|
||||||
|
const CURRENCY_SYMBOL = <?= json_encode(defined('TJJ_CURRENCY_SYMBOL') ? TJJ_CURRENCY_SYMBOL : '$') ?>;
|
||||||
|
|
||||||
|
function fmt(amount) {
|
||||||
|
return CURRENCY_SYMBOL + Number(amount).toFixed(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
function showWalletMessage(msg, isError) {
|
||||||
|
const el = document.getElementById('wallet-message');
|
||||||
|
el.textContent = msg;
|
||||||
|
el.style.color = isError ? 'var(--color-error)' : 'var(--color-success)';
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyWalletResult(data) {
|
||||||
|
document.getElementById('wallet_amount_used').value = data.apply_amount;
|
||||||
|
document.getElementById('checkout-total-display').textContent = fmt(data.new_total);
|
||||||
|
document.getElementById('wallet-balance-display').textContent = fmt(data.wallet_balance);
|
||||||
|
|
||||||
|
if (data.apply_amount > 0) {
|
||||||
|
document.getElementById('wallet-discount-row').style.display = '';
|
||||||
|
document.getElementById('wallet-discount-amount').textContent = '-' + fmt(data.apply_amount);
|
||||||
|
document.getElementById('wallet-applied-row').style.display = '';
|
||||||
|
document.getElementById('wallet-applied-amount').textContent = fmt(data.apply_amount);
|
||||||
|
} else {
|
||||||
|
document.getElementById('wallet-discount-row').style.display = 'none';
|
||||||
|
document.getElementById('wallet-applied-row').style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function applyCredit(body) {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/apply-wallet-credit.php', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(Object.assign({ order_total: ORDER_TOTAL_PRE_DISCOUNT }, body))
|
||||||
|
});
|
||||||
|
const data = await response.json();
|
||||||
|
if (data.error) {
|
||||||
|
showWalletMessage(data.error, true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
showWalletMessage('Applied ' + fmt(data.apply_amount) + ' to your order.', false);
|
||||||
|
applyWalletResult(data);
|
||||||
|
} catch (err) {
|
||||||
|
showWalletMessage('Failed to apply credit. Please try again.', true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const applyWalletBtn = document.getElementById('apply-wallet-btn');
|
||||||
|
if (applyWalletBtn) {
|
||||||
|
applyWalletBtn.addEventListener('click', function() {
|
||||||
|
applyCredit({});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('apply-gift-card-btn').addEventListener('click', function() {
|
||||||
|
const code = document.getElementById('gift-card-code').value.trim();
|
||||||
|
if (!code) {
|
||||||
|
showWalletMessage('Enter a gift card code first.', true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
applyCredit({ gift_card_code: code });
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('remove-wallet-btn').addEventListener('click', function() {
|
||||||
|
document.getElementById('wallet_amount_used').value = 0;
|
||||||
|
document.getElementById('checkout-total-display').textContent = fmt(ORDER_TOTAL_PRE_DISCOUNT);
|
||||||
|
document.getElementById('wallet-discount-row').style.display = 'none';
|
||||||
|
document.getElementById('wallet-applied-row').style.display = 'none';
|
||||||
|
showWalletMessage('', false);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
<?php require_once __DIR__ . '/includes/footer.php'; ?>
|
<?php require_once __DIR__ . '/includes/footer.php'; ?>
|
||||||
|
|||||||
+1
-65
@@ -1,66 +1,2 @@
|
|||||||
<?php
|
<?php
|
||||||
/**
|
require_once dirname(__DIR__, 2) . "/config-secrets.php";
|
||||||
* Tom's Java Jive - Main Configuration
|
|
||||||
*/
|
|
||||||
|
|
||||||
// Site Configuration
|
|
||||||
if (!defined('SITE_NAME')) { define('SITE_NAME', "Tom's Java Jive"); }
|
|
||||||
if (!defined('SITE_URL')) { define('SITE_URL', 'https://tomsjavajive.com'); }
|
|
||||||
if (!defined('SITE_EMAIL')) { define('SITE_EMAIL', 'support@tomsjavajive.com'); }
|
|
||||||
|
|
||||||
// Environment
|
|
||||||
if (!defined('ENVIRONMENT')) { define('ENVIRONMENT', 'production'); }
|
|
||||||
if (!defined('DEBUG_MODE')) { define('DEBUG_MODE', false); }
|
|
||||||
|
|
||||||
// Session Configuration
|
|
||||||
if (!defined('SESSION_NAME')) { define('SESSION_NAME', 'tjj_session'); }
|
|
||||||
if (!defined('SESSION_LIFETIME')) { define('SESSION_LIFETIME', 86400 * 7); }
|
|
||||||
|
|
||||||
// Security
|
|
||||||
if (!defined('HASH_COST')) { define('HASH_COST', 12); }
|
|
||||||
if (!defined('CSRF_TOKEN_NAME')) { define('CSRF_TOKEN_NAME', 'csrf_token'); }
|
|
||||||
|
|
||||||
// Stripe
|
|
||||||
if (!defined('STRIPE_SECRET_KEY')) { define('STRIPE_SECRET_KEY', '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'); }
|
|
||||||
if (!defined('STRIPE_PUBLISHABLE_KEY')) { define('STRIPE_PUBLISHABLE_KEY', 'pk_live_51TcSte42hN9FJ8ht6QMzoTJwji8q1me1cFvEX5Y3mES1cJnA6LQ0ROvqkoiiPBMGf73jszIHZPv6Q37qUgLYdCeG00W4uehVba'); }
|
|
||||||
if (!defined('STRIPE_WEBHOOK_SECRET')) { define('STRIPE_WEBHOOK_SECRET', 'whsec_esEudSGg3zch1zoHaYD3b9Bj2CfZerO9'); }
|
|
||||||
|
|
||||||
// CyberMail SMTP
|
|
||||||
if (!defined('SMTP_HOST')) { define('SMTP_HOST', 'mail.cyberpersons.com'); }
|
|
||||||
if (!defined('SMTP_PORT')) { define('SMTP_PORT', 587); }
|
|
||||||
if (!defined('SMTP_USER')) { define('SMTP_USER', 'smtp_49a1fa9c0f15d2d7'); }
|
|
||||||
if (!defined('SMTP_PASS')) { define('SMTP_PASS', 'T3mOFSMK1SG1l4D1d7N8NefRd8xypwMy'); }
|
|
||||||
if (!defined('SMTP_SECURE')) { define('SMTP_SECURE', 'tls'); } // STARTTLS
|
|
||||||
if (!defined('SENDER_EMAIL')) { define('SENDER_EMAIL', 'noreply@tomsjavajive.com'); }
|
|
||||||
if (!defined('SENDER_NAME')) { define('SENDER_NAME', "Tom's Java Jive"); }
|
|
||||||
if (!defined('CYBERMAIL_API_KEY')) { define('CYBERMAIL_API_KEY', 'sk_live_d52bf062797105aeaafac9954c21ff988e9b41b77315807d'); }
|
|
||||||
|
|
||||||
// Twilio (optional)
|
|
||||||
if (!defined('TWILIO_SID')) { define('TWILIO_SID', ''); }
|
|
||||||
if (!defined('TWILIO_AUTH_TOKEN')) { define('TWILIO_AUTH_TOKEN', ''); }
|
|
||||||
if (!defined('TWILIO_PHONE')) { define('TWILIO_PHONE', ''); }
|
|
||||||
|
|
||||||
// File Uploads
|
|
||||||
if (!defined('UPLOAD_DIR')) { define('UPLOAD_DIR', __DIR__ . '/../uploads/'); }
|
|
||||||
if (!defined('MAX_UPLOAD_SIZE')) { define('MAX_UPLOAD_SIZE', 5 * 1024 * 1024); }
|
|
||||||
if (!defined('ALLOWED_IMAGE_TYPES')) { define('ALLOWED_IMAGE_TYPES', ['image/jpeg', 'image/png', 'image/gif', 'image/webp']); }
|
|
||||||
|
|
||||||
// Pagination
|
|
||||||
if (!defined('ITEMS_PER_PAGE')) { define('ITEMS_PER_PAGE', 12); }
|
|
||||||
if (!defined('ADMIN_ITEMS_PER_PAGE')) { define('ADMIN_ITEMS_PER_PAGE', 20); }
|
|
||||||
|
|
||||||
// Currency
|
|
||||||
if (!defined('CURRENCY_CODE')) { define('CURRENCY_CODE', 'USD'); }
|
|
||||||
if (!defined('TJJ_CURRENCY_SYMBOL')) { define('TJJ_CURRENCY_SYMBOL', '$'); }
|
|
||||||
|
|
||||||
// Timezone
|
|
||||||
date_default_timezone_set('America/New_York');
|
|
||||||
|
|
||||||
// Error Reporting
|
|
||||||
if (ENVIRONMENT === 'development' || DEBUG_MODE) {
|
|
||||||
error_reporting(E_ALL);
|
|
||||||
ini_set('display_errors', 1);
|
|
||||||
} else {
|
|
||||||
error_reporting(0);
|
|
||||||
ini_set('display_errors', 0);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -326,6 +326,8 @@ CREATE TABLE `orders` (
|
|||||||
`order_status` enum('pending','confirmed','processing','shipped','delivered','cancelled','refunded') DEFAULT 'pending',
|
`order_status` enum('pending','confirmed','processing','shipped','delivered','cancelled','refunded') DEFAULT 'pending',
|
||||||
`stripe_session_id` varchar(255) DEFAULT NULL,
|
`stripe_session_id` varchar(255) DEFAULT NULL,
|
||||||
`stripe_payment_intent` varchar(255) DEFAULT NULL,
|
`stripe_payment_intent` varchar(255) DEFAULT NULL,
|
||||||
|
`square_payment_id` varchar(255) DEFAULT NULL,
|
||||||
|
`square_order_id` varchar(255) DEFAULT NULL,
|
||||||
`tracking_number` varchar(100) DEFAULT NULL,
|
`tracking_number` varchar(100) DEFAULT NULL,
|
||||||
`tracking_url` varchar(500) DEFAULT NULL,
|
`tracking_url` varchar(500) DEFAULT NULL,
|
||||||
`notes` text DEFAULT NULL,
|
`notes` text DEFAULT NULL,
|
||||||
|
|||||||
+82
-2
@@ -150,10 +150,10 @@ class LoyaltyProgram {
|
|||||||
db()->query(
|
db()->query(
|
||||||
"UPDATE customers SET
|
"UPDATE customers SET
|
||||||
reward_points = reward_points + :points,
|
reward_points = reward_points + :points,
|
||||||
lifetime_points = COALESCE(lifetime_points, 0) + :points,
|
lifetime_points = COALESCE(lifetime_points, 0) + :points2,
|
||||||
updated_at = NOW()
|
updated_at = NOW()
|
||||||
WHERE customer_id = :id",
|
WHERE customer_id = :id",
|
||||||
['points' => $totalPoints, 'id' => $customerId]
|
['points' => $totalPoints, 'points2' => $totalPoints, 'id' => $customerId]
|
||||||
);
|
);
|
||||||
|
|
||||||
$newBalance = db()->fetch(
|
$newBalance = db()->fetch(
|
||||||
@@ -417,6 +417,86 @@ class LoyaltyProgram {
|
|||||||
'new_customer_bonus' => $newCustomerBonus
|
'new_customer_bonus' => $newCustomerBonus
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Redeem a gift card code into wallet balance. Shared by the Wallet page
|
||||||
|
* and checkout so both go through the same transaction logic instead of
|
||||||
|
* duplicating it.
|
||||||
|
*/
|
||||||
|
public function redeemGiftCardToWallet(string $customerId, string $rawCode): array {
|
||||||
|
$code = strtoupper(str_replace(['-', ' '], '', trim($rawCode)));
|
||||||
|
|
||||||
|
if (empty($code) || strlen($code) < 8) {
|
||||||
|
return ['success' => false, 'error' => 'Invalid gift card code'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$giftCard = db()->fetch(
|
||||||
|
"SELECT * FROM gift_cards WHERE code = :code AND is_active = 1",
|
||||||
|
['code' => $code]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!$giftCard) {
|
||||||
|
return ['success' => false, 'error' => 'Gift card not found or already used'];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($giftCard['current_balance'] <= 0) {
|
||||||
|
return ['success' => false, 'error' => 'This gift card has no remaining balance'];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($giftCard['expires_at'] && strtotime($giftCard['expires_at']) < time()) {
|
||||||
|
return ['success' => false, 'error' => 'This gift card has expired'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$customer = db()->fetch("SELECT wallet_balance FROM customers WHERE customer_id = :id", ['id' => $customerId]);
|
||||||
|
if (!$customer) {
|
||||||
|
return ['success' => false, 'error' => 'Customer not found'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$amount = $giftCard['current_balance'];
|
||||||
|
|
||||||
|
try {
|
||||||
|
db()->query("START TRANSACTION");
|
||||||
|
|
||||||
|
db()->query(
|
||||||
|
"UPDATE gift_cards SET current_balance = 0, is_active = 0 WHERE gift_card_id = :id",
|
||||||
|
['id' => $giftCard['gift_card_id']]
|
||||||
|
);
|
||||||
|
|
||||||
|
db()->insert('gift_card_transactions', [
|
||||||
|
'gift_card_id' => $giftCard['gift_card_id'],
|
||||||
|
'amount' => -$amount,
|
||||||
|
'balance_after' => 0,
|
||||||
|
'type' => 'redemption',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$newWalletBalance = (float) $customer['wallet_balance'] + $amount;
|
||||||
|
|
||||||
|
db()->query(
|
||||||
|
"UPDATE customers SET wallet_balance = :balance, updated_at = NOW() WHERE customer_id = :id",
|
||||||
|
['balance' => $newWalletBalance, 'id' => $customerId]
|
||||||
|
);
|
||||||
|
|
||||||
|
db()->insert('wallet_transactions', [
|
||||||
|
'transaction_id' => generateId('wt_'),
|
||||||
|
'customer_id' => $customerId,
|
||||||
|
'amount' => $amount,
|
||||||
|
'balance_after' => $newWalletBalance,
|
||||||
|
'type' => 'deposit',
|
||||||
|
'description' => 'Gift card redeemed: ' . $code
|
||||||
|
]);
|
||||||
|
|
||||||
|
db()->query("COMMIT");
|
||||||
|
|
||||||
|
return [
|
||||||
|
'success' => true,
|
||||||
|
'amount' => $amount,
|
||||||
|
'new_balance' => $newWalletBalance
|
||||||
|
];
|
||||||
|
} catch (Exception $e) {
|
||||||
|
db()->query("ROLLBACK");
|
||||||
|
return ['success' => false, 'error' => 'Failed to redeem gift card. Please try again.'];
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper function
|
// Helper function
|
||||||
|
|||||||
@@ -0,0 +1,175 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Tom's Java Jive - Square Integration (cURL-based, no SDK required)
|
||||||
|
*/
|
||||||
|
|
||||||
|
function squareApi(string $method, string $path, array $body = []): array {
|
||||||
|
$base = (defined('SQUARE_ENV') && SQUARE_ENV === 'sandbox')
|
||||||
|
? 'https://connect.squareupsandbox.com/v2'
|
||||||
|
: 'https://connect.squareup.com/v2';
|
||||||
|
|
||||||
|
$ch = curl_init($base . $path);
|
||||||
|
$opts = [
|
||||||
|
CURLOPT_RETURNTRANSFER => true,
|
||||||
|
CURLOPT_TIMEOUT => 30,
|
||||||
|
CURLOPT_SSL_VERIFYPEER => true,
|
||||||
|
CURLOPT_HTTPHEADER => [
|
||||||
|
'Content-Type: application/json',
|
||||||
|
'Authorization: Bearer ' . SQUARE_ACCESS_TOKEN,
|
||||||
|
'Square-Version: 2024-01-18',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
if ($method !== 'GET') {
|
||||||
|
$opts[CURLOPT_CUSTOMREQUEST] = $method;
|
||||||
|
$opts[CURLOPT_POSTFIELDS] = json_encode($body ?: new stdClass());
|
||||||
|
}
|
||||||
|
curl_setopt_array($ch, $opts);
|
||||||
|
|
||||||
|
$resp = curl_exec($ch);
|
||||||
|
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||||
|
$err = curl_error($ch);
|
||||||
|
curl_close($ch);
|
||||||
|
|
||||||
|
if ($err) {
|
||||||
|
throw new Exception('Square API connection error: ' . $err);
|
||||||
|
}
|
||||||
|
|
||||||
|
$decoded = json_decode($resp ?: '{}', true);
|
||||||
|
|
||||||
|
if (!empty($decoded['errors'])) {
|
||||||
|
$msg = $decoded['errors'][0]['detail'] ?? ($decoded['errors'][0]['code'] ?? 'Unknown Square error');
|
||||||
|
throw new Exception($msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($httpCode >= 400) {
|
||||||
|
throw new Exception('Square API error (HTTP ' . $httpCode . ')');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $decoded;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a payment. $sourceId is the nonce from the Web Payments SDK's tokenize() call.
|
||||||
|
*/
|
||||||
|
function squareCreatePayment(string $sourceId, float $amount, string $referenceId, array $options = []): array {
|
||||||
|
$body = [
|
||||||
|
'source_id' => $sourceId,
|
||||||
|
'idempotency_key' => $referenceId . '_' . bin2hex(random_bytes(8)),
|
||||||
|
'amount_money' => [
|
||||||
|
'amount' => (int) round($amount * 100),
|
||||||
|
'currency' => 'USD',
|
||||||
|
],
|
||||||
|
'location_id' => SQUARE_LOCATION_ID,
|
||||||
|
'autocomplete' => true,
|
||||||
|
'reference_id' => $referenceId,
|
||||||
|
];
|
||||||
|
if (!empty($options['note'])) {
|
||||||
|
$body['note'] = $options['note'];
|
||||||
|
}
|
||||||
|
return squareApi('POST', '/payments', $body);
|
||||||
|
}
|
||||||
|
|
||||||
|
function squareGetPayment(string $paymentId): array {
|
||||||
|
return squareApi('GET', '/payments/' . $paymentId);
|
||||||
|
}
|
||||||
|
|
||||||
|
function squareRefundPayment(string $paymentId, float $amount, string $reason = ''): array {
|
||||||
|
$body = [
|
||||||
|
'idempotency_key' => 'refund_' . $paymentId . '_' . bin2hex(random_bytes(6)),
|
||||||
|
'amount_money' => [
|
||||||
|
'amount' => (int) round($amount * 100),
|
||||||
|
'currency' => 'USD',
|
||||||
|
],
|
||||||
|
'payment_id' => $paymentId,
|
||||||
|
];
|
||||||
|
if ($reason) {
|
||||||
|
$body['reason'] = substr($reason, 0, 192);
|
||||||
|
}
|
||||||
|
return squareApi('POST', '/refunds', $body);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isSquareConfigured(): bool {
|
||||||
|
return defined('SQUARE_ACCESS_TOKEN') && defined('SQUARE_APP_ID') && defined('SQUARE_LOCATION_ID')
|
||||||
|
&& !empty(SQUARE_ACCESS_TOKEN) && !empty(SQUARE_APP_ID) && !empty(SQUARE_LOCATION_ID)
|
||||||
|
&& SQUARE_ACCESS_TOKEN !== 'REPLACE_ME';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Square's webhook signature: base64(HMAC-SHA256(notification_url . raw_body, signature_key)).
|
||||||
|
* The notification URL must exactly match what's configured in the Square Dashboard subscription.
|
||||||
|
*/
|
||||||
|
function verifySquareWebhookSignature(string $payload, string $signature, string $notificationUrl, string $signatureKey): bool {
|
||||||
|
if (empty($signatureKey) || empty($signature)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
$expected = base64_encode(hash_hmac('sha256', $notificationUrl . $payload, $signatureKey, true));
|
||||||
|
return hash_equals($expected, $signature);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Single source of truth for "an order's Square payment resolved" — called from the
|
||||||
|
* synchronous create-payment response, the webhook, and the reconciliation poll, so
|
||||||
|
* loyalty points/emails are never awarded or sent more than once regardless of which
|
||||||
|
* path resolves the order first.
|
||||||
|
*/
|
||||||
|
function markSquarePaymentResult(string $paymentId, ?string $orderId, string $status): void {
|
||||||
|
$order = $orderId
|
||||||
|
? db()->fetch("SELECT * FROM orders WHERE order_id = :id", ['id' => $orderId])
|
||||||
|
: db()->fetch("SELECT * FROM orders WHERE square_payment_id = :pid", ['pid' => $paymentId]);
|
||||||
|
|
||||||
|
if (!$order) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($status === 'COMPLETED') {
|
||||||
|
if ($order['order_status'] !== 'confirmed') {
|
||||||
|
db()->update('orders',
|
||||||
|
[
|
||||||
|
'payment_status' => 'paid',
|
||||||
|
'order_status' => 'confirmed',
|
||||||
|
'square_payment_id' => $paymentId,
|
||||||
|
'payment_method' => 'square',
|
||||||
|
],
|
||||||
|
'order_id = :id',
|
||||||
|
['id' => $order['order_id']]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!empty($order['customer_id'])) {
|
||||||
|
loyalty()->awardPoints(
|
||||||
|
$order['customer_id'],
|
||||||
|
(float) $order['total'],
|
||||||
|
'Order #' . $order['order_number'],
|
||||||
|
$order['order_id']
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($order['wallet_amount_used']) && (float) $order['wallet_amount_used'] > 0 && !empty($order['customer_id'])) {
|
||||||
|
$walletAmount = (float) $order['wallet_amount_used'];
|
||||||
|
$cust = db()->fetch("SELECT wallet_balance FROM customers WHERE customer_id = :id", ['id' => $order['customer_id']]);
|
||||||
|
if ($cust) {
|
||||||
|
$newBalance = max(0, (float) $cust['wallet_balance'] - $walletAmount);
|
||||||
|
db()->update('customers', ['wallet_balance' => $newBalance], 'customer_id = :id', ['id' => $order['customer_id']]);
|
||||||
|
db()->insert('wallet_transactions', [
|
||||||
|
'transaction_id' => generateId('wt_'),
|
||||||
|
'customer_id' => $order['customer_id'],
|
||||||
|
'amount' => -$walletAmount,
|
||||||
|
'balance_after' => $newBalance,
|
||||||
|
'type' => 'purchase',
|
||||||
|
'description' => 'Applied to Order #' . $order['order_number'],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$updated = db()->fetch("SELECT * FROM orders WHERE order_id = :id", ['id' => $order['order_id']]);
|
||||||
|
if ($updated) {
|
||||||
|
emailService()->sendOrderConfirmation($updated);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} elseif (in_array($status, ['FAILED', 'CANCELED'], true)) {
|
||||||
|
db()->update('orders',
|
||||||
|
['payment_status' => 'failed'],
|
||||||
|
'order_id = :id',
|
||||||
|
['id' => $order['order_id']]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -88,6 +88,13 @@ require_once __DIR__ . '/includes/header.php';
|
|||||||
<span><?= $order['shipping_cost'] > 0 ? formatCurrency($order['shipping_cost']) : 'FREE' ?></span>
|
<span><?= $order['shipping_cost'] > 0 ? formatCurrency($order['shipping_cost']) : 'FREE' ?></span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<?php if (!empty($order['wallet_amount_used']) && (float) $order['wallet_amount_used'] > 0): ?>
|
||||||
|
<div style="display: flex; justify-content: space-between; padding: 0.75rem 0; border-bottom: 1px solid var(--color-border);">
|
||||||
|
<span>Wallet / Gift Card</span>
|
||||||
|
<span class="text-success">-<?= formatCurrency($order['wallet_amount_used']) ?></span>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
<div style="display: flex; justify-content: space-between; padding: 0.75rem 0; font-weight: 600; font-size: 1.125rem;">
|
<div style="display: flex; justify-content: space-between; padding: 0.75rem 0; font-weight: 600; font-size: 1.125rem;">
|
||||||
<span>Total</span>
|
<span>Total</span>
|
||||||
<span><?= formatCurrency($order['total']) ?></span>
|
<span><?= formatCurrency($order['total']) ?></span>
|
||||||
|
|||||||
+224
-50
@@ -1,13 +1,20 @@
|
|||||||
<?php
|
<?php
|
||||||
/**
|
/**
|
||||||
* Tom's Java Jive - Payment Page (Stripe)
|
* Tom's Java Jive - Payment Page
|
||||||
* Supports both PaymentIntent (card element) and Checkout Session (redirect) flows
|
* Supports Stripe (legacy) or Square, gated by the PAYMENT_PROCESSOR constant.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
$pageTitle = "Payment - Tom's Java Jive";
|
$pageTitle = "Payment - Tom's Java Jive";
|
||||||
require_once __DIR__ . '/includes/functions.php';
|
require_once __DIR__ . '/includes/functions.php';
|
||||||
require_once __DIR__ . '/includes/auth.php';
|
require_once __DIR__ . '/includes/auth.php';
|
||||||
|
|
||||||
|
$processor = defined('PAYMENT_PROCESSOR') ? PAYMENT_PROCESSOR : 'stripe';
|
||||||
|
|
||||||
|
if ($processor === 'square') {
|
||||||
|
require_once __DIR__ . '/includes/square.php';
|
||||||
|
} else {
|
||||||
require_once __DIR__ . '/includes/stripe.php';
|
require_once __DIR__ . '/includes/stripe.php';
|
||||||
|
}
|
||||||
|
|
||||||
$orderId = $_GET['order'] ?? $_SESSION['pending_order_id'] ?? '';
|
$orderId = $_GET['order'] ?? $_SESSION['pending_order_id'] ?? '';
|
||||||
$cancelled = isset($_GET['cancelled']);
|
$cancelled = isset($_GET['cancelled']);
|
||||||
@@ -16,7 +23,6 @@ if (empty($orderId)) {
|
|||||||
redirect('/cart.php');
|
redirect('/cart.php');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get order
|
|
||||||
$order = db()->fetch(
|
$order = db()->fetch(
|
||||||
"SELECT * FROM orders WHERE order_id = :id",
|
"SELECT * FROM orders WHERE order_id = :id",
|
||||||
['id' => $orderId]
|
['id' => $orderId]
|
||||||
@@ -26,15 +32,19 @@ if (!$order) {
|
|||||||
redirect('/cart.php');
|
redirect('/cart.php');
|
||||||
}
|
}
|
||||||
|
|
||||||
// If already paid, redirect to confirmation
|
|
||||||
if ($order['payment_status'] === 'paid') {
|
if ($order['payment_status'] === 'paid') {
|
||||||
clearCart();
|
clearCart();
|
||||||
redirect('/order-confirmation.php?order=' . $orderId);
|
redirect('/order-confirmation.php?order=' . $orderId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$total = $order['total'];
|
||||||
|
|
||||||
|
if ($processor === 'square') {
|
||||||
|
$squareConfigured = isSquareConfigured();
|
||||||
|
} else {
|
||||||
$stripePublishableKey = STRIPE_PUBLISHABLE_KEY;
|
$stripePublishableKey = STRIPE_PUBLISHABLE_KEY;
|
||||||
$stripeConfigured = isStripeConfigured();
|
$stripeConfigured = isStripeConfigured();
|
||||||
$total = $order['total'];
|
}
|
||||||
|
|
||||||
require_once __DIR__ . '/includes/header.php';
|
require_once __DIR__ . '/includes/header.php';
|
||||||
?>
|
?>
|
||||||
@@ -62,8 +72,61 @@ require_once __DIR__ . '/includes/header.php';
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<?php if ($processor === 'square'): ?>
|
||||||
|
<?php if (!$squareConfigured): ?>
|
||||||
|
<div class="alert alert-info mb-2">
|
||||||
|
<i class="fas fa-info-circle"></i> <strong>Demo Mode:</strong> Square is not configured. Click below to simulate a successful payment.
|
||||||
|
</div>
|
||||||
|
<form id="demo-payment-form">
|
||||||
|
<button type="submit" id="demo-submit" class="btn btn-primary btn-lg btn-block">
|
||||||
|
<span id="demo-text">Complete Demo Payment <?= formatCurrency($total) ?></span>
|
||||||
|
<span id="demo-spinner" style="display: none;">
|
||||||
|
<span class="loading"></span> Processing...
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
<?php else: ?>
|
||||||
|
<div class="payment-options mb-2" style="display: flex; gap: 0.5rem;">
|
||||||
|
<button type="button" id="pay-tab-card" class="btn btn-primary" style="flex: 1;">
|
||||||
|
<i class="fas fa-credit-card"></i> Card
|
||||||
|
</button>
|
||||||
|
<button type="button" id="pay-tab-giftcard" class="btn btn-secondary" style="flex: 1;">
|
||||||
|
<i class="fas fa-gift"></i> Gift Card
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form id="payment-form">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Card Details</label>
|
||||||
|
<div id="card-element" style="padding: 0.75rem; border: 1px solid var(--color-border); border-radius: var(--radius-md);"></div>
|
||||||
|
<div id="card-errors" class="form-error" style="margin-top: 0.5rem;"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" id="submit-button" class="btn btn-secondary btn-lg btn-block">
|
||||||
|
<span id="button-text">Pay <?= formatCurrency($total) ?></span>
|
||||||
|
<span id="spinner" style="display: none;">
|
||||||
|
<span class="loading"></span> Processing...
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<form id="gift-card-payment-form" style="display: none;">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">Square Gift Card</label>
|
||||||
|
<div id="gift-card-element" style="padding: 0.75rem; border: 1px solid var(--color-border); border-radius: var(--radius-md);"></div>
|
||||||
|
<div id="gift-card-errors" class="form-error" style="margin-top: 0.5rem;"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" id="gift-card-submit-button" class="btn btn-secondary btn-lg btn-block">
|
||||||
|
<span id="gift-card-button-text">Pay <?= formatCurrency($total) ?></span>
|
||||||
|
<span id="gift-card-spinner" style="display: none;">
|
||||||
|
<span class="loading"></span> Processing...
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php else: ?>
|
||||||
<?php if (!$stripeConfigured): ?>
|
<?php if (!$stripeConfigured): ?>
|
||||||
<!-- Demo Mode - No Stripe Keys -->
|
|
||||||
<div class="alert alert-info mb-2">
|
<div class="alert alert-info mb-2">
|
||||||
<i class="fas fa-info-circle"></i> <strong>Demo Mode:</strong> Stripe is not configured. Click below to simulate a successful payment.
|
<i class="fas fa-info-circle"></i> <strong>Demo Mode:</strong> Stripe is not configured. Click below to simulate a successful payment.
|
||||||
</div>
|
</div>
|
||||||
@@ -76,7 +139,6 @@ require_once __DIR__ . '/includes/header.php';
|
|||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
<?php else: ?>
|
<?php else: ?>
|
||||||
<!-- Stripe Payment Options -->
|
|
||||||
<div class="payment-options mb-2">
|
<div class="payment-options mb-2">
|
||||||
<button type="button" id="checkout-btn" class="btn btn-primary btn-lg btn-block mb-1">
|
<button type="button" id="checkout-btn" class="btn btn-primary btn-lg btn-block mb-1">
|
||||||
<i class="fas fa-credit-card"></i> Pay with Stripe Checkout
|
<i class="fas fa-credit-card"></i> Pay with Stripe Checkout
|
||||||
@@ -99,6 +161,7 @@ require_once __DIR__ . '/includes/header.php';
|
|||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
<div id="payment-message" style="display: none; margin-top: 1rem;"></div>
|
<div id="payment-message" style="display: none; margin-top: 1rem;"></div>
|
||||||
|
|
||||||
@@ -110,9 +173,12 @@ require_once __DIR__ . '/includes/header.php';
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<?php if ($processor === 'square' && $squareConfigured): ?>
|
||||||
|
<script src="<?= (defined('SQUARE_ENV') && SQUARE_ENV === 'sandbox') ? 'https://sandbox.web.squarecdn.com/v1/square.js' : 'https://web.squarecdn.com/v1/square.js' ?>"></script>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
const orderId = '<?= $orderId ?>';
|
const orderId = '<?= $orderId ?>';
|
||||||
const stripeConfigured = <?= $stripeConfigured ? 'true' : 'false' ?>;
|
|
||||||
const messageEl = document.getElementById('payment-message');
|
const messageEl = document.getElementById('payment-message');
|
||||||
|
|
||||||
function showMessage(message, type = 'info') {
|
function showMessage(message, type = 'info') {
|
||||||
@@ -121,29 +187,23 @@ function showMessage(message, type = 'info') {
|
|||||||
messageEl.innerHTML = `<i class="fas fa-${type === 'error' ? 'exclamation-circle' : 'check-circle'}"></i> ${message}`;
|
messageEl.innerHTML = `<i class="fas fa-${type === 'error' ? 'exclamation-circle' : 'check-circle'}"></i> ${message}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
<?php if (!$stripeConfigured): ?>
|
function wireDemoForm(endpoint) {
|
||||||
// Demo mode payment
|
|
||||||
const demoForm = document.getElementById('demo-payment-form');
|
const demoForm = document.getElementById('demo-payment-form');
|
||||||
demoForm.addEventListener('submit', async function(e) {
|
demoForm.addEventListener('submit', async function(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
const btn = document.getElementById('demo-submit');
|
const btn = document.getElementById('demo-submit');
|
||||||
const text = document.getElementById('demo-text');
|
const text = document.getElementById('demo-text');
|
||||||
const spinner = document.getElementById('demo-spinner');
|
const spinner = document.getElementById('demo-spinner');
|
||||||
|
|
||||||
btn.disabled = true;
|
btn.disabled = true;
|
||||||
text.style.display = 'none';
|
text.style.display = 'none';
|
||||||
spinner.style.display = 'inline';
|
spinner.style.display = 'inline';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/create-payment-intent.php', {
|
const response = await fetch(endpoint, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ order_id: orderId })
|
body: JSON.stringify({ order_id: orderId })
|
||||||
});
|
});
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
if (data.demo_mode && data.redirect) {
|
if (data.demo_mode && data.redirect) {
|
||||||
showMessage('Payment simulated successfully! Redirecting...', 'success');
|
showMessage('Payment simulated successfully! Redirecting...', 'success');
|
||||||
setTimeout(() => window.location.href = data.redirect, 1000);
|
setTimeout(() => window.location.href = data.redirect, 1000);
|
||||||
@@ -160,49 +220,172 @@ demoForm.addEventListener('submit', async function(e) {
|
|||||||
spinner.style.display = 'none';
|
spinner.style.display = 'none';
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
<?php if ($processor === 'square'): ?>
|
||||||
|
<?php if (!$squareConfigured): ?>
|
||||||
|
wireDemoForm('/api/create-square-payment.php');
|
||||||
|
<?php else: ?>
|
||||||
|
// Square Web Payments SDK
|
||||||
|
let squareCard, squareGiftCard;
|
||||||
|
const squarePayments = window.Square.payments('<?= SQUARE_APP_ID ?>', '<?= SQUARE_LOCATION_ID ?>');
|
||||||
|
(async function initSquare() {
|
||||||
|
squareCard = await squarePayments.card();
|
||||||
|
await squareCard.attach('#card-element');
|
||||||
|
})();
|
||||||
|
|
||||||
|
const cardTab = document.getElementById('pay-tab-card');
|
||||||
|
const giftCardTab = document.getElementById('pay-tab-giftcard');
|
||||||
|
const cardForm = document.getElementById('payment-form');
|
||||||
|
const giftCardForm = document.getElementById('gift-card-payment-form');
|
||||||
|
let giftCardInitialized = false;
|
||||||
|
|
||||||
|
cardTab.addEventListener('click', function() {
|
||||||
|
cardTab.className = 'btn btn-primary';
|
||||||
|
giftCardTab.className = 'btn btn-secondary';
|
||||||
|
cardForm.style.display = '';
|
||||||
|
giftCardForm.style.display = 'none';
|
||||||
|
});
|
||||||
|
|
||||||
|
giftCardTab.addEventListener('click', async function() {
|
||||||
|
cardTab.className = 'btn btn-secondary';
|
||||||
|
giftCardTab.className = 'btn btn-primary';
|
||||||
|
cardForm.style.display = 'none';
|
||||||
|
giftCardForm.style.display = '';
|
||||||
|
if (!giftCardInitialized) {
|
||||||
|
giftCardInitialized = true;
|
||||||
|
squareGiftCard = await squarePayments.giftCard();
|
||||||
|
await squareGiftCard.attach('#gift-card-element');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
giftCardForm.addEventListener('submit', async function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
const submitBtn = document.getElementById('gift-card-submit-button');
|
||||||
|
const buttonText = document.getElementById('gift-card-button-text');
|
||||||
|
const spinner = document.getElementById('gift-card-spinner');
|
||||||
|
submitBtn.disabled = true;
|
||||||
|
buttonText.style.display = 'none';
|
||||||
|
spinner.style.display = 'inline';
|
||||||
|
document.getElementById('gift-card-errors').textContent = '';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const tokenResult = await squareGiftCard.tokenize();
|
||||||
|
if (tokenResult.status !== 'OK') {
|
||||||
|
const msg = (tokenResult.errors && tokenResult.errors[0] && tokenResult.errors[0].message) || 'Gift card validation failed';
|
||||||
|
document.getElementById('gift-card-errors').textContent = msg;
|
||||||
|
submitBtn.disabled = false;
|
||||||
|
buttonText.style.display = 'inline';
|
||||||
|
spinner.style.display = 'none';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch('/api/create-square-payment.php', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ order_id: orderId, source_id: tokenResult.token })
|
||||||
|
});
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.error) {
|
||||||
|
showMessage(data.error, 'error');
|
||||||
|
submitBtn.disabled = false;
|
||||||
|
buttonText.style.display = 'inline';
|
||||||
|
spinner.style.display = 'none';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (data.success && data.redirect) {
|
||||||
|
showMessage('Payment successful! Redirecting...', 'success');
|
||||||
|
setTimeout(() => window.location.href = data.redirect, 1000);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
showMessage('Payment failed. Please try again.', 'error');
|
||||||
|
console.error(err);
|
||||||
|
submitBtn.disabled = false;
|
||||||
|
buttonText.style.display = 'inline';
|
||||||
|
spinner.style.display = 'none';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const form = document.getElementById('payment-form');
|
||||||
|
const submitButton = document.getElementById('submit-button');
|
||||||
|
const buttonText = document.getElementById('button-text');
|
||||||
|
const spinner = document.getElementById('spinner');
|
||||||
|
|
||||||
|
form.addEventListener('submit', async function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
submitButton.disabled = true;
|
||||||
|
buttonText.style.display = 'none';
|
||||||
|
spinner.style.display = 'inline';
|
||||||
|
document.getElementById('card-errors').textContent = '';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const tokenResult = await squareCard.tokenize();
|
||||||
|
if (tokenResult.status !== 'OK') {
|
||||||
|
const msg = (tokenResult.errors && tokenResult.errors[0] && tokenResult.errors[0].message) || 'Card validation failed';
|
||||||
|
document.getElementById('card-errors').textContent = msg;
|
||||||
|
submitButton.disabled = false;
|
||||||
|
buttonText.style.display = 'inline';
|
||||||
|
spinner.style.display = 'none';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch('/api/create-square-payment.php', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ order_id: orderId, source_id: tokenResult.token })
|
||||||
|
});
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.demo_mode && data.redirect) {
|
||||||
|
window.location.href = data.redirect;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (data.error) {
|
||||||
|
showMessage(data.error, 'error');
|
||||||
|
submitButton.disabled = false;
|
||||||
|
buttonText.style.display = 'inline';
|
||||||
|
spinner.style.display = 'none';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (data.success && data.redirect) {
|
||||||
|
showMessage('Payment successful! Redirecting...', 'success');
|
||||||
|
setTimeout(() => window.location.href = data.redirect, 1000);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
showMessage('Payment failed. Please try again.', 'error');
|
||||||
|
console.error(err);
|
||||||
|
submitButton.disabled = false;
|
||||||
|
buttonText.style.display = 'inline';
|
||||||
|
spinner.style.display = 'none';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php else: ?>
|
||||||
|
<?php if (!$stripeConfigured): ?>
|
||||||
|
wireDemoForm('/api/create-payment-intent.php');
|
||||||
<?php else: ?>
|
<?php else: ?>
|
||||||
// Stripe initialized
|
|
||||||
const stripe = Stripe('<?= $stripePublishableKey ?>');
|
const stripe = Stripe('<?= $stripePublishableKey ?>');
|
||||||
const elements = stripe.elements();
|
const elements = stripe.elements();
|
||||||
const cardElement = elements.create('card', {
|
const cardElement = elements.create('card', {
|
||||||
style: {
|
style: { base: { fontSize: '16px', color: '#1B1B1B', '::placeholder': { color: '#9CA3AF' } } }
|
||||||
base: {
|
|
||||||
fontSize: '16px',
|
|
||||||
color: '#1B1B1B',
|
|
||||||
'::placeholder': { color: '#9CA3AF' }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
cardElement.mount('#card-element');
|
cardElement.mount('#card-element');
|
||||||
|
|
||||||
// Handle validation errors
|
|
||||||
cardElement.on('change', function(event) {
|
cardElement.on('change', function(event) {
|
||||||
const displayError = document.getElementById('card-errors');
|
const displayError = document.getElementById('card-errors');
|
||||||
if (event.error) {
|
displayError.textContent = event.error ? event.error.message : '';
|
||||||
displayError.textContent = event.error.message;
|
|
||||||
} else {
|
|
||||||
displayError.textContent = '';
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Stripe Checkout button (redirect to hosted page)
|
|
||||||
document.getElementById('checkout-btn').addEventListener('click', async function() {
|
document.getElementById('checkout-btn').addEventListener('click', async function() {
|
||||||
this.disabled = true;
|
this.disabled = true;
|
||||||
this.innerHTML = '<span class="loading"></span> Redirecting to Stripe...';
|
this.innerHTML = '<span class="loading"></span> Redirecting to Stripe...';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/create-checkout-session.php', {
|
const response = await fetch('/api/create-checkout-session.php', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({ order_id: orderId, origin_url: window.location.origin })
|
||||||
order_id: orderId,
|
|
||||||
origin_url: window.location.origin
|
|
||||||
})
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
if (data.demo_mode && data.redirect) {
|
if (data.demo_mode && data.redirect) {
|
||||||
window.location.href = data.redirect;
|
window.location.href = data.redirect;
|
||||||
} else if (data.url) {
|
} else if (data.url) {
|
||||||
@@ -219,7 +402,6 @@ document.getElementById('checkout-btn').addEventListener('click', async function
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// PaymentIntent form (inline card element)
|
|
||||||
const form = document.getElementById('payment-form');
|
const form = document.getElementById('payment-form');
|
||||||
const submitButton = document.getElementById('submit-button');
|
const submitButton = document.getElementById('submit-button');
|
||||||
const buttonText = document.getElementById('button-text');
|
const buttonText = document.getElementById('button-text');
|
||||||
@@ -227,25 +409,20 @@ const spinner = document.getElementById('spinner');
|
|||||||
|
|
||||||
form.addEventListener('submit', async function(e) {
|
form.addEventListener('submit', async function(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
submitButton.disabled = true;
|
submitButton.disabled = true;
|
||||||
buttonText.style.display = 'none';
|
buttonText.style.display = 'none';
|
||||||
spinner.style.display = 'inline';
|
spinner.style.display = 'inline';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/create-payment-intent.php', {
|
const response = await fetch('/api/create-payment-intent.php', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ order_id: orderId })
|
body: JSON.stringify({ order_id: orderId })
|
||||||
});
|
});
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
if (data.demo_mode && data.redirect) {
|
if (data.demo_mode && data.redirect) {
|
||||||
window.location.href = data.redirect;
|
window.location.href = data.redirect;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.error) {
|
if (data.error) {
|
||||||
showMessage(data.error, 'error');
|
showMessage(data.error, 'error');
|
||||||
submitButton.disabled = false;
|
submitButton.disabled = false;
|
||||||
@@ -253,8 +430,6 @@ form.addEventListener('submit', async function(e) {
|
|||||||
spinner.style.display = 'none';
|
spinner.style.display = 'none';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Confirm payment with Stripe
|
|
||||||
const { error, paymentIntent } = await stripe.confirmCardPayment(data.client_secret, {
|
const { error, paymentIntent } = await stripe.confirmCardPayment(data.client_secret, {
|
||||||
payment_method: {
|
payment_method: {
|
||||||
card: cardElement,
|
card: cardElement,
|
||||||
@@ -264,14 +439,12 @@ form.addEventListener('submit', async function(e) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
showMessage(error.message, 'error');
|
showMessage(error.message, 'error');
|
||||||
} else if (paymentIntent.status === 'succeeded') {
|
} else if (paymentIntent.status === 'succeeded') {
|
||||||
showMessage('Payment successful! Redirecting...', 'success');
|
showMessage('Payment successful! Redirecting...', 'success');
|
||||||
setTimeout(() => window.location.href = '/order-confirmation.php?order=' + orderId, 1000);
|
setTimeout(() => window.location.href = '/order-confirmation.php?order=' + orderId, 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
showMessage('Payment failed. Please try again.', 'error');
|
showMessage('Payment failed. Please try again.', 'error');
|
||||||
console.error(err);
|
console.error(err);
|
||||||
@@ -282,6 +455,7 @@ form.addEventListener('submit', async function(e) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
<?php endif; ?>
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<?php require_once __DIR__ . '/includes/footer.php'; ?>
|
<?php require_once __DIR__ . '/includes/footer.php'; ?>
|
||||||
|
|||||||
Reference in New Issue
Block a user