';
}
// Get product categories
$categories = get_terms([
'taxonomy' => 'product_cat',
'hide_empty' => false,
]);
include LRWC_PATH . 'templates/admin-settings.php';
}
// ========================
// WOOCOMMERCE INTEGRATION
// ========================
public function award_points_on_order($order_id) {
if (get_option('lrwc_auto_award_orders') !== 'yes') {
return;
}
$order = wc_get_order($order_id);
if (!$order) return;
// Skip rewards redemption orders (staff scanner orders paid fully with points)
if ($order->get_meta('_lrwc_rewards_redemption') === 'yes') {
return;
}
// Check if already awarded
if ($order->get_meta('_lrwc_points_awarded')) {
return;
}
$customer_id = $order->get_customer_id();
if (!$customer_id) return;
$min_total = floatval(get_option('lrwc_min_order_total', 0));
$order_total = floatval($order->get_total());
// Get the actual amount paid (excluding points discount)
// Check if points were used at checkout
$checkout_points_used = intval($order->get_meta('_lrwc_checkout_points_used'));
$checkout_discount = floatval($order->get_meta('_lrwc_checkout_discount'));
// Also check for staff redemption points discount
$staff_points_discount = floatval($order->get_meta('_lrwc_points_discount'));
// Calculate the amount that was actually paid with money (not points)
// Order total already has the discount applied, so we use the order total
// But we should only award points on the actual cash paid, not the discounted amount
$amount_paid_with_cash = $order_total; // This is already the discounted total
// If the entire order was paid with points, don't award any points
if ($amount_paid_with_cash <= 0) {
return;
}
if ($amount_paid_with_cash < $min_total) return;
$points = $this->calculate_points($amount_paid_with_cash);
if ($points > 0) {
$note = 'Order #' . $order_id;
if ($checkout_points_used > 0 || $staff_points_discount > 0) {
$note .= ' (cash portion only)';
}
$this->add_points($customer_id, $points, 'earn', null, $order_id, null, $note);
$order->update_meta_data('_lrwc_points_awarded', $points);
$order->save();
}
}
/**
* Apply points discount as a negative fee in cart
*/
public function apply_points_discount_fee($cart) {
if (is_admin() && !defined('DOING_AJAX')) return;
if (!WC()->session) return;
$points_data = WC()->session->get('lrwc_points_discount');
if (!$points_data || empty($points_data['discount'])) return;
$discount = floatval($points_data['discount']);
$cart_total = $cart->get_subtotal();
// Don't discount more than cart total
if ($discount > $cart_total) {
$discount = $cart_total;
}
if ($discount > 0) {
$cart->add_fee(__('Points Redemption', '800website-loyalty-rewards'), -$discount, false);
}
}
/**
* Process points redemption when order is placed
*/
public function process_checkout_points_redemption($order_id, $posted_data, $order) {
if (!WC()->session) return;
$points_data = WC()->session->get('lrwc_points_discount');
if (!$points_data || empty($points_data['points'])) return;
$customer_id = $order->get_customer_id();
if (!$customer_id) return;
$points_used = intval($points_data['points']);
$discount_amount = floatval($points_data['discount']);
// Verify customer still has enough points
$available = $this->get_points_balance($customer_id);
if ($points_used > $available) {
$points_used = $available;
}
if ($points_used > 0) {
// Deduct points
$this->add_points(
$customer_id,
-$points_used,
'redeem',
null,
$order_id,
null,
'Checkout discount - Order #' . $order_id
);
// Store on order
$order->update_meta_data('_lrwc_checkout_points_used', $points_used);
$order->update_meta_data('_lrwc_checkout_discount', $discount_amount);
$order->save();
$order->add_order_note(sprintf(
'💳 %s points redeemed for %s discount',
number_format($points_used),
wc_price($discount_amount)
));
}
// Clear session
WC()->session->set('lrwc_points_discount', null);
}
/**
* Display points redemption option on checkout
*/
public function display_points_redemption_option() {
if (get_option('lrwc_enable_checkout_redemption', 'yes') !== 'yes') return;
if (!is_user_logged_in()) return;
$customer_id = get_current_user_id();
$points = $this->get_points_balance($customer_id);
if ($points <= 0) return;
$point_value = floatval(get_option('lrwc_point_value', 1));
$max_discount = $points * $point_value;
$cart_total = WC()->cart->get_subtotal();
// Already applied?
$points_data = WC()->session ? WC()->session->get('lrwc_points_discount') : null;
$applied_points = $points_data ? intval($points_data['points']) : 0;
$currency = get_woocommerce_currency_symbol();
?>
🎁 Redeem Your Points
You have points available
(worth up to )
0): ?>
✓ Using points
( discount)
= 0 discount
1 point =
get_points_balance($customer_id);
if ($points > $available) {
wp_send_json_error('Not enough points');
}
$point_value = floatval(get_option('lrwc_point_value', 1));
$discount = $points * $point_value;
WC()->session->set('lrwc_points_discount', [
'points' => $points,
'discount' => $discount
]);
wp_send_json_success(['points' => $points, 'discount' => $discount]);
}
/**
* AJAX handler to remove points at checkout
*/
public function ajax_remove_checkout_points() {
check_ajax_referer('lrwc_checkout_points', 'nonce');
WC()->session->set('lrwc_points_discount', null);
wp_send_json_success();
}
/**
* AJAX add to cart handler for rewards
*/
public function ajax_add_to_cart() {
$product_id = absint($_POST['product_id']);
$quantity = empty($_POST['quantity']) ? 1 : wc_stock_amount($_POST['quantity']);
// Ensure WC is loaded
if (!function_exists('WC') || !WC()->cart) {
wp_send_json(['error' => true, 'message' => 'WooCommerce not available']);
return;
}
$product = wc_get_product($product_id);
if (!$product) {
wp_send_json(['error' => true, 'message' => 'Product not found']);
return;
}
// Check if product is purchasable
if (!$product->is_purchasable()) {
wp_send_json(['error' => true, 'message' => 'This product cannot be purchased']);
return;
}
// Check stock
if (!$product->is_in_stock()) {
wp_send_json(['error' => true, 'message' => 'Product is out of stock']);
return;
}
// Add to cart
$cart_item_key = WC()->cart->add_to_cart($product_id, $quantity);
if ($cart_item_key) {
do_action('woocommerce_ajax_added_to_cart', $product_id);
// Get mini cart HTML
ob_start();
woocommerce_mini_cart();
$mini_cart = ob_get_clean();
// Get cart count
$cart_count = WC()->cart->get_cart_contents_count();
// Get fragments
$fragments = apply_filters('woocommerce_add_to_cart_fragments', [
'div.widget_shopping_cart_content' => '
' . $mini_cart . '
',
]);
wp_send_json([
'success' => true,
'fragments' => $fragments,
'cart_hash' => WC()->cart->get_cart_hash(),
'cart_count' => $cart_count,
'message' => $product->get_name() . ' added to cart'
]);
} else {
// Get WooCommerce notices
$notices = wc_get_notices('error');
wc_clear_notices();
$error_msg = !empty($notices) ? wp_strip_all_tags($notices[0]['notice']) : 'Could not add to cart';
wp_send_json([
'error' => true,
'message' => $error_msg
]);
}
wp_die();
}
/**
* Display REWARDS REDEMPTION badge on order admin page
*/
public function display_rewards_redemption_badge($order) {
if ($order->get_meta('_lrwc_rewards_redemption') === 'yes') {
$points_used = $order->get_meta('_lrwc_points_used');
?>
🎁 REWARDS REDEMPTION
This order was paid with loyalty points
get_meta('_lrwc_checkout_points_used');
if ($checkout_points) {
$checkout_discount = $order->get_meta('_lrwc_checkout_discount');
?>
💳 Points Used at Checkout
points = discount
get_meta('_lrwc_rewards_redemption') === 'yes') {
$data['status_name'] = '🎁 ' . $data['status_name'] . ' (REWARDS)';
}
return $data;
}
public function add_account_menu_item($items) {
$new_items = [];
foreach ($items as $key => $value) {
$new_items[$key] = $value;
if ($key === 'orders') {
$new_items['lrwc-loyalty-points'] = 'Loyalty Points';
}
}
return $new_items;
}
public function account_loyalty_content() {
$customer_id = get_current_user_id();
$points = $this->get_points_balance($customer_id);
$tier = $this->get_tier($points);
$transactions = $this->get_transactions($customer_id, 20);
$rewards = $this->get_available_rewards();
include LRWC_PATH . 'templates/myaccount-loyalty.php';
}
public function add_order_column($columns) {
$new_columns = [];
foreach ($columns as $key => $value) {
$new_columns[$key] = $value;
if ($key === 'order_total') {
$new_columns['lrwc_points'] = 'Points';
}
}
return $new_columns;
}
public function order_column_content($column, $post_id) {
if ($column === 'lrwc_points') {
$order = wc_get_order($post_id);
$points = $order ? $order->get_meta('_lrwc_points_awarded') : 0;
echo $points ? '+' . intval($points) : '-';
}
}
public function order_column_content_hpos($column, $order) {
if ($column === 'lrwc_points') {
$points = $order->get_meta('_lrwc_points_awarded');
echo $points ? '+' . intval($points) : '-';
}
}
// ========================
// POINTS HELPERS
// ========================
public function get_points_balance($customer_id) {
return intval(get_user_meta($customer_id, 'lrwc_points_balance', true));
}
public function update_points_balance($customer_id, $points) {
update_user_meta($customer_id, 'lrwc_points_balance', max(0, $points));
}
public function add_points($customer_id, $points, $type, $staff_id = null, $order_id = null, $product_id = null, $note = '') {
global $wpdb;
$current = $this->get_points_balance($customer_id);
if ($type === 'redeem') {
$points = -abs($points);
}
$new_balance = max(0, $current + $points);
$this->update_points_balance($customer_id, $new_balance);
// Log transaction
$wpdb->insert($wpdb->prefix . 'lrwc_transactions', [
'customer_id' => $customer_id,
'staff_id' => $staff_id,
'type' => $type,
'points' => $points,
'order_id' => $order_id,
'product_id' => $product_id,
'note' => $note,
'created_at' => current_time('mysql')
]);
return $new_balance;
}
public function get_transactions($customer_id, $limit = 50) {
global $wpdb;
return $wpdb->get_results($wpdb->prepare("
SELECT * FROM {$wpdb->prefix}lrwc_transactions
WHERE customer_id = %d
ORDER BY created_at DESC
LIMIT %d
", $customer_id, $limit));
}
public function get_tier($points) {
$platinum = intval(get_option('lrwc_tier_platinum', 5000));
$gold = intval(get_option('lrwc_tier_gold', 1500));
$silver = intval(get_option('lrwc_tier_silver', 500));
if ($points >= $platinum) return 'Platinum';
if ($points >= $gold) return 'Gold';
if ($points >= $silver) return 'Silver';
return 'Bronze';
}
/**
* Calculate points for a given order amount
* Supports two modes:
* - currency_per_point: e.g., 200 J.J = 1 point
* - points_per_currency: e.g., 0.005 points per 1 J.J
*/
public function calculate_points($order_total) {
$mode = get_option('lrwc_points_mode', 'currency_per_point');
$order_total = floatval($order_total);
if ($mode === 'currency_per_point') {
// e.g., 200 currency = 1 point
$currency_per_point = floatval(get_option('lrwc_currency_per_point', 1));
if ($currency_per_point <= 0) $currency_per_point = 1;
$points = floor($order_total / $currency_per_point);
} else {
// e.g., 0.005 points per 1 currency
$points_rate = floatval(get_option('lrwc_points_per_currency', 1));
$points = floor($order_total * $points_rate);
}
// Ensure we return a reasonable number (prevent overflow)
return max(0, min($points, PHP_INT_MAX));
}
public function get_available_rewards() {
$category_id = get_option('lrwc_rewards_category');
if (!$category_id) return [];
$args = [
'post_type' => 'product',
'posts_per_page' => -1,
'post_status' => 'publish',
'tax_query' => [
[
'taxonomy' => 'product_cat',
'field' => 'term_id',
'terms' => $category_id,
]
]
];
$products = get_posts($args);
$rewards = [];
foreach ($products as $product) {
$wc_product = wc_get_product($product->ID);
if (!$wc_product) continue;
// Points cost = custom meta or product price
$points_cost = get_post_meta($product->ID, '_lrwc_points_cost', true);
if (!$points_cost) {
$points_cost = intval($wc_product->get_price());
}
$rewards[] = [
'id' => $product->ID,
'name' => $product->post_title,
'description' => $wc_product->get_short_description(),
'image' => get_the_post_thumbnail_url($product->ID, 'thumbnail'),
'points_cost' => intval($points_cost),
'price' => floatval($wc_product->get_price()),
'price_html' => $wc_product->get_price_html(),
'in_stock' => $wc_product->is_in_stock(),
'stock' => $wc_product->is_in_stock(), // backwards compatibility
];
}
return $rewards;
}
public function generate_qr_code_data($customer_id) {
$user = get_user_by('ID', $customer_id);
if (!$user) return '';
// Create a unique hash for this customer
$hash = wp_hash($customer_id . $user->user_email . 'lrwc_salt');
return json_encode([
'id' => $customer_id,
'hash' => substr($hash, 0, 16),
'site' => get_site_url()
]);
}
// ========================
// REST API
// ========================
/**
* Permission callback for staff endpoints - validates staff token.
*/
public function check_staff_permission($request) {
return (bool) $this->verify_staff_token($request);
}
/**
* Permission callback for logged-in customer endpoints.
*/
public function check_logged_in_permission() {
return is_user_logged_in();
}
public function register_api() {
// Customer endpoints
register_rest_route('lrwc/v1', '/customer/profile', [
'methods' => 'GET',
'callback' => [$this, 'api_customer_profile'],
'permission_callback' => function() {
return is_user_logged_in();
}
]);
register_rest_route('lrwc/v1', '/customer/transactions', [
'methods' => 'GET',
'callback' => [$this, 'api_customer_transactions'],
'permission_callback' => function() {
return is_user_logged_in();
}
]);
// Public endpoint: rewards catalog is publicly viewable.
register_rest_route('lrwc/v1', '/rewards', [
'methods' => 'GET',
'callback' => [$this, 'api_rewards'],
'permission_callback' => '__return_true' // Intentionally public - displays reward catalog.
]);
register_rest_route('lrwc/v1', '/redeem', [
'methods' => 'POST',
'callback' => [$this, 'api_redeem'],
'permission_callback' => function() {
return is_user_logged_in();
}
]);
// Staff login endpoint: must be public; authentication is handled in the callback.
register_rest_route('lrwc/v1', '/staff/login', [
'methods' => 'POST',
'callback' => [$this, 'api_staff_login'],
'permission_callback' => '__return_true' // Intentionally public - credentials verified in callback.
]);
register_rest_route('lrwc/v1', '/staff/scan', [
'methods' => 'POST',
'callback' => [$this, 'api_staff_scan'],
'permission_callback' => [$this, 'check_staff_permission']
]);
register_rest_route('lrwc/v1', '/staff/add-points', [
'methods' => 'POST',
'callback' => [$this, 'api_staff_add_points'],
'permission_callback' => [$this, 'check_staff_permission']
]);
register_rest_route('lrwc/v1', '/staff/process-redemption', [
'methods' => 'POST',
'callback' => [$this, 'api_staff_process_redemption'],
'permission_callback' => [$this, 'check_staff_permission']
]);
register_rest_route('lrwc/v1', '/staff/pending-redemptions', [
'methods' => 'GET',
'callback' => [$this, 'api_staff_pending_redemptions'],
'permission_callback' => [$this, 'check_staff_permission']
]);
// Staff rewards list for redemption
register_rest_route('lrwc/v1', '/staff/rewards', [
'methods' => 'GET',
'callback' => [$this, 'api_staff_rewards'],
'permission_callback' => [$this, 'check_staff_permission']
]);
// Staff redeem product for customer
register_rest_route('lrwc/v1', '/staff/redeem-product', [
'methods' => 'POST',
'callback' => [$this, 'api_staff_redeem_product'],
'permission_callback' => [$this, 'check_staff_permission']
]);
// Staff lookup customer by phone/email/username
register_rest_route('lrwc/v1', '/staff/lookup', [
'methods' => 'POST',
'callback' => [$this, 'api_staff_lookup_customer'],
'permission_callback' => [$this, 'check_staff_permission']
]);
// Staff create order with multiple products
register_rest_route('lrwc/v1', '/staff/create-order', [
'methods' => 'POST',
'callback' => [$this, 'api_staff_create_order'],
'permission_callback' => [$this, 'check_staff_permission']
]);
// Customer checkout points
register_rest_route('lrwc/v1', '/checkout/apply-points', [
'methods' => 'POST',
'callback' => [$this, 'api_apply_points_to_checkout'],
'permission_callback' => [$this, 'check_logged_in_permission']
]);
register_rest_route('lrwc/v1', '/checkout/remove-points', [
'methods' => 'POST',
'callback' => [$this, 'api_remove_points_from_checkout'],
'permission_callback' => [$this, 'check_logged_in_permission']
]);
}
public function api_customer_profile($request) {
$customer_id = get_current_user_id();
$user = get_user_by('ID', $customer_id);
$points = $this->get_points_balance($customer_id);
// Get total earned
global $wpdb;
$total_earned = $wpdb->get_var($wpdb->prepare(
"SELECT COALESCE(SUM(points), 0) FROM {$wpdb->prefix}lrwc_transactions WHERE customer_id = %d AND type IN ('earn', 'bonus', 'manual')",
$customer_id
));
$order_count = wc_get_customer_order_count($customer_id);
return [
'success' => true,
'customer' => [
'id' => $customer_id,
'name' => $user->display_name,
'email' => $user->user_email,
'points' => $points,
'tier' => $this->get_tier($points),
'total_earned' => intval($total_earned),
'order_count' => $order_count,
'qr_data' => $this->generate_qr_code_data($customer_id)
]
];
}
public function api_customer_transactions($request) {
$customer_id = get_current_user_id();
$transactions = $this->get_transactions($customer_id, 50);
return [
'success' => true,
'transactions' => array_map(function($t) {
return [
'id' => $t->id,
'type' => $t->type,
'points' => intval($t->points),
'note' => $t->note,
'created_at' => $t->created_at
];
}, $transactions)
];
}
public function api_rewards($request) {
return [
'success' => true,
'rewards' => $this->get_available_rewards()
];
}
public function api_redeem($request) {
global $wpdb;
$customer_id = get_current_user_id();
$product_id = intval($request->get_param('product_id'));
$rewards = $this->get_available_rewards();
$reward = null;
foreach ($rewards as $r) {
if ($r['id'] == $product_id) {
$reward = $r;
break;
}
}
if (!$reward) {
return new WP_Error('invalid_reward', 'Reward not found', ['status' => 400]);
}
if (!$reward['stock']) {
return new WP_Error('out_of_stock', 'Reward is out of stock', ['status' => 400]);
}
$balance = $this->get_points_balance($customer_id);
if ($balance < $reward['points_cost']) {
return new WP_Error('insufficient_points', 'Not enough points', ['status' => 400]);
}
// Create redemption code
$code = 'LRW-' . strtoupper(wp_generate_password(8, false));
// Create redemption record
$wpdb->insert($wpdb->prefix . 'lrwc_redemptions', [
'customer_id' => $customer_id,
'product_id' => $product_id,
'points_used' => $reward['points_cost'],
'status' => 'pending',
'code' => $code,
'created_at' => current_time('mysql')
]);
// Deduct points
$this->add_points($customer_id, $reward['points_cost'], 'redeem', null, null, $product_id, 'Redeemed: ' . $reward['name']);
return [
'success' => true,
'message' => 'Reward redeemed successfully!',
'code' => $code,
'new_balance' => $this->get_points_balance($customer_id)
];
}
public function api_staff_login($request) {
global $wpdb;
$email = sanitize_email($request->get_param('email'));
$pin = sanitize_text_field($request->get_param('pin'));
$user = get_user_by('email', $email);
if (!$user) {
return new WP_Error('invalid_credentials', 'Invalid email or PIN', ['status' => 401]);
}
$staff = $wpdb->get_row($wpdb->prepare(
"SELECT * FROM {$wpdb->prefix}lrwc_staff WHERE user_id = %d AND pin = %s AND status = 'active'",
$user->ID, $pin
));
if (!$staff) {
return new WP_Error('invalid_credentials', 'Invalid email or PIN', ['status' => 401]);
}
// Generate token with staff ID embedded for easy lookup
$token = 'lrwc_' . $staff->id . '_' . md5($user->ID . time() . wp_rand());
// Store token in user meta (more reliable than transients)
update_user_meta($user->ID, '_lrwc_staff_token', $token);
update_user_meta($user->ID, '_lrwc_staff_token_time', time());
return [
'success' => true,
'staff' => [
'id' => $staff->id,
'user_id' => $user->ID,
'name' => $user->display_name,
'role' => $staff->role
],
'token' => $token
];
}
private function verify_staff_token($request) {
global $wpdb;
// Try multiple ways to get the token
$token = $request->get_header('X-Staff-Token');
if (!$token) {
$token = $request->get_param('staff_token');
}
if (!$token && isset($_SERVER['HTTP_X_STAFF_TOKEN'])) {
$token = sanitize_text_field($_SERVER['HTTP_X_STAFF_TOKEN']);
}
if (!$token && isset($_POST['staff_token'])) {
$token = sanitize_text_field($_POST['staff_token']);
}
if (!$token && isset($_GET['staff_token'])) {
$token = sanitize_text_field($_GET['staff_token']);
}
if (!$token || strpos($token, 'lrwc_') !== 0) {
return false;
}
// Extract staff_id from token (format: lrwc_STAFFID_HASH)
$parts = explode('_', $token);
if (count($parts) < 3) {
return false;
}
$staff_id = intval($parts[1]);
if (!$staff_id) {
return false;
}
// Get staff record
$staff = $wpdb->get_row($wpdb->prepare(
"SELECT * FROM {$wpdb->prefix}lrwc_staff WHERE id = %d AND status = 'active'",
$staff_id
));
if (!$staff) {
return false;
}
// Verify token matches what's stored for this user
$stored_token = get_user_meta($staff->user_id, '_lrwc_staff_token', true);
$token_time = get_user_meta($staff->user_id, '_lrwc_staff_token_time', true);
if ($stored_token !== $token) {
return false;
}
// Check if token is expired (12 hours)
if (time() - intval($token_time) > 12 * HOUR_IN_SECONDS) {
delete_user_meta($staff->user_id, '_lrwc_staff_token');
delete_user_meta($staff->user_id, '_lrwc_staff_token_time');
return false;
}
return [
'staff_id' => $staff->id,
'user_id' => $staff->user_id
];
}
public function api_staff_scan($request) {
global $wpdb;
// Verify staff token
$session = $this->verify_staff_token($request);
if (!$session) {
return new WP_Error('unauthorized', 'Invalid or expired session. Please login again.', ['status' => 401]);
}
$qr_data = json_decode($request->get_param('qr_data'), true);
if (!$qr_data || !isset($qr_data['id']) || !isset($qr_data['hash'])) {
return new WP_Error('invalid_qr', 'Invalid QR code', ['status' => 400]);
}
$customer_id = intval($qr_data['id']);
$user = get_user_by('ID', $customer_id);
if (!$user) {
return new WP_Error('customer_not_found', 'Customer not found', ['status' => 404]);
}
// Verify hash
$expected_hash = substr(wp_hash($customer_id . $user->user_email . 'lrwc_salt'), 0, 16);
if ($qr_data['hash'] !== $expected_hash) {
return new WP_Error('invalid_qr', 'Invalid QR code', ['status' => 400]);
}
$points = $this->get_points_balance($customer_id);
// Get display name
$first_name = get_user_meta($customer_id, 'billing_first_name', true) ?: get_user_meta($customer_id, 'first_name', true);
$last_name = get_user_meta($customer_id, 'billing_last_name', true) ?: get_user_meta($customer_id, 'last_name', true);
$display_name = trim($first_name . ' ' . $last_name);
if (empty($display_name)) {
$display_name = $user->display_name;
}
// Get pending redemptions for this customer
$pending = $wpdb->get_results($wpdb->prepare(
"SELECT r.*, p.post_title as product_name
FROM {$wpdb->prefix}lrwc_redemptions r
LEFT JOIN {$wpdb->posts} p ON r.product_id = p.ID
WHERE r.customer_id = %d AND r.status = 'pending'
ORDER BY r.created_at DESC",
$customer_id
));
return [
'success' => true,
'customer' => [
'id' => $customer_id,
'name' => $display_name,
'username' => $user->user_login,
'email' => $user->user_email,
'points' => $points,
'tier' => $this->get_tier($points)
],
'pending_redemptions' => array_map(function($r) {
return [
'id' => $r->id,
'product_name' => $r->product_name,
'points_used' => $r->points_used,
'code' => $r->code,
'created_at' => $r->created_at
];
}, $pending)
];
}
public function api_staff_add_points($request) {
global $wpdb;
// Verify staff token
$session = $this->verify_staff_token($request);
if (!$session) {
return new WP_Error('unauthorized', 'Invalid or expired session. Please login again.', ['status' => 401]);
}
$customer_id = intval($request->get_param('customer_id'));
$amount = floatval($request->get_param('amount'));
$invoice_number = sanitize_text_field($request->get_param('invoice_number'));
if (empty($invoice_number)) {
return new WP_Error('missing_invoice', 'Invoice number is required', ['status' => 400]);
}
if ($amount <= 0) {
return new WP_Error('invalid_amount', 'Amount must be positive', ['status' => 400]);
}
// Ensure table exists
$table_name = $wpdb->prefix . 'lrwc_pending_points';
$table_exists = $wpdb->get_var("SHOW TABLES LIKE '$table_name'");
if (!$table_exists) {
$charset = $wpdb->get_charset_collate();
$wpdb->query("CREATE TABLE IF NOT EXISTS {$table_name} (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
customer_id BIGINT UNSIGNED NOT NULL,
staff_id BIGINT UNSIGNED NOT NULL,
invoice_number VARCHAR(100) NOT NULL,
bill_amount DECIMAL(15,2) NOT NULL,
points BIGINT NOT NULL,
status VARCHAR(20) DEFAULT 'pending',
admin_id BIGINT UNSIGNED,
admin_note TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
processed_at DATETIME,
INDEX idx_customer (customer_id),
INDEX idx_staff (staff_id),
INDEX idx_status (status),
INDEX idx_invoice (invoice_number)
) $charset");
}
// Check for duplicate invoice
$existing = $wpdb->get_var($wpdb->prepare(
"SELECT id FROM {$wpdb->prefix}lrwc_pending_points WHERE invoice_number = %s",
$invoice_number
));
if ($existing) {
return new WP_Error('duplicate_invoice', 'This invoice number has already been submitted', ['status' => 400]);
}
$points = $this->calculate_points($amount);
// Create pending request (admin must approve)
$result = $wpdb->insert($wpdb->prefix . 'lrwc_pending_points', [
'customer_id' => $customer_id,
'staff_id' => $session['staff_id'],
'invoice_number' => $invoice_number,
'bill_amount' => $amount,
'points' => $points,
'status' => 'pending',
'created_at' => current_time('mysql')
]);
if ($result === false) {
return new WP_Error('db_error', 'Failed to save request: ' . $wpdb->last_error, ['status' => 500]);
}
$request_id = $wpdb->insert_id;
// Get customer info for response
$customer = get_user_by('ID', $customer_id);
$current_balance = $this->get_points_balance($customer_id);
return [
'success' => true,
'request_id' => $request_id,
'points_requested' => $points,
'current_balance' => $current_balance,
'message' => 'Request submitted! ' . number_format($points) . ' points pending admin approval for invoice #' . $invoice_number
];
}
public function api_staff_process_redemption($request) {
global $wpdb;
// Verify staff token
$session = $this->verify_staff_token($request);
if (!$session) {
return new WP_Error('unauthorized', 'Invalid or expired session. Please login again.', ['status' => 401]);
}
$redemption_id = intval($request->get_param('redemption_id'));
$redemption = $wpdb->get_row($wpdb->prepare(
"SELECT * FROM {$wpdb->prefix}lrwc_redemptions WHERE id = %d AND status = 'pending'",
$redemption_id
));
if (!$redemption) {
return new WP_Error('not_found', 'Redemption not found or already processed', ['status' => 404]);
}
$wpdb->update(
$wpdb->prefix . 'lrwc_redemptions',
[
'status' => 'completed',
'staff_id' => $session['user_id'],
'redeemed_at' => current_time('mysql')
],
['id' => $redemption_id]
);
return [
'success' => true,
'message' => 'Redemption completed successfully'
];
}
public function api_staff_pending_redemptions($request) {
global $wpdb;
// Verify staff token
$session = $this->verify_staff_token($request);
if (!$session) {
return new WP_Error('unauthorized', 'Invalid or expired session. Please login again.', ['status' => 401]);
}
$pending = $wpdb->get_results("
SELECT r.*, u.display_name as customer_name, p.post_title as product_name
FROM {$wpdb->prefix}lrwc_redemptions r
LEFT JOIN {$wpdb->users} u ON r.customer_id = u.ID
LEFT JOIN {$wpdb->posts} p ON r.product_id = p.ID
WHERE r.status = 'pending'
ORDER BY r.created_at DESC
");
return [
'success' => true,
'redemptions' => $pending
];
}
/**
* Get rewards products for staff to select
*/
public function api_staff_rewards($request) {
$session = $this->verify_staff_token($request);
if (!$session) {
return new WP_Error('unauthorized', 'Invalid or expired session. Please login again.', ['status' => 401]);
}
$rewards = $this->get_available_rewards();
return [
'success' => true,
'rewards' => $rewards
];
}
/**
* Staff redeems a product for a customer - creates WooCommerce order
*/
public function api_staff_redeem_product($request) {
global $wpdb;
$session = $this->verify_staff_token($request);
if (!$session) {
return new WP_Error('unauthorized', 'Invalid or expired session. Please login again.', ['status' => 401]);
}
$customer_id = intval($request->get_param('customer_id'));
$product_id = intval($request->get_param('product_id'));
$points_to_use = intval($request->get_param('points_to_use')); // Optional: specific points to use
if (!$customer_id || !$product_id) {
return new WP_Error('invalid_params', 'Missing customer or product ID', ['status' => 400]);
}
// Get product
$product = wc_get_product($product_id);
if (!$product) {
return new WP_Error('invalid_product', 'Product not found', ['status' => 404]);
}
// Get product price and points cost
$product_price = floatval($product->get_price());
$points_cost = get_post_meta($product_id, '_lrwc_points_cost', true);
if (!$points_cost) {
$points_cost = intval($product_price);
}
$points_cost = intval($points_cost);
// Get customer points
$customer_points = $this->get_points_balance($customer_id);
// Determine points to use
if ($points_to_use > 0) {
$points_to_use = min($points_to_use, $customer_points, $points_cost);
} else {
// Use as many points as needed/available
$points_to_use = min($customer_points, $points_cost);
}
// Calculate point value for discount
$point_value = floatval(get_option('lrwc_point_value', 1));
$points_discount = $points_to_use * $point_value;
// If using points_cost mode, full redemption means $0
$order_total = max(0, $product_price - $points_discount);
$full_redemption = ($points_to_use >= $points_cost);
// Check stock
if (!$product->is_in_stock()) {
return new WP_Error('out_of_stock', 'Product is out of stock', ['status' => 400]);
}
// Get customer data
$customer = get_user_by('ID', $customer_id);
if (!$customer) {
return new WP_Error('invalid_customer', 'Customer not found', ['status' => 404]);
}
// Create WooCommerce order
$order = wc_create_order([
'customer_id' => $customer_id,
'status' => 'processing'
]);
if (is_wp_error($order)) {
return new WP_Error('order_failed', 'Failed to create order: ' . $order->get_error_message(), ['status' => 500]);
}
// Add product to order at full price
$order->add_product($product, 1);
// Add points discount as fee
if ($points_discount > 0) {
$item = new WC_Order_Item_Fee();
$item->set_name('Points Redemption (' . number_format($points_to_use) . ' points)');
$item->set_amount(-$points_discount);
$item->set_total(-$points_discount);
$order->add_item($item);
}
// Recalculate totals
$order->calculate_totals();
// Add order meta
$order->update_meta_data('_lrwc_rewards_redemption', 'yes');
$order->update_meta_data('_lrwc_points_used', $points_to_use);
$order->update_meta_data('_lrwc_points_discount', $points_discount);
$order->update_meta_data('_lrwc_staff_id', $session['staff_id']);
// Set billing details from customer
$order->set_billing_first_name(get_user_meta($customer_id, 'billing_first_name', true) ?: $customer->first_name ?: $customer->display_name);
$order->set_billing_last_name(get_user_meta($customer_id, 'billing_last_name', true) ?: $customer->last_name ?: '');
$order->set_billing_email($customer->user_email);
$order->set_billing_phone(get_user_meta($customer_id, 'billing_phone', true));
// Set payment method
$order->set_payment_method('lrwc_points');
$order->set_payment_method_title($full_redemption ? 'Points Redemption' : 'Points + Cash');
// Add order note
$note = $full_redemption
? sprintf('🎁 REWARDS REDEMPTION - Fully paid with %s points', number_format($points_to_use))
: sprintf('🎁 REWARDS REDEMPTION - %s points used (%s discount), remaining %s paid',
number_format($points_to_use),
wc_price($points_discount),
wc_price($order->get_total())
);
$order->add_order_note($note . ' - Staff processed');
// Set status based on payment
if ($full_redemption || $order->get_total() <= 0) {
$order->set_status('completed');
}
$order->save();
// Deduct points from customer
if ($points_to_use > 0) {
$new_balance = $this->add_points(
$customer_id,
-$points_to_use,
'redeem',
$session['staff_id'],
$order->get_id(),
$product_id,
'Redeemed: ' . $product->get_name() . ' (Order #' . $order->get_id() . ')'
);
} else {
$new_balance = $customer_points;
}
// Record redemption
$wpdb->insert($wpdb->prefix . 'lrwc_redemptions', [
'customer_id' => $customer_id,
'staff_id' => $session['staff_id'],
'product_id' => $product_id,
'points_used' => $points_to_use,
'status' => 'completed',
'code' => 'ORDER-' . $order->get_id(),
'redeemed_at' => current_time('mysql')
]);
// Reduce stock
wc_reduce_stock_levels($order->get_id());
return [
'success' => true,
'message' => sprintf('Order #%d created! %s points used.', $order->get_id(), number_format($points_to_use)),
'order_id' => $order->get_id(),
'order_total' => $order->get_total(),
'points_used' => $points_to_use,
'points_discount' => $points_discount,
'new_balance' => $new_balance,
'full_redemption' => $full_redemption
];
}
/**
* Lookup customer by phone, email, or username
*/
public function api_staff_lookup_customer($request) {
global $wpdb;
$session = $this->verify_staff_token($request);
if (!$session) {
return new WP_Error('unauthorized', 'Invalid or expired session. Please login again.', ['status' => 401]);
}
$search = sanitize_text_field($request->get_param('search'));
if (empty($search) || strlen($search) < 2) {
return new WP_Error('invalid_search', 'Please enter at least 2 characters', ['status' => 400]);
}
$search_like = '%' . $wpdb->esc_like($search) . '%';
// Search users by display_name, user_email, user_login, or billing_phone
$user_ids = $wpdb->get_col($wpdb->prepare("
SELECT DISTINCT u.ID
FROM {$wpdb->users} u
LEFT JOIN {$wpdb->usermeta} um_phone ON u.ID = um_phone.user_id AND um_phone.meta_key = 'billing_phone'
LEFT JOIN {$wpdb->usermeta} um_fname ON u.ID = um_fname.user_id AND um_fname.meta_key = 'first_name'
LEFT JOIN {$wpdb->usermeta} um_lname ON u.ID = um_lname.user_id AND um_lname.meta_key = 'last_name'
LEFT JOIN {$wpdb->usermeta} um_bfname ON u.ID = um_bfname.user_id AND um_bfname.meta_key = 'billing_first_name'
LEFT JOIN {$wpdb->usermeta} um_blname ON u.ID = um_blname.user_id AND um_blname.meta_key = 'billing_last_name'
WHERE u.display_name LIKE %s
OR u.user_email LIKE %s
OR u.user_login LIKE %s
OR um_phone.meta_value LIKE %s
OR um_fname.meta_value LIKE %s
OR um_lname.meta_value LIKE %s
OR um_bfname.meta_value LIKE %s
OR um_blname.meta_value LIKE %s
OR CONCAT(um_fname.meta_value, ' ', um_lname.meta_value) LIKE %s
OR CONCAT(um_bfname.meta_value, ' ', um_blname.meta_value) LIKE %s
LIMIT 20
", $search_like, $search_like, $search_like, $search_like, $search_like, $search_like, $search_like, $search_like, $search_like, $search_like));
if (empty($user_ids)) {
return new WP_Error('not_found', 'No customers found for "' . $search . '"', ['status' => 404]);
}
$results = [];
foreach ($user_ids as $user_id) {
$user = get_user_by('ID', $user_id);
if (!$user) continue;
$points = $this->get_points_balance($user_id);
$phone = get_user_meta($user_id, 'billing_phone', true);
$first_name = get_user_meta($user_id, 'billing_first_name', true) ?: get_user_meta($user_id, 'first_name', true);
$last_name = get_user_meta($user_id, 'billing_last_name', true) ?: get_user_meta($user_id, 'last_name', true);
$display_name = trim($first_name . ' ' . $last_name);
if (empty($display_name)) {
$display_name = $user->display_name;
}
$results[] = [
'id' => intval($user_id),
'name' => $display_name,
'username' => $user->user_login,
'email' => $user->user_email,
'phone' => $phone,
'points' => $points,
'tier' => $this->get_tier($points)
];
}
return [
'success' => true,
'customers' => $results
];
}
/**
* Staff creates order with multiple products and points payment
*/
public function api_staff_create_order($request) {
global $wpdb;
$session = $this->verify_staff_token($request);
if (!$session) {
return new WP_Error('unauthorized', 'Invalid or expired session. Please login again.', ['status' => 401]);
}
// Parse JSON body if sent as JSON
$body = $request->get_json_params();
if (!empty($body)) {
$customer_id = intval($body['customer_id'] ?? 0);
$products = $body['products'] ?? [];
$points_to_use = intval($body['points_to_use'] ?? 0);
} else {
$customer_id = intval($request->get_param('customer_id'));
$products = $request->get_param('products');
$points_to_use = intval($request->get_param('points_to_use'));
// Legacy support for product_ids
if (empty($products)) {
$product_ids = $request->get_param('product_ids');
if (!empty($product_ids)) {
$products = array_map(function($id) {
return ['id' => intval($id), 'qty' => 1];
}, $product_ids);
}
}
}
if (!$customer_id) {
return new WP_Error('invalid_params', 'Missing customer ID', ['status' => 400]);
}
if (empty($products) || !is_array($products)) {
return new WP_Error('invalid_params', 'No products selected', ['status' => 400]);
}
// Get customer
$customer = get_user_by('ID', $customer_id);
if (!$customer) {
return new WP_Error('invalid_customer', 'Customer not found', ['status' => 404]);
}
// Check customer has enough points
$customer_points = $this->get_points_balance($customer_id);
if ($points_to_use > $customer_points) {
$points_to_use = $customer_points;
}
// Calculate point value for discount
$point_value = floatval(get_option('lrwc_point_value', 1));
$max_points_discount = $points_to_use * $point_value;
// Create WooCommerce order
$order = wc_create_order([
'customer_id' => $customer_id,
'status' => 'processing'
]);
if (is_wp_error($order)) {
return new WP_Error('order_failed', 'Failed to create order: ' . $order->get_error_message(), ['status' => 500]);
}
// Add products to order with quantities
$subtotal = 0;
$product_names = [];
$first_product_id = null;
foreach ($products as $item) {
$product_id = intval($item['id'] ?? 0);
$qty = intval($item['qty'] ?? 1);
if (!$product_id || $qty < 1) continue;
$product = wc_get_product($product_id);
if (!$product) continue;
if ($first_product_id === null) {
$first_product_id = $product_id;
}
$order->add_product($product, $qty);
$subtotal += floatval($product->get_price()) * $qty;
$product_names[] = $product->get_name() . ($qty > 1 ? ' × ' . $qty : '');
}
// Calculate actual points discount (can't exceed subtotal)
$points_discount = min($max_points_discount, $subtotal);
$actual_points_used = ceil($points_discount / $point_value);
$to_pay = max(0, $subtotal - $points_discount);
// Add points discount as fee
if ($points_discount > 0) {
$item = new WC_Order_Item_Fee();
$item->set_name('Points Redemption (' . number_format($actual_points_used) . ' points)');
$item->set_amount(-$points_discount);
$item->set_total(-$points_discount);
$order->add_item($item);
}
// Set billing details
$order->set_billing_first_name(get_user_meta($customer_id, 'billing_first_name', true) ?: $customer->first_name ?: $customer->display_name);
$order->set_billing_last_name(get_user_meta($customer_id, 'billing_last_name', true) ?: $customer->last_name ?: '');
$order->set_billing_email($customer->user_email);
$order->set_billing_phone(get_user_meta($customer_id, 'billing_phone', true));
// Set payment method
if ($to_pay <= 0) {
$order->set_payment_method('lrwc_points');
$order->set_payment_method_title('Points Redemption');
$order->set_status('completed');
} else {
$order->set_payment_method('lrwc_points_cash');
$order->set_payment_method_title('Points + Cash');
}
// Add order meta
$order->update_meta_data('_lrwc_rewards_redemption', 'yes');
$order->update_meta_data('_lrwc_points_used', $actual_points_used);
$order->update_meta_data('_lrwc_points_discount', $points_discount);
$order->update_meta_data('_lrwc_staff_id', $session['staff_id']);
$order->update_meta_data('_lrwc_to_pay', $to_pay);
// Calculate totals
$order->calculate_totals();
// Add order note
$note = sprintf(
'🎁 REWARDS ORDER - Staff processed\nItems: %s\nPoints used: %s\nDiscount: %s\nTo collect: %s',
implode(', ', $product_names),
number_format($actual_points_used),
wc_price($points_discount),
wc_price($to_pay)
);
$order->add_order_note($note);
$order->save();
// Deduct points from customer
if ($actual_points_used > 0) {
$new_balance = $this->add_points(
$customer_id,
-$actual_points_used,
'redeem',
$session['staff_id'],
$order->get_id(),
null,
'Order #' . $order->get_id() . ' - ' . implode(', ', array_slice($product_names, 0, 2))
);
} else {
$new_balance = $customer_points;
}
// Record redemption
$wpdb->insert($wpdb->prefix . 'lrwc_redemptions', [
'customer_id' => $customer_id,
'staff_id' => $session['staff_id'],
'product_id' => $first_product_id ?: 0,
'points_used' => $actual_points_used,
'status' => 'completed',
'code' => 'ORDER-' . $order->get_id(),
'redeemed_at' => current_time('mysql')
]);
// Reduce stock
wc_reduce_stock_levels($order->get_id());
return [
'success' => true,
'message' => 'Order created successfully',
'order_id' => $order->get_id(),
'subtotal' => $subtotal,
'points_used' => $actual_points_used,
'points_discount' => $points_discount,
'to_pay' => $to_pay,
'new_balance' => $new_balance
];
}
/**
* Apply points to checkout
*/
public function api_apply_points_to_checkout($request) {
if (!is_user_logged_in()) {
return new WP_Error('not_logged_in', 'Please log in to use points', ['status' => 401]);
}
$customer_id = get_current_user_id();
$points_to_use = intval($request->get_param('points'));
if ($points_to_use <= 0) {
return new WP_Error('invalid_points', 'Invalid points amount', ['status' => 400]);
}
$available_points = $this->get_points_balance($customer_id);
if ($points_to_use > $available_points) {
return new WP_Error('insufficient_points', 'Not enough points', ['status' => 400]);
}
// Calculate discount value
$point_value = floatval(get_option('lrwc_point_value', 1));
$discount_amount = $points_to_use * $point_value;
// Store in session
WC()->session->set('lrwc_points_discount', [
'points' => $points_to_use,
'discount' => $discount_amount
]);
return [
'success' => true,
'points_applied' => $points_to_use,
'discount_amount' => $discount_amount,
'message' => sprintf('Applied %s points for %s discount', number_format($points_to_use), wc_price($discount_amount))
];
}
/**
* Remove points from checkout
*/
public function api_remove_points_from_checkout($request) {
WC()->session->set('lrwc_points_discount', null);
return [
'success' => true,
'message' => 'Points discount removed'
];
}
// ========================
// SHORTCODES
// ========================
public function shortcode_customer_app($atts) {
if (!is_user_logged_in()) {
return '