Deploy: 2026-05-22 23:55:19
This commit is contained in:
60
backend/app/Models/CompanySubscription.php
Normal file
60
backend/app/Models/CompanySubscription.php
Normal file
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Core\Database;
|
||||
|
||||
/**
|
||||
* CompanySubscription Model
|
||||
* Manages active tenancies linked to subscription plans.
|
||||
*/
|
||||
class CompanySubscription extends BaseModel
|
||||
{
|
||||
protected static string $table = 'company_subscriptions';
|
||||
|
||||
/**
|
||||
* Get active subscription for a company
|
||||
*/
|
||||
public static function findActiveByCompany(int $companyId): ?array
|
||||
{
|
||||
$now = date('Y-m-d H:i:s');
|
||||
return Database::selectOne(
|
||||
"SELECT cs.*, sp.name as plan_name, sp.max_sessions, sp.max_requests,
|
||||
sp.max_voice_requests, sp.max_ocr_requests, sp.features
|
||||
FROM " . static::$table . " cs
|
||||
JOIN subscription_plans sp ON cs.plan_id = sp.id
|
||||
WHERE cs.company_id = ?
|
||||
AND cs.status = 'active'
|
||||
AND cs.starts_at <= ?
|
||||
AND cs.ends_at >= ?
|
||||
LIMIT 1",
|
||||
[$companyId, $now, $now]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create or update subscription for a company
|
||||
*/
|
||||
public static function subscribeCompany(int $companyId, int $planId, int $durationDays = 30, ?string $gateway = null, ?string $ref = null): string
|
||||
{
|
||||
$now = time();
|
||||
$startsAt = date('Y-m-d H:i:s', $now);
|
||||
$endsAt = date('Y-m-d H:i:s', $now + ($durationDays * 86400));
|
||||
|
||||
// Deactivate previous active subscriptions
|
||||
Database::execute(
|
||||
"UPDATE " . static::$table . " SET status = 'expired' WHERE company_id = ? AND status = 'active'",
|
||||
[$companyId]
|
||||
);
|
||||
|
||||
return self::create([
|
||||
'company_id' => $companyId,
|
||||
'plan_id' => $planId,
|
||||
'status' => 'active',
|
||||
'starts_at' => $startsAt,
|
||||
'ends_at' => $endsAt,
|
||||
'payment_gateway' => $gateway,
|
||||
'subscription_ref' => $ref
|
||||
]);
|
||||
}
|
||||
}
|
||||
117
backend/app/Models/CompanySubscriptionUsage.php
Normal file
117
backend/app/Models/CompanySubscriptionUsage.php
Normal file
@@ -0,0 +1,117 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Core\Database;
|
||||
|
||||
/**
|
||||
* CompanySubscriptionUsage Model
|
||||
* Tracks API usage stats per company dynamically linked to active billing cycles.
|
||||
*/
|
||||
class CompanySubscriptionUsage extends BaseModel
|
||||
{
|
||||
protected static string $table = 'company_subscription_usage';
|
||||
|
||||
/**
|
||||
* Get or initialize usage record for the active billing cycle of a company
|
||||
*/
|
||||
public static function getOrCreateCurrentUsage(int $companyId, array $activeSubscription): array
|
||||
{
|
||||
$billingStart = date('Y-m-d', strtotime($activeSubscription['starts_at']));
|
||||
$billingEnd = date('Y-m-d', strtotime($activeSubscription['ends_at']));
|
||||
|
||||
// Check if usage record already exists
|
||||
$usage = Database::selectOne(
|
||||
"SELECT * FROM " . static::$table . "
|
||||
WHERE company_id = ? AND billing_start = ? AND billing_end = ?
|
||||
LIMIT 1",
|
||||
[$companyId, $billingStart, $billingEnd]
|
||||
);
|
||||
|
||||
if (!$usage) {
|
||||
// Initialize new record
|
||||
try {
|
||||
$id = self::create([
|
||||
'company_id' => $companyId,
|
||||
'billing_start' => $billingStart,
|
||||
'billing_end' => $billingEnd,
|
||||
'request_count' => 0,
|
||||
'voice_count' => 0,
|
||||
'ocr_count' => 0
|
||||
]);
|
||||
return [
|
||||
'id' => $id,
|
||||
'company_id' => $companyId,
|
||||
'billing_start' => $billingStart,
|
||||
'billing_end' => $billingEnd,
|
||||
'request_count' => 0,
|
||||
'voice_count' => 0,
|
||||
'ocr_count' => 0
|
||||
];
|
||||
} catch (\Exception $e) {
|
||||
// Handle concurrent insertions gracefully
|
||||
$usage = Database::selectOne(
|
||||
"SELECT * FROM " . static::$table . "
|
||||
WHERE company_id = ? AND billing_start = ? AND billing_end = ?
|
||||
LIMIT 1",
|
||||
[$companyId, $billingStart, $billingEnd]
|
||||
);
|
||||
if (!$usage) {
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $usage;
|
||||
}
|
||||
|
||||
/**
|
||||
* Increment usage counts for the current billing cycle
|
||||
*/
|
||||
public static function incrementUsage(int $companyId, string $type = 'request', int $amount = 1): bool
|
||||
{
|
||||
$activeSub = CompanySubscription::findActiveByCompany($companyId);
|
||||
if (!$activeSub) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$currentUsage = self::getOrCreateCurrentUsage($companyId, $activeSub);
|
||||
|
||||
$column = 'request_count';
|
||||
if ($type === 'voice') {
|
||||
$column = 'voice_count';
|
||||
} elseif ($type === 'ocr') {
|
||||
$column = 'ocr_count';
|
||||
}
|
||||
|
||||
return Database::execute(
|
||||
"UPDATE " . static::$table . "
|
||||
SET {$column} = {$column} + ?
|
||||
WHERE id = ?",
|
||||
[$amount, $currentUsage['id']]
|
||||
) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a company has exceeded its plan limits for a certain action
|
||||
*/
|
||||
public static function hasRemainingLimit(int $companyId, string $type = 'request'): bool
|
||||
{
|
||||
$activeSub = CompanySubscription::findActiveByCompany($companyId);
|
||||
if (!$activeSub) {
|
||||
return false; // No active subscription means no requests allowed
|
||||
}
|
||||
|
||||
$usage = self::getOrCreateCurrentUsage($companyId, $activeSub);
|
||||
|
||||
if ($type === 'request') {
|
||||
return $usage['request_count'] < $activeSub['max_requests'];
|
||||
} elseif ($type === 'voice') {
|
||||
return $usage['voice_count'] < $activeSub['max_voice_requests'];
|
||||
} elseif ($type === 'ocr') {
|
||||
return $usage['ocr_count'] < $activeSub['max_ocr_requests'];
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
25
backend/app/Models/SubscriptionPlan.php
Normal file
25
backend/app/Models/SubscriptionPlan.php
Normal file
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Core\Database;
|
||||
|
||||
/**
|
||||
* SubscriptionPlan Model
|
||||
* Represents SaaS pricing and request limit plans.
|
||||
*/
|
||||
class SubscriptionPlan extends BaseModel
|
||||
{
|
||||
protected static string $table = 'subscription_plans';
|
||||
|
||||
/**
|
||||
* Get plan by name
|
||||
*/
|
||||
public static function findByName(string $name): ?array
|
||||
{
|
||||
return Database::selectOne(
|
||||
"SELECT * FROM " . static::$table . " WHERE name = ? LIMIT 1",
|
||||
[$name]
|
||||
);
|
||||
}
|
||||
}
|
||||
75
backend/app/Models/WooCommerceStore.php
Normal file
75
backend/app/Models/WooCommerceStore.php
Normal file
@@ -0,0 +1,75 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Core\Database;
|
||||
use App\Core\Security;
|
||||
|
||||
/**
|
||||
* WooCommerceStore Model
|
||||
* Represents WooCommerce connections per company. Keys are encrypted.
|
||||
*/
|
||||
class WooCommerceStore extends BaseModel
|
||||
{
|
||||
protected static string $table = 'woocommerce_stores';
|
||||
|
||||
/**
|
||||
* Find store connection by company ID
|
||||
*/
|
||||
public static function findByCompany(int $companyId): ?array
|
||||
{
|
||||
return Database::selectOne(
|
||||
"SELECT * FROM " . static::$table . " WHERE company_id = ? LIMIT 1",
|
||||
[$companyId]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get decrypted credentials for API calls
|
||||
*/
|
||||
public static function getDecryptedCredentials(array $store): array
|
||||
{
|
||||
return [
|
||||
'store_url' => $store['store_url'],
|
||||
'consumer_key' => Security::decrypt($store['consumer_key']),
|
||||
'consumer_secret' => Security::decrypt($store['consumer_secret']),
|
||||
'webhook_secret' => $store['webhook_secret'] ?? null
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Save or update WooCommerce connection
|
||||
*/
|
||||
public static function saveStore(int $companyId, string $storeUrl, string $consumerKey, string $consumerSecret, ?string $webhookSecret = null): string
|
||||
{
|
||||
$encryptedKey = Security::encrypt($consumerKey);
|
||||
$encryptedSecret = Security::encrypt($consumerSecret);
|
||||
|
||||
$existing = self::findByCompany($companyId);
|
||||
$data = [
|
||||
'company_id' => $companyId,
|
||||
'store_url' => rtrim($storeUrl, '/'),
|
||||
'consumer_key' => $encryptedKey,
|
||||
'consumer_secret' => $encryptedSecret,
|
||||
'webhook_secret' => $webhookSecret
|
||||
];
|
||||
|
||||
if ($existing) {
|
||||
self::update($existing['id'], $data);
|
||||
return $existing['id'];
|
||||
} else {
|
||||
return self::create($data);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete WooCommerce connection
|
||||
*/
|
||||
public static function deleteByCompany(int $companyId): int
|
||||
{
|
||||
return Database::execute(
|
||||
"DELETE FROM " . static::$table . " WHERE company_id = ?",
|
||||
[$companyId]
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user