You've already forked wc-licensed-product
Implement versions 0.0.4-0.0.7 features
v0.0.4: - Add WooCommerce settings tab for default license settings - Per-product settings override global defaults v0.0.5: - Add bulk license operations (activate, deactivate, revoke, extend, delete) - Add license renewal/extension and lifetime functionality - Add quick action buttons per license row v0.0.6: - Add license dashboard with statistics and analytics - Add license transfer functionality (admin) - Add CSV export for licenses - Add OpenAPI 3.1 specification - Remove /deactivate API endpoint v0.0.7: - Move license dashboard to WooCommerce Reports section - Add license search and filtering in admin - Add customer-facing license transfer with AJAX modal - Add email notifications for license expiration warnings - Add bulk import licenses from CSV - Update README with comprehensive documentation Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -176,21 +176,63 @@ class LicenseManager
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all licenses (for admin)
|
||||
* Get all licenses (for admin) with optional filtering
|
||||
*
|
||||
* @param int $page Page number
|
||||
* @param int $perPage Items per page
|
||||
* @param array $filters Optional filters: search, status, product_id, customer_id
|
||||
* @return array Array of License objects
|
||||
*/
|
||||
public function getAllLicenses(int $page = 1, int $perPage = 20): array
|
||||
public function getAllLicenses(int $page = 1, int $perPage = 20, array $filters = []): array
|
||||
{
|
||||
global $wpdb;
|
||||
|
||||
$tableName = Installer::getLicensesTable();
|
||||
$offset = ($page - 1) * $perPage;
|
||||
|
||||
$where = [];
|
||||
$params = [];
|
||||
|
||||
// Search filter (searches license key, domain, customer email)
|
||||
if (!empty($filters['search'])) {
|
||||
$search = '%' . $wpdb->esc_like($filters['search']) . '%';
|
||||
$where[] = "(license_key LIKE %s OR domain LIKE %s)";
|
||||
$params[] = $search;
|
||||
$params[] = $search;
|
||||
}
|
||||
|
||||
// Status filter
|
||||
if (!empty($filters['status']) && in_array($filters['status'], [
|
||||
License::STATUS_ACTIVE,
|
||||
License::STATUS_INACTIVE,
|
||||
License::STATUS_EXPIRED,
|
||||
License::STATUS_REVOKED,
|
||||
], true)) {
|
||||
$where[] = "status = %s";
|
||||
$params[] = $filters['status'];
|
||||
}
|
||||
|
||||
// Product filter
|
||||
if (!empty($filters['product_id'])) {
|
||||
$where[] = "product_id = %d";
|
||||
$params[] = absint($filters['product_id']);
|
||||
}
|
||||
|
||||
// Customer filter
|
||||
if (!empty($filters['customer_id'])) {
|
||||
$where[] = "customer_id = %d";
|
||||
$params[] = absint($filters['customer_id']);
|
||||
}
|
||||
|
||||
$whereClause = !empty($where) ? 'WHERE ' . implode(' AND ', $where) : '';
|
||||
|
||||
$params[] = $perPage;
|
||||
$params[] = $offset;
|
||||
|
||||
$sql = "SELECT * FROM {$tableName} {$whereClause} ORDER BY created_at DESC LIMIT %d OFFSET %d";
|
||||
|
||||
$rows = $wpdb->get_results(
|
||||
$wpdb->prepare(
|
||||
"SELECT * FROM {$tableName} ORDER BY created_at DESC LIMIT %d OFFSET %d",
|
||||
$perPage,
|
||||
$offset
|
||||
),
|
||||
$wpdb->prepare($sql, $params),
|
||||
ARRAY_A
|
||||
);
|
||||
|
||||
@@ -198,14 +240,83 @@ class LicenseManager
|
||||
}
|
||||
|
||||
/**
|
||||
* Get total license count
|
||||
* Get total license count with optional filtering
|
||||
*
|
||||
* @param array $filters Optional filters: search, status, product_id, customer_id
|
||||
* @return int Total count
|
||||
*/
|
||||
public function getLicenseCount(): int
|
||||
public function getLicenseCount(array $filters = []): int
|
||||
{
|
||||
global $wpdb;
|
||||
|
||||
$tableName = Installer::getLicensesTable();
|
||||
return (int) $wpdb->get_var("SELECT COUNT(*) FROM {$tableName}");
|
||||
|
||||
$where = [];
|
||||
$params = [];
|
||||
|
||||
// Search filter
|
||||
if (!empty($filters['search'])) {
|
||||
$search = '%' . $wpdb->esc_like($filters['search']) . '%';
|
||||
$where[] = "(license_key LIKE %s OR domain LIKE %s)";
|
||||
$params[] = $search;
|
||||
$params[] = $search;
|
||||
}
|
||||
|
||||
// Status filter
|
||||
if (!empty($filters['status']) && in_array($filters['status'], [
|
||||
License::STATUS_ACTIVE,
|
||||
License::STATUS_INACTIVE,
|
||||
License::STATUS_EXPIRED,
|
||||
License::STATUS_REVOKED,
|
||||
], true)) {
|
||||
$where[] = "status = %s";
|
||||
$params[] = $filters['status'];
|
||||
}
|
||||
|
||||
// Product filter
|
||||
if (!empty($filters['product_id'])) {
|
||||
$where[] = "product_id = %d";
|
||||
$params[] = absint($filters['product_id']);
|
||||
}
|
||||
|
||||
// Customer filter
|
||||
if (!empty($filters['customer_id'])) {
|
||||
$where[] = "customer_id = %d";
|
||||
$params[] = absint($filters['customer_id']);
|
||||
}
|
||||
|
||||
$whereClause = !empty($where) ? 'WHERE ' . implode(' AND ', $where) : '';
|
||||
|
||||
if (empty($params)) {
|
||||
return (int) $wpdb->get_var("SELECT COUNT(*) FROM {$tableName}");
|
||||
}
|
||||
|
||||
return (int) $wpdb->get_var(
|
||||
$wpdb->prepare("SELECT COUNT(*) FROM {$tableName} {$whereClause}", $params)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all licensed products for filter dropdown
|
||||
*
|
||||
* @return array Array of [id => name] pairs
|
||||
*/
|
||||
public function getLicensedProducts(): array
|
||||
{
|
||||
global $wpdb;
|
||||
|
||||
$tableName = Installer::getLicensesTable();
|
||||
$productIds = $wpdb->get_col("SELECT DISTINCT product_id FROM {$tableName}");
|
||||
|
||||
$products = [];
|
||||
foreach ($productIds as $productId) {
|
||||
$product = wc_get_product((int) $productId);
|
||||
if ($product) {
|
||||
$products[(int) $productId] = $product->get_name();
|
||||
}
|
||||
}
|
||||
|
||||
return $products;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -360,4 +471,430 @@ class LicenseManager
|
||||
|
||||
return $versionId ? (int) $versionId : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extend license expiration
|
||||
*
|
||||
* @param int $licenseId License ID
|
||||
* @param int $days Number of days to extend
|
||||
* @return bool Success
|
||||
*/
|
||||
public function extendLicense(int $licenseId, int $days): bool
|
||||
{
|
||||
global $wpdb;
|
||||
|
||||
$license = $this->getLicenseById($licenseId);
|
||||
if (!$license) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Calculate new expiration date
|
||||
$currentExpiry = $license->getExpiresAt();
|
||||
if ($currentExpiry === null) {
|
||||
// License is lifetime, set expiration from now
|
||||
$newExpiry = (new \DateTimeImmutable())->modify("+{$days} days");
|
||||
} elseif ($currentExpiry < new \DateTimeImmutable()) {
|
||||
// License is expired, extend from now
|
||||
$newExpiry = (new \DateTimeImmutable())->modify("+{$days} days");
|
||||
} else {
|
||||
// License still valid, extend from current expiry
|
||||
$newExpiry = \DateTimeImmutable::createFromInterface($currentExpiry)->modify("+{$days} days");
|
||||
}
|
||||
|
||||
$tableName = Installer::getLicensesTable();
|
||||
$result = $wpdb->update(
|
||||
$tableName,
|
||||
['expires_at' => $newExpiry->format('Y-m-d H:i:s')],
|
||||
['id' => $licenseId],
|
||||
['%s'],
|
||||
['%d']
|
||||
);
|
||||
|
||||
// If license was expired, reactivate it
|
||||
if ($result !== false && $license->getStatus() === License::STATUS_EXPIRED) {
|
||||
$this->updateLicenseStatus($licenseId, License::STATUS_ACTIVE);
|
||||
}
|
||||
|
||||
return $result !== false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set license to lifetime (no expiration)
|
||||
*
|
||||
* @param int $licenseId License ID
|
||||
* @return bool Success
|
||||
*/
|
||||
public function setLicenseLifetime(int $licenseId): bool
|
||||
{
|
||||
global $wpdb;
|
||||
|
||||
$license = $this->getLicenseById($licenseId);
|
||||
|
||||
$tableName = Installer::getLicensesTable();
|
||||
// Use raw query to set NULL
|
||||
$result = $wpdb->query(
|
||||
$wpdb->prepare(
|
||||
"UPDATE {$tableName} SET expires_at = NULL WHERE id = %d",
|
||||
$licenseId
|
||||
)
|
||||
);
|
||||
|
||||
// If license was expired, reactivate it
|
||||
if ($result !== false && $license && $license->getStatus() === License::STATUS_EXPIRED) {
|
||||
$this->updateLicenseStatus($licenseId, License::STATUS_ACTIVE);
|
||||
}
|
||||
|
||||
return $result !== false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Bulk update license status
|
||||
*
|
||||
* @param array $licenseIds Array of license IDs
|
||||
* @param string $status New status
|
||||
* @return int Number of licenses updated
|
||||
*/
|
||||
public function bulkUpdateStatus(array $licenseIds, string $status): int
|
||||
{
|
||||
global $wpdb;
|
||||
|
||||
if (empty($licenseIds)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$tableName = Installer::getLicensesTable();
|
||||
$ids = array_map('absint', $licenseIds);
|
||||
$placeholders = implode(',', array_fill(0, count($ids), '%d'));
|
||||
|
||||
$result = $wpdb->query(
|
||||
$wpdb->prepare(
|
||||
"UPDATE {$tableName} SET status = %s WHERE id IN ({$placeholders})",
|
||||
array_merge([$status], $ids)
|
||||
)
|
||||
);
|
||||
|
||||
return $result !== false ? (int) $result : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Bulk delete licenses
|
||||
*
|
||||
* @param array $licenseIds Array of license IDs
|
||||
* @return int Number of licenses deleted
|
||||
*/
|
||||
public function bulkDelete(array $licenseIds): int
|
||||
{
|
||||
global $wpdb;
|
||||
|
||||
if (empty($licenseIds)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$tableName = Installer::getLicensesTable();
|
||||
$ids = array_map('absint', $licenseIds);
|
||||
$placeholders = implode(',', array_fill(0, count($ids), '%d'));
|
||||
|
||||
$result = $wpdb->query(
|
||||
$wpdb->prepare(
|
||||
"DELETE FROM {$tableName} WHERE id IN ({$placeholders})",
|
||||
$ids
|
||||
)
|
||||
);
|
||||
|
||||
return $result !== false ? (int) $result : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Bulk extend licenses
|
||||
*
|
||||
* @param array $licenseIds Array of license IDs
|
||||
* @param int $days Number of days to extend
|
||||
* @return int Number of licenses extended
|
||||
*/
|
||||
public function bulkExtend(array $licenseIds, int $days): int
|
||||
{
|
||||
$count = 0;
|
||||
foreach ($licenseIds as $licenseId) {
|
||||
if ($this->extendLicense((int) $licenseId, $days)) {
|
||||
$count++;
|
||||
}
|
||||
}
|
||||
return $count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transfer license to a new domain
|
||||
*
|
||||
* @param int $licenseId License ID
|
||||
* @param string $newDomain New domain to transfer to
|
||||
* @return bool Success
|
||||
*/
|
||||
public function transferLicense(int $licenseId, string $newDomain): bool
|
||||
{
|
||||
$license = $this->getLicenseById($licenseId);
|
||||
if (!$license) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Cannot transfer revoked licenses
|
||||
if ($license->getStatus() === License::STATUS_REVOKED) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->updateLicenseDomain($licenseId, $newDomain);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get license statistics
|
||||
*
|
||||
* @return array Statistics data
|
||||
*/
|
||||
public function getStatistics(): array
|
||||
{
|
||||
global $wpdb;
|
||||
|
||||
$tableName = Installer::getLicensesTable();
|
||||
|
||||
// Get counts by status
|
||||
$statusCounts = $wpdb->get_results(
|
||||
"SELECT status, COUNT(*) as count FROM {$tableName} GROUP BY status",
|
||||
ARRAY_A
|
||||
);
|
||||
|
||||
$byStatus = [
|
||||
License::STATUS_ACTIVE => 0,
|
||||
License::STATUS_INACTIVE => 0,
|
||||
License::STATUS_EXPIRED => 0,
|
||||
License::STATUS_REVOKED => 0,
|
||||
];
|
||||
foreach ($statusCounts ?: [] as $row) {
|
||||
$byStatus[$row['status']] = (int) $row['count'];
|
||||
}
|
||||
|
||||
// Get total count
|
||||
$total = array_sum($byStatus);
|
||||
|
||||
// Get lifetime vs expiring licenses
|
||||
$lifetimeCount = (int) $wpdb->get_var(
|
||||
"SELECT COUNT(*) FROM {$tableName} WHERE expires_at IS NULL"
|
||||
);
|
||||
$expiringCount = $total - $lifetimeCount;
|
||||
|
||||
// Get licenses expiring soon (next 30 days)
|
||||
$expiringSoon = (int) $wpdb->get_var(
|
||||
$wpdb->prepare(
|
||||
"SELECT COUNT(*) FROM {$tableName} WHERE expires_at IS NOT NULL AND expires_at <= %s AND expires_at > NOW() AND status = %s",
|
||||
(new \DateTimeImmutable())->modify('+30 days')->format('Y-m-d H:i:s'),
|
||||
License::STATUS_ACTIVE
|
||||
)
|
||||
);
|
||||
|
||||
// Get licenses by product
|
||||
$byProduct = $wpdb->get_results(
|
||||
"SELECT product_id, COUNT(*) as count FROM {$tableName} GROUP BY product_id ORDER BY count DESC LIMIT 10",
|
||||
ARRAY_A
|
||||
);
|
||||
|
||||
$productStats = [];
|
||||
foreach ($byProduct ?: [] as $row) {
|
||||
$product = wc_get_product((int) $row['product_id']);
|
||||
$productStats[] = [
|
||||
'product_id' => (int) $row['product_id'],
|
||||
'product_name' => $product ? $product->get_name() : __('Unknown Product', 'wc-licensed-product'),
|
||||
'count' => (int) $row['count'],
|
||||
];
|
||||
}
|
||||
|
||||
// Get licenses created per month (last 12 months)
|
||||
$monthlyData = $wpdb->get_results(
|
||||
"SELECT DATE_FORMAT(created_at, '%Y-%m') as month, COUNT(*) as count
|
||||
FROM {$tableName}
|
||||
WHERE created_at >= DATE_SUB(NOW(), INTERVAL 12 MONTH)
|
||||
GROUP BY DATE_FORMAT(created_at, '%Y-%m')
|
||||
ORDER BY month ASC",
|
||||
ARRAY_A
|
||||
);
|
||||
|
||||
$monthlyStats = [];
|
||||
foreach ($monthlyData ?: [] as $row) {
|
||||
$monthlyStats[$row['month']] = (int) $row['count'];
|
||||
}
|
||||
|
||||
// Get top domains
|
||||
$topDomains = $wpdb->get_results(
|
||||
"SELECT domain, COUNT(*) as count FROM {$tableName} GROUP BY domain ORDER BY count DESC LIMIT 10",
|
||||
ARRAY_A
|
||||
);
|
||||
|
||||
return [
|
||||
'total' => $total,
|
||||
'by_status' => $byStatus,
|
||||
'lifetime' => $lifetimeCount,
|
||||
'expiring' => $expiringCount,
|
||||
'expiring_soon' => $expiringSoon,
|
||||
'by_product' => $productStats,
|
||||
'monthly' => $monthlyStats,
|
||||
'top_domains' => $topDomains ?: [],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get licenses expiring within specified days
|
||||
*
|
||||
* @param int $days Number of days to look ahead
|
||||
* @param bool $excludeNotified Whether to exclude already notified licenses
|
||||
* @return array Array of License objects with customer data
|
||||
*/
|
||||
public function getLicensesExpiringSoon(int $days = 7, bool $excludeNotified = true): array
|
||||
{
|
||||
global $wpdb;
|
||||
|
||||
$tableName = Installer::getLicensesTable();
|
||||
$now = new \DateTimeImmutable();
|
||||
$future = $now->modify("+{$days} days");
|
||||
|
||||
$sql = "SELECT * FROM {$tableName}
|
||||
WHERE expires_at IS NOT NULL
|
||||
AND expires_at > %s
|
||||
AND expires_at <= %s
|
||||
AND status = %s";
|
||||
|
||||
$params = [
|
||||
$now->format('Y-m-d H:i:s'),
|
||||
$future->format('Y-m-d H:i:s'),
|
||||
License::STATUS_ACTIVE,
|
||||
];
|
||||
|
||||
$rows = $wpdb->get_results(
|
||||
$wpdb->prepare($sql, $params),
|
||||
ARRAY_A
|
||||
);
|
||||
|
||||
return array_map(fn(array $row) => License::fromArray($row), $rows ?: []);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark license as notified for expiration warning
|
||||
*
|
||||
* @param int $licenseId License ID
|
||||
* @param string $notificationType Type of notification (e.g., 'expiring_7_days', 'expiring_1_day')
|
||||
* @return bool Success
|
||||
*/
|
||||
public function markExpirationNotified(int $licenseId, string $notificationType): bool
|
||||
{
|
||||
$metaKey = '_wclp_expiration_notified_' . sanitize_key($notificationType);
|
||||
update_user_meta($this->getLicenseById($licenseId)?->getCustomerId() ?? 0, $metaKey . '_' . $licenseId, current_time('mysql'));
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if license was already notified for expiration
|
||||
*
|
||||
* @param int $licenseId License ID
|
||||
* @param string $notificationType Type of notification
|
||||
* @return bool Whether already notified
|
||||
*/
|
||||
public function wasExpirationNotified(int $licenseId, string $notificationType): bool
|
||||
{
|
||||
$license = $this->getLicenseById($licenseId);
|
||||
if (!$license) {
|
||||
return true; // Consider notified if license doesn't exist
|
||||
}
|
||||
|
||||
$metaKey = '_wclp_expiration_notified_' . sanitize_key($notificationType) . '_' . $licenseId;
|
||||
return (bool) get_user_meta($license->getCustomerId(), $metaKey, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Import a license from CSV data
|
||||
*
|
||||
* @param string $licenseKey License key
|
||||
* @param int $productId Product ID
|
||||
* @param int $customerId Customer ID
|
||||
* @param string $domain Domain name
|
||||
* @param int $orderId Order ID (optional)
|
||||
* @param string $status License status
|
||||
* @param int $maxActivations Maximum activations
|
||||
* @param int $activationsCount Current activation count
|
||||
* @param \DateTimeImmutable|null $expiresAt Expiration date or null for lifetime
|
||||
* @return bool Success
|
||||
*/
|
||||
public function importLicense(
|
||||
string $licenseKey,
|
||||
int $productId,
|
||||
int $customerId,
|
||||
string $domain,
|
||||
int $orderId = 0,
|
||||
string $status = License::STATUS_ACTIVE,
|
||||
int $maxActivations = 1,
|
||||
int $activationsCount = 1,
|
||||
?\DateTimeImmutable $expiresAt = null
|
||||
): bool {
|
||||
global $wpdb;
|
||||
|
||||
$tableName = Installer::getLicensesTable();
|
||||
|
||||
$result = $wpdb->insert(
|
||||
$tableName,
|
||||
[
|
||||
'license_key' => $licenseKey,
|
||||
'order_id' => $orderId,
|
||||
'product_id' => $productId,
|
||||
'customer_id' => $customerId,
|
||||
'domain' => $this->normalizeDomain($domain),
|
||||
'version_id' => null,
|
||||
'status' => $status,
|
||||
'activations_count' => $activationsCount,
|
||||
'max_activations' => $maxActivations,
|
||||
'expires_at' => $expiresAt ? $expiresAt->format('Y-m-d H:i:s') : null,
|
||||
],
|
||||
['%s', '%d', '%d', '%d', '%s', '%d', '%s', '%d', '%d', '%s']
|
||||
);
|
||||
|
||||
return $result !== false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Export all licenses to array format suitable for CSV
|
||||
*
|
||||
* @return array Array of license data for CSV export
|
||||
*/
|
||||
public function exportLicensesForCsv(): array
|
||||
{
|
||||
global $wpdb;
|
||||
|
||||
$tableName = Installer::getLicensesTable();
|
||||
$rows = $wpdb->get_results(
|
||||
"SELECT * FROM {$tableName} ORDER BY created_at DESC",
|
||||
ARRAY_A
|
||||
);
|
||||
|
||||
$exportData = [];
|
||||
foreach ($rows ?: [] as $row) {
|
||||
$product = wc_get_product((int) $row['product_id']);
|
||||
$customer = get_userdata((int) $row['customer_id']);
|
||||
$order = wc_get_order((int) $row['order_id']);
|
||||
|
||||
$exportData[] = [
|
||||
'ID' => $row['id'],
|
||||
'License Key' => $row['license_key'],
|
||||
'Product' => $product ? $product->get_name() : 'Unknown',
|
||||
'Product ID' => $row['product_id'],
|
||||
'Order ID' => $row['order_id'],
|
||||
'Order Number' => $order ? $order->get_order_number() : '',
|
||||
'Customer' => $customer ? $customer->display_name : 'Guest',
|
||||
'Customer Email' => $customer ? $customer->user_email : '',
|
||||
'Customer ID' => $row['customer_id'],
|
||||
'Domain' => $row['domain'],
|
||||
'Status' => ucfirst($row['status']),
|
||||
'Activations' => $row['activations_count'],
|
||||
'Max Activations' => $row['max_activations'],
|
||||
'Expires At' => $row['expires_at'] ?: 'Lifetime',
|
||||
'Created At' => $row['created_at'],
|
||||
'Updated At' => $row['updated_at'],
|
||||
];
|
||||
}
|
||||
|
||||
return $exportData;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user