Skip to content

Instantly share code, notes, and snippets.

@goranefbl
Created June 29, 2025 06:15
Show Gist options
  • Save goranefbl/ff9093aa1c4b2f23e1752ef499199150 to your computer and use it in GitHub Desktop.
Save goranefbl/ff9093aa1c4b2f23e1752ef499199150 to your computer and use it in GitHub Desktop.
<?php
class WPGL_Points_API
{
private static $instance = null;
public static function init()
{
if (self::$instance === null) {
self::$instance = new self();
}
self::register_routes();
}
public static function register_routes()
{
add_action('rest_api_init', function () {
// Points Program Routes
register_rest_route(WPGENS_LOYALTY_REST_API_ROUTE, '/points/status', array(
'methods' => array('GET', 'POST'),
'callback' => array(__CLASS__, 'manage_points_status'),
'permission_callback' => array(__CLASS__, 'get_permission')
));
register_rest_route(WPGENS_LOYALTY_REST_API_ROUTE, '/points/settings', array(
'methods' => array('GET', 'POST'),
'callback' => array(__CLASS__, 'manage_points_settings'),
'permission_callback' => array(__CLASS__, 'get_permission')
));
register_rest_route(WPGENS_LOYALTY_REST_API_ROUTE, '/points/earning-actions', array(
'methods' => array('GET', 'POST'),
'callback' => array(__CLASS__, 'manage_points_earning_actions'),
'permission_callback' => array(__CLASS__, 'get_permission')
));
register_rest_route(WPGENS_LOYALTY_REST_API_ROUTE, '/points/earning-actions/(?P<id>\\d+)', array(
'methods' => array('POST', 'DELETE'),
'callback' => array(__CLASS__, 'manage_points_earning_action'),
'permission_callback' => array(__CLASS__, 'get_permission')
));
register_rest_route(WPGENS_LOYALTY_REST_API_ROUTE, '/points/redeem-actions', array(
'methods' => array('GET', 'POST'),
'callback' => array(__CLASS__, 'manage_points_redeem_actions'),
'permission_callback' => array(__CLASS__, 'get_permission')
));
register_rest_route(WPGENS_LOYALTY_REST_API_ROUTE, '/points/redeem-actions/(?P<id>\\d+)', array(
'methods' => array('POST', 'DELETE'),
'callback' => array(__CLASS__, 'manage_points_redeem_action'),
'permission_callback' => array(__CLASS__, 'get_permission')
));
register_rest_route(WPGENS_LOYALTY_REST_API_ROUTE, '/points/activity', array(
'methods' => 'GET',
'callback' => array(__CLASS__, 'get_points_activity'),
'permission_callback' => array(__CLASS__, 'get_permission')
));
register_rest_route(WPGENS_LOYALTY_REST_API_ROUTE, '/points/activity/download', array(
'methods' => 'GET',
'callback' => array(__CLASS__, 'download_points_activity'),
'permission_callback' => array(__CLASS__, 'get_permission')
));
// Member points management routes
register_rest_route(WPGENS_LOYALTY_REST_API_ROUTE, '/points/import', array(
'methods' => 'POST',
'callback' => array(__CLASS__, 'import_points'),
'permission_callback' => array(__CLASS__, 'get_permission')
));
register_rest_route(WPGENS_LOYALTY_REST_API_ROUTE, '/points/adjust', array(
'methods' => 'POST',
'callback' => array(__CLASS__, 'adjust_points'),
'permission_callback' => array(__CLASS__, 'get_permission')
));
});
// Register Store Checkout Block API endpoints
add_action('woocommerce_blocks_loaded', function() {
woocommerce_store_api_register_update_callback(
[
'namespace' => 'wpgens-points-and-rewards-program',
'callback' => function($data) {
if (!function_exists('WC')) {
return;
}
$points = $data['points'] ?? 0;
if ($points > 0) {
WPGL_Points_Checkout::handle_apply_points_discount($points);
} else {
WPGL_Points_Checkout::handle_remove_points_discount();
}
},
]
);
});
}
public static function get_permission()
{
return current_user_can('administrator') || current_user_can('manage_woocommerce');
}
public static function manage_points_status($request)
{
$prefix = 'wpgens_loyalty_';
$key = $prefix . 'points_program_active';
if ($request->get_method() === 'POST') {
$data = $request->get_params();
if (isset($data['active'])) {
update_option($key, $data['active']);
return new WP_REST_Response(['active' => $data['active']], 200);
}
return new WP_REST_Response(['message' => 'Invalid data'], 400);
}
return new WP_REST_Response(['active' => get_option($key, '0')], 200);
}
public static function manage_points_settings($request)
{
$settings_key = WPGL_Points_Settings::OPTION_POINTS_SETTINGS;
if ($request->get_method() === 'POST') {
$data = $request->get_params();
if (isset($data['settings'])) {
$existing_settings = get_option($settings_key, '{}');
$existing_settings = json_decode($existing_settings, true);
$new_settings = wp_parse_args($data['settings'], $existing_settings);
update_option($settings_key, wp_json_encode($new_settings));
}
return new WP_REST_Response($data, 200);
}
$settings = get_option($settings_key, '{}');
$settings = wp_parse_args(json_decode($settings, true), WPGL_Points_Settings::get_default_settings());
return new WP_REST_Response(['settings' => $settings], 200);
}
public static function manage_points_earning_actions($request)
{
$prefix = 'wpgens_loyalty_';
$key = $prefix . 'points_earning_actions';
$actions = get_option($key, array());
if ($request->get_method() === 'POST') {
$data = $request->get_params();
if (!empty($data)) {
// Only set referFriendIsPercent for REFER_FRIEND
if (isset($data['type']) && $data['type'] === 'REFER_FRIEND') {
$data['referFriendIsPercent'] = !empty($data['referFriendIsPercent']);
}
// Find the highest existing ID and add 1 to ensure uniqueness
$highest_id = 0;
foreach ($actions as $action) {
if (isset($action['id']) && $action['id'] > $highest_id) {
$highest_id = $action['id'];
}
}
$data['id'] = $highest_id + 1;
if (!isset($data['referFriendIsPercent']) && $data['type'] === 'REFER_FRIEND') {
$data['referFriendIsPercent'] = false;
}
$actions[] = $data;
update_option($key, $actions);
return new WP_REST_Response($data, 200);
}
return new WP_REST_Response(['message' => 'Invalid data'], 400);
}
return new WP_REST_Response($actions, 200);
}
public static function manage_points_earning_action($request)
{
$id = $request['id'];
$key = 'wpgens_loyalty_points_earning_actions';
$actions = get_option($key, array());
if ($request->get_method() === 'DELETE') {
foreach ($actions as $index => $action) {
if ($action['id'] == $id) {
array_splice($actions, $index, 1);
break;
}
}
update_option($key, $actions);
return new WP_REST_Response(null, 204);
}
$data = $request->get_params();
if (!empty($data)) {
foreach ($actions as $index => $action) {
if ($action['id'] == $id) {
// Only set referFriendIsPercent for REFER_FRIEND
if (isset($data['type']) && $data['type'] === 'REFER_FRIEND') {
$data['referFriendIsPercent'] = !empty($data['referFriendIsPercent']);
}
$actions[$index] = array_merge($action, $data);
break;
}
}
update_option($key, $actions);
return new WP_REST_Response($data, 200);
}
return new WP_REST_Response(['message' => 'Invalid data'], 400);
}
public static function manage_points_redeem_actions($request)
{
$prefix = 'wpgens_loyalty_';
$key = $prefix . 'points_redeem_actions';
$actions = get_option($key, array());
if ($request->get_method() === 'POST') {
$data = $request->get_params();
if (!empty($data)) {
$data['id'] = (string)(count($actions) + 1);
$actions[] = $data;
update_option($key, $actions);
return new WP_REST_Response($data, 200);
}
return new WP_REST_Response(['message' => 'Invalid data'], 400);
}
return new WP_REST_Response($actions, 200);
}
public static function manage_points_redeem_action($request)
{
$id = (string)$request['id'];
$key = 'wpgens_loyalty_points_redeem_actions';
$actions = get_option($key, array());
if ($request->get_method() === 'DELETE') {
foreach ($actions as $index => $action) {
if ((string)$action['id'] === $id) {
array_splice($actions, $index, 1);
break;
}
}
update_option($key, $actions);
return new WP_REST_Response(null, 204);
}
$data = $request->get_params();
if (!empty($data)) {
foreach ($actions as $index => $action) {
if ((string)$action['id'] === $id) {
$actions[$index] = array_merge($action, $data);
break;
}
}
update_option($key, $actions);
return new WP_REST_Response($data, 200);
}
return new WP_REST_Response(['message' => 'Invalid data'], 400);
}
public static function get_points_activity($request)
{
// Sanitize and validate inputs
$per_page = min(50, absint($request->get_param('limit')) ?: 10);
$page = max(1, absint($request->get_param('page')) ?: 1);
$filter = $request->get_param('filter');
$sort_by = $request->get_param('sort_by');
// Get activity data
$activity = WPGL_Database::get_points_activity([
'page' => $page,
'limit' => $per_page,
'filter' => $filter,
'sort_by' => $sort_by
]);
// Get stats with filters
$stats = WPGL_Database::get_points_stats($filter);
// Format response
$response = [
'data' => $activity['items'],
'meta' => [
'currentPage' => (int)$activity['current_page'],
'itemsPerPage' => (int)$activity['per_page'],
'totalItems' => (int)$activity['total_items'],
'totalPages' => (int)$activity['total_pages']
],
'stats' => [
'totalEarned' => $stats->total_earned,
'totalSpent' => $stats->total_spent,
'redemptionRate' => $stats->redemption_rate
]
];
return new WP_REST_Response($response, 200);
}
public static function download_points_activity($request)
{
$filter = $request->get_param('filter');
$items = WPGL_Database::get_points_activity_for_export(['filter' => $filter]);
$filename = 'points-activity-' . gmdate('Y-m-d') . '.csv';
// Create CSV content
$csv_content = '';
// Add headers
$headers = ['Date', 'User Email', 'Points', 'Type', 'Source', 'Reference ID', 'Description'];
$csv_content .= implode(',', array_map(function ($value) {
return strpos($value, ',') !== false ? '"' . str_replace('"', '""', esc_html($value)) . '"' : esc_html($value);
}, $headers)) . "\n";
// Add data rows
foreach ($items as $item) {
$row = [
esc_html($item->timestamp),
esc_html($item->user_email),
absint($item->points),
esc_html($item->type),
esc_html($item->source),
esc_html($item->reference_id),
esc_html($item->description)
];
$csv_content .= implode(',', array_map(function ($value) {
return strpos($value, ',') !== false ? '"' . str_replace('"', '""', $value) . '"' : $value;
}, $row)) . "\n";
}
// Send headers
nocache_headers();
header('Content-Type: text/csv; charset=utf-8');
header('Content-Disposition: attachment; filename="' . sanitize_file_name($filename) . '"');
// Output the CSV content
echo esc_html($csv_content);
exit;
}
public static function import_points($request)
{
// Verify nonce
$nonce = $request->get_param('nonce');
if (!$nonce || !wp_verify_nonce($nonce, 'wp_rest')) {
return new WP_Error('invalid_nonce', 'Invalid security token', array('status' => 403));
}
// Validate file upload
if (
!isset($_FILES['file']) ||
!isset($_FILES['file']['tmp_name']) ||
!isset($_FILES['file']['name']) ||
!isset($_FILES['file']['type']) ||
!isset($_FILES['file']['error']) ||
!isset($_FILES['file']['size'])
) {
return new WP_Error('missing_file', 'Invalid file upload', array('status' => 400));
}
// Sanitize and validate the uploaded file
$file = array(
'name' => sanitize_file_name($_FILES['file']['name']),
'type' => sanitize_mime_type($_FILES['file']['type']),
'tmp_name' => sanitize_text_field($_FILES['file']['tmp_name']),
'error' => absint($_FILES['file']['error']),
'size' => absint($_FILES['file']['size'])
);
// Accept any file type if it has a .csv extension
$file_extension = pathinfo($file['name'], PATHINFO_EXTENSION);
if (strtolower($file_extension) !== 'csv') {
return new WP_Error('invalid_type', 'Invalid file type. Please upload a CSV file.', array('status' => 400));
}
// Validate file size (max 5MB)
if ($file['size'] > 5 * 1024 * 1024) {
return new WP_Error('file_too_large', 'File is too large. Maximum size is 5MB.', array('status' => 400));
}
$mode = sanitize_text_field($request->get_param('mode'));
if ($file['error'] !== UPLOAD_ERR_OK) {
return new WP_Error('upload_error', 'File upload failed', array('status' => 400));
}
// Read file contents directly
$file_contents = file_get_contents($file['tmp_name']);
if (false === $file_contents) {
return new WP_Error('file_read_error', 'Could not read file', array('status' => 400));
}
// Check if file is empty
if (empty($file_contents)) {
return new WP_Error('empty_file', 'The uploaded file is empty', array('status' => 400));
}
$success_count = 0;
$error_count = 0;
$errors = array();
// Parse CSV contents - handle different line endings
$file_contents = str_replace(["\r\n", "\r"], "\n", $file_contents);
$rows = explode("\n", $file_contents);
// Remove header row
$header = str_getcsv(array_shift($rows), ',', '"', '\\');
// Validate header row - accept 2 or 3 columns
$header_count = count($header);
if ($header_count < 2 || $header_count > 3 ||
strtolower($header[0]) !== 'email' ||
strtolower($header[1]) !== 'points' ||
($header_count === 3 && strtolower($header[2]) !== 'description')) {
return new WP_Error('invalid_format', 'Invalid CSV format. The first row should contain "email,points" or "email,points,description"', array('status' => 400));
}
foreach ($rows as $index => $row) {
// Skip empty rows
if (empty($row)) {
continue;
}
// Parse the CSV row
$data = str_getcsv($row, ',', '"', '\\');
if (!is_array($data) || count($data) < 2 || count($data) > 3) {
$error_count++;
$errors[] = "Row " . ($index + 2) . ": Invalid format. Expected 2 or 3 columns (email,points,description).";
continue;
}
$email = sanitize_email($data[0]);
$points = absint($data[1]);
$description = isset($data[2]) && !empty(trim($data[2])) ? sanitize_text_field(trim($data[2])) : 'Points imported';
if (!is_email($email)) {
$errors[] = "Row " . ($index + 2) . ": Invalid email format: " . esc_html($email);
$error_count++;
continue;
}
$user = get_user_by('email', $email);
if (!$user) {
$errors[] = "Row " . ($index + 2) . ": User not found: " . esc_html($email);
$error_count++;
continue;
}
try {
if ($mode === 'replace') {
// First set points to 0, then add the new amount
$current_points = WPGL_Database::get_user_points($user->ID);
if ($current_points > 0) {
// Directly update points to avoid issues with action hooks
$result = WPGL_Database::update_user_points(
$user->ID,
-$current_points,
WPGL_Points_Activity_Type::DEDUCT,
WPGL_Points_Source_Type::IMPORT,
null,
'Points reset during import'
);
if (is_wp_error($result)) {
throw new Exception($result->get_error_message());
}
}
// Add the new points
$result = WPGL_Database::update_user_points(
$user->ID,
$points,
WPGL_Points_Activity_Type::EARN,
WPGL_Points_Source_Type::IMPORT,
null,
$description
);
if (is_wp_error($result)) {
throw new Exception($result->get_error_message());
}
} else {
// Directly update points to avoid issues with action hooks
$result = WPGL_Database::update_user_points(
$user->ID,
$points,
WPGL_Points_Activity_Type::EARN,
WPGL_Points_Source_Type::IMPORT,
null,
$description
);
if (is_wp_error($result)) {
throw new Exception($result->get_error_message());
}
}
// Verify points were actually updated
$new_balance = WPGL_Database::get_user_points($user->ID);
$success_count++;
} catch (Exception $e) {
$errors[] = "Row " . ($index + 2) . ": Error updating points for " . esc_html($email) . ": " . esc_html($e->getMessage());
$error_count++;
}
}
// If no rows were processed successfully, return an error
if ($success_count === 0) {
if (count($errors) > 0) {
return new WP_Error('import_failed', 'Import failed: ' . implode('; ', $errors), array('status' => 400));
} else {
return new WP_Error('import_failed', 'Import failed: No valid rows found in the CSV file', array('status' => 400));
}
}
return new WP_REST_Response(array(
'success' => true,
'success_count' => $success_count,
'error_count' => $error_count,
'errors' => $errors
));
}
public static function adjust_points($request)
{
// Verify nonce
$nonce = $request->get_param('nonce');
if (!$nonce || !wp_verify_nonce($nonce, 'wp_rest')) {
return new WP_Error('invalid_nonce', 'Invalid security token', array('status' => 403));
}
$data = $request->get_params();
if (!isset($data['user_id']) || !isset($data['points'])) {
return new WP_REST_Response(['message' => 'Missing required fields'], 400);
}
$user_id = intval($data['user_id']);
$points = intval($data['points']);
$description = isset($data['description']) ? sanitize_text_field($data['description']) : null;
// Determine type based on points value
$type = $points >= 0 ? WPGL_Points_Activity_Type::EARN : WPGL_Points_Activity_Type::DEDUCT;
try {
// Use the action hook to trigger all registered callbacks
do_action('wpgens_loyalty_update_points', $user_id, $points, $type, WPGL_Points_Source_Type::MANUAL, null, $description);
// Get the new balance after the action has been processed
$new_balance = WPGL_Database::get_user_points($user_id);
// Check if points were actually updated
if ($new_balance === 0 && $points > 0) {
// This might indicate an error in the update process
return new WP_REST_Response([
'success' => false,
'message' => 'Points update may have failed. Please check the logs.'
], 400);
}
return new WP_REST_Response([
'success' => true,
'new_balance' => $new_balance
]);
} catch (Exception $e) {
return new WP_REST_Response([
'success' => false,
'message' => $e->getMessage()
], 500);
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment