Merge pull request 'DX audit and fix (PHP)' (#50) from agent/dx-audit-and-fix--laravel-php-package into dev

Reviewed-on: #50
This commit is contained in:
Snider 2026-03-24 11:35:32 +00:00
commit c51e4310b1
52 changed files with 365 additions and 197 deletions

View file

@ -4,10 +4,20 @@ declare(strict_types=1);
namespace Core\Tenant;
use App\Services\UserStatsService;
use App\Services\WorkspaceCacheManager;
use App\Services\WorkspaceManager;
use App\Services\WorkspaceService;
use Core\Events\AdminPanelBooting;
use Core\Events\ApiRoutesRegistering;
use Core\Events\ConsoleBooting;
use Core\Events\WebRoutesRegistering;
use Core\Tenant\Contracts\TwoFactorAuthenticationProvider;
use Core\Tenant\Services\EntitlementService;
use Core\Tenant\Services\EntitlementWebhookService;
use Core\Tenant\Services\TotpService;
use Core\Tenant\Services\UsageAlertService;
use Core\Tenant\Services\WorkspaceTeamService;
use Illuminate\Support\Facades\Route;
use Illuminate\Support\ServiceProvider;
@ -40,48 +50,48 @@ class Boot extends ServiceProvider
public function register(): void
{
$this->app->singleton(
\Core\Tenant\Contracts\TwoFactorAuthenticationProvider::class,
\Core\Tenant\Services\TotpService::class
TwoFactorAuthenticationProvider::class,
TotpService::class
);
$this->app->singleton(
\Core\Tenant\Services\EntitlementService::class,
\Core\Tenant\Services\EntitlementService::class
EntitlementService::class,
EntitlementService::class
);
$this->app->singleton(
\Core\Tenant\Services\WorkspaceManager::class,
\Core\Tenant\Services\WorkspaceManager::class
Services\WorkspaceManager::class,
Services\WorkspaceManager::class
);
$this->app->singleton(
\Core\Tenant\Services\UserStatsService::class,
\Core\Tenant\Services\UserStatsService::class
Services\UserStatsService::class,
Services\UserStatsService::class
);
$this->app->singleton(
\Core\Tenant\Services\WorkspaceService::class,
\Core\Tenant\Services\WorkspaceService::class
Services\WorkspaceService::class,
Services\WorkspaceService::class
);
$this->app->singleton(
\Core\Tenant\Services\WorkspaceCacheManager::class,
\Core\Tenant\Services\WorkspaceCacheManager::class
Services\WorkspaceCacheManager::class,
Services\WorkspaceCacheManager::class
);
$this->app->singleton(
\Core\Tenant\Services\UsageAlertService::class,
\Core\Tenant\Services\UsageAlertService::class
UsageAlertService::class,
UsageAlertService::class
);
$this->app->singleton(
\Core\Tenant\Services\EntitlementWebhookService::class,
\Core\Tenant\Services\EntitlementWebhookService::class
EntitlementWebhookService::class,
EntitlementWebhookService::class
);
$this->app->singleton(
\Core\Tenant\Services\WorkspaceTeamService::class,
\Core\Tenant\Services\WorkspaceTeamService::class
WorkspaceTeamService::class,
WorkspaceTeamService::class
);
$this->registerBackwardCompatAliases();
@ -89,31 +99,31 @@ class Boot extends ServiceProvider
protected function registerBackwardCompatAliases(): void
{
if (! class_exists(\App\Services\WorkspaceManager::class)) {
if (! class_exists(WorkspaceManager::class)) {
class_alias(
\Core\Tenant\Services\WorkspaceManager::class,
\App\Services\WorkspaceManager::class
Services\WorkspaceManager::class,
WorkspaceManager::class
);
}
if (! class_exists(\App\Services\UserStatsService::class)) {
if (! class_exists(UserStatsService::class)) {
class_alias(
\Core\Tenant\Services\UserStatsService::class,
\App\Services\UserStatsService::class
Services\UserStatsService::class,
UserStatsService::class
);
}
if (! class_exists(\App\Services\WorkspaceService::class)) {
if (! class_exists(WorkspaceService::class)) {
class_alias(
\Core\Tenant\Services\WorkspaceService::class,
\App\Services\WorkspaceService::class
Services\WorkspaceService::class,
WorkspaceService::class
);
}
if (! class_exists(\App\Services\WorkspaceCacheManager::class)) {
if (! class_exists(WorkspaceCacheManager::class)) {
class_alias(
\Core\Tenant\Services\WorkspaceCacheManager::class,
\App\Services\WorkspaceCacheManager::class
Services\WorkspaceCacheManager::class,
WorkspaceCacheManager::class
);
}
}

View file

@ -4,6 +4,7 @@ declare(strict_types=1);
namespace Core\Tenant\Controllers;
use Core\Front\Controller;
use Core\Helpers\PrivacyHelper;
use Core\Mod\Trees\Models\TreePlanting;
use Core\Mod\Trees\Models\TreePlantingStats;
@ -21,7 +22,7 @@ use Illuminate\Support\Facades\Cookie;
*
* On signup, PlantTreeForAgentReferral listener plants a tree for the referrer.
*/
class ReferralController extends \Core\Front\Controller
class ReferralController extends Controller
{
/**
* Cookie name for agent referral tracking.

View file

@ -9,6 +9,8 @@ use Core\Tenant\Models\User;
use Core\Tenant\Models\Workspace;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use Mod\Api\Controllers\Concerns\HasApiResponses;
use Mod\Api\Controllers\Concerns\ResolvesWorkspace;
use Mod\Api\Resources\PaginatedCollection;
@ -133,7 +135,7 @@ class WorkspaceController extends Controller
// Generate slug if not provided
if (empty($validated['slug'])) {
$validated['slug'] = \Illuminate\Support\Str::slug($validated['name']).'-'.\Illuminate\Support\Str::random(6);
$validated['slug'] = Str::slug($validated['name']).'-'.Str::random(6);
}
// Set default domain
@ -252,9 +254,9 @@ class WorkspaceController extends Controller
// Use a single transaction with optimised query:
// Clear all defaults and set the new one in one operation using raw update
\Illuminate\Support\Facades\DB::transaction(function () use ($user, $workspace) {
DB::transaction(function () use ($user, $workspace) {
// Clear all existing defaults for this user's hub workspaces
\Illuminate\Support\Facades\DB::table('user_workspace')
DB::table('user_workspace')
->where('user_id', $user->id)
->whereIn('workspace_id', function ($query) {
$query->select('id')
@ -264,7 +266,7 @@ class WorkspaceController extends Controller
->update(['is_default' => false]);
// Set the new default
\Illuminate\Support\Facades\DB::table('user_workspace')
DB::table('user_workspace')
->where('user_id', $user->id)
->where('workspace_id', $workspace->id)
->update(['is_default' => true]);

View file

@ -1,13 +1,16 @@
<?php
declare(strict_types=1);
namespace Core\Tenant\Database\Factories;
use Core\Tenant\Models\User;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Str;
/**
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\Core\Tenant\Models\User>
* @extends Factory<User>
*/
class UserFactory extends Factory
{
@ -17,7 +20,7 @@ class UserFactory extends Factory
* Uses the backward-compatible alias class to ensure type compatibility
* with existing code that expects Mod\Tenant\Models\User.
*/
protected $model = \Core\Tenant\Models\User::class;
protected $model = User::class;
/**
* The current password being used by the factory.

View file

@ -8,7 +8,7 @@ use Core\Tenant\Models\WaitlistEntry;
use Illuminate\Database\Eloquent\Factories\Factory;
/**
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\Core\Tenant\Models\WaitlistEntry>
* @extends Factory<WaitlistEntry>
*/
class WaitlistEntryFactory extends Factory
{

View file

@ -1,12 +1,14 @@
<?php
declare(strict_types=1);
namespace Core\Tenant\Database\Factories;
use Core\Tenant\Models\Workspace;
use Illuminate\Database\Eloquent\Factories\Factory;
/**
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\Core\Tenant\Models\Workspace>
* @extends Factory<Workspace>
*/
class WorkspaceFactory extends Factory
{

View file

@ -9,7 +9,7 @@ use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Str;
/**
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\Core\Tenant\Models\WorkspaceInvitation>
* @extends Factory<WorkspaceInvitation>
*/
class WorkspaceInvitationFactory extends Factory
{

View file

@ -1,7 +1,10 @@
<?php
declare(strict_types=1);
namespace Core\Tenant\Database\Seeders;
use Core\Mod\Web\Models\Page;
use Core\Tenant\Models\Package;
use Core\Tenant\Models\User;
use Core\Tenant\Models\Workspace;
@ -94,11 +97,11 @@ class DemoTestUserSeeder extends Seeder
protected function createTestBioPage(Workspace $workspace, User $user): void
{
// Only create if Web Page model exists and no test page exists
if (! class_exists(\Core\Mod\Web\Models\Page::class)) {
if (! class_exists(Page::class)) {
return;
}
$existingPage = \Core\Mod\Web\Models\Page::where('workspace_id', $workspace->id)
$existingPage = Page::where('workspace_id', $workspace->id)
->where('url', 'nyx-test')
->first();
@ -106,7 +109,7 @@ class DemoTestUserSeeder extends Seeder
return;
}
\Core\Mod\Web\Models\Page::create([
Page::create([
'workspace_id' => $workspace->id,
'user_id' => $user->id,
'url' => 'nyx-test',
@ -146,11 +149,11 @@ class DemoTestUserSeeder extends Seeder
protected function createTestShortLink(Workspace $workspace, User $user): void
{
// Only create if Web Page model exists
if (! class_exists(\Core\Mod\Web\Models\Page::class)) {
if (! class_exists(Page::class)) {
return;
}
$existingLink = \Core\Mod\Web\Models\Page::where('workspace_id', $workspace->id)
$existingLink = Page::where('workspace_id', $workspace->id)
->where('url', 'nyx-short')
->first();
@ -158,7 +161,7 @@ class DemoTestUserSeeder extends Seeder
return;
}
\Core\Mod\Web\Models\Page::create([
Page::create([
'workspace_id' => $workspace->id,
'user_id' => $user->id,
'url' => 'nyx-short',

View file

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace Core\Tenant\Database\Seeders;
use Core\Tenant\Models\Feature;

View file

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace Core\Tenant\Database\Seeders;
use Core\Tenant\Models\Feature;

View file

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace Core\Tenant\Database\Seeders;
use Core\Tenant\Models\Package;

View file

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace Core\Tenant\Database\Seeders;
use Core\Tenant\Enums\UserTier;

View file

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace Core\Tenant\Enums;
enum UserTier: string

View file

@ -8,6 +8,7 @@ use Core\Tenant\Models\AccountDeletionRequest;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Attachment;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;
use Illuminate\Queue\SerializesModels;
@ -53,7 +54,7 @@ class AccountDeletionRequested extends Mailable implements ShouldQueue
/**
* Get the attachments for the message.
*
* @return array<int, \Illuminate\Mail\Mailables\Attachment>
* @return array<int, Attachment>
*/
public function attachments(): array
{

View file

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace Core\Tenant\Models;
use Illuminate\Database\Eloquent\Model;

View file

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace Core\Tenant\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;

View file

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace Core\Tenant\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;

View file

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace Core\Tenant\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;

View file

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace Core\Tenant\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;

View file

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace Core\Tenant\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;

View file

@ -4,8 +4,11 @@ declare(strict_types=1);
namespace Core\Tenant\Models;
use Core\Tenant\Database\Factories\UserFactory;
use Core\Tenant\Enums\UserTier;
use Illuminate\Auth\Notifications\VerifyEmail;
use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
@ -22,9 +25,9 @@ class User extends Authenticatable implements MustVerifyEmail
/**
* Create a new factory instance for the model.
*/
protected static function newFactory(): \Core\Tenant\Database\Factories\UserFactory
protected static function newFactory(): UserFactory
{
return \Core\Tenant\Database\Factories\UserFactory::new();
return UserFactory::new();
}
/**
@ -235,7 +238,7 @@ class User extends Authenticatable implements MustVerifyEmail
/**
* Get all namespaces accessible by this user (owned + via workspaces).
*/
public function accessibleNamespaces(): \Illuminate\Database\Eloquent\Builder
public function accessibleNamespaces(): Builder
{
return Namespace_::accessibleBy($this);
}
@ -269,7 +272,7 @@ class User extends Authenticatable implements MustVerifyEmail
*/
public function sendEmailVerificationNotification(): void
{
$this->notify(new \Illuminate\Auth\Notifications\VerifyEmail);
$this->notify(new VerifyEmail);
}
/**

View file

@ -4,6 +4,7 @@ declare(strict_types=1);
namespace Core\Tenant\Models;
use Core\Tenant\Database\Factories\UserTokenFactory;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
@ -18,9 +19,9 @@ class UserToken extends Model
{
use HasFactory;
protected static function newFactory(): \Core\Tenant\Database\Factories\UserTokenFactory
protected static function newFactory(): UserTokenFactory
{
return \Core\Tenant\Database\Factories\UserTokenFactory::new();
return UserTokenFactory::new();
}
/**

View file

@ -1,7 +1,10 @@
<?php
declare(strict_types=1);
namespace Core\Tenant\Models;
use Core\Tenant\Database\Factories\WaitlistEntryFactory;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
@ -13,9 +16,9 @@ class WaitlistEntry extends Model
use HasFactory;
use Notifiable;
protected static function newFactory(): \Core\Tenant\Database\Factories\WaitlistEntryFactory
protected static function newFactory(): WaitlistEntryFactory
{
return \Core\Tenant\Database\Factories\WaitlistEntryFactory::new();
return WaitlistEntryFactory::new();
}
protected $fillable = [

View file

@ -4,6 +4,39 @@ declare(strict_types=1);
namespace Core\Tenant\Models;
use App\Services\WorkspaceService;
use Core\Mod\Analytics\Models\AnalyticsGoal;
use Core\Mod\Analytics\Models\AnalyticsWebsite;
use Core\Mod\Analytics\Models\Goal;
use Core\Mod\Analytics\Models\Website;
use Core\Mod\Api\Models\ApiKey;
use Core\Mod\Api\Models\WebhookEndpoint;
use Core\Mod\Content\Models\ContentAuthor;
use Core\Mod\Content\Models\ContentItem;
use Core\Mod\Notify\Models\PushCampaign;
use Core\Mod\Notify\Models\PushFlow;
use Core\Mod\Notify\Models\PushSegment;
use Core\Mod\Notify\Models\PushWebsite;
use Core\Mod\Social\Models\Account;
use Core\Mod\Social\Models\Analytics;
use Core\Mod\Social\Models\Audience;
use Core\Mod\Social\Models\FacebookInsight;
use Core\Mod\Social\Models\HashtagGroup;
use Core\Mod\Social\Models\ImportedPost;
use Core\Mod\Social\Models\InstagramInsight;
use Core\Mod\Social\Models\Media;
use Core\Mod\Social\Models\Metric;
use Core\Mod\Social\Models\PinterestAnalytic;
use Core\Mod\Social\Models\Post;
use Core\Mod\Social\Models\PostingSchedule;
use Core\Mod\Social\Models\Template;
use Core\Mod\Social\Models\Variable;
use Core\Mod\Social\Models\Webhook;
use Core\Mod\Trees\Models\TreePlanting;
use Core\Mod\Trust\Models\Campaign;
use Core\Mod\Trust\Models\Notification;
use Core\Tenant\Database\Factories\WorkspaceFactory;
use Core\Tenant\Notifications\WorkspaceInvitationNotification;
use Core\Tenant\Services\EntitlementResult;
use Core\Tenant\Services\EntitlementService;
use Illuminate\Database\Eloquent\Factories\HasFactory;
@ -11,14 +44,19 @@ use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\MorphMany;
use Illuminate\Support\Collection;
use Mod\Commerce\Models\Invoice;
use Mod\Commerce\Models\Order;
use Mod\Commerce\Models\PaymentMethod;
use Mod\Commerce\Models\Subscription;
class Workspace extends Model
{
use HasFactory;
protected static function newFactory(): \Core\Tenant\Database\Factories\WorkspaceFactory
protected static function newFactory(): WorkspaceFactory
{
return \Core\Tenant\Database\Factories\WorkspaceFactory::new();
return WorkspaceFactory::new();
}
protected $fillable = [
@ -235,7 +273,7 @@ class Workspace extends Model
*/
public function socialAccounts(): HasMany
{
return $this->hasMany(\Core\Mod\Social\Models\Account::class);
return $this->hasMany(Account::class);
}
/**
@ -243,7 +281,7 @@ class Workspace extends Model
*/
public function socialPosts(): HasMany
{
return $this->hasMany(\Core\Mod\Social\Models\Post::class);
return $this->hasMany(Post::class);
}
/**
@ -251,7 +289,7 @@ class Workspace extends Model
*/
public function socialTemplates(): HasMany
{
return $this->hasMany(\Core\Mod\Social\Models\Template::class);
return $this->hasMany(Template::class);
}
/**
@ -259,7 +297,7 @@ class Workspace extends Model
*/
public function socialMedia(): HasMany
{
return $this->hasMany(\Core\Mod\Social\Models\Media::class);
return $this->hasMany(Media::class);
}
/**
@ -267,7 +305,7 @@ class Workspace extends Model
*/
public function socialHashtagGroups(): HasMany
{
return $this->hasMany(\Core\Mod\Social\Models\HashtagGroup::class);
return $this->hasMany(HashtagGroup::class);
}
/**
@ -275,7 +313,7 @@ class Workspace extends Model
*/
public function socialWebhooks(): HasMany
{
return $this->hasMany(\Core\Mod\Social\Models\Webhook::class);
return $this->hasMany(Webhook::class);
}
/**
@ -283,7 +321,7 @@ class Workspace extends Model
*/
public function socialAnalytics(): HasMany
{
return $this->hasMany(\Core\Mod\Social\Models\Analytics::class);
return $this->hasMany(Analytics::class);
}
/**
@ -291,7 +329,7 @@ class Workspace extends Model
*/
public function socialVariables(): HasMany
{
return $this->hasMany(\Core\Mod\Social\Models\Variable::class);
return $this->hasMany(Variable::class);
}
/**
@ -299,7 +337,7 @@ class Workspace extends Model
*/
public function socialPostingSchedule(): HasMany
{
return $this->hasMany(\Core\Mod\Social\Models\PostingSchedule::class);
return $this->hasMany(PostingSchedule::class);
}
/**
@ -307,7 +345,7 @@ class Workspace extends Model
*/
public function socialImportedPosts(): HasMany
{
return $this->hasMany(\Core\Mod\Social\Models\ImportedPost::class);
return $this->hasMany(ImportedPost::class);
}
/**
@ -315,7 +353,7 @@ class Workspace extends Model
*/
public function socialMetrics(): HasMany
{
return $this->hasMany(\Core\Mod\Social\Models\Metric::class);
return $this->hasMany(Metric::class);
}
/**
@ -323,7 +361,7 @@ class Workspace extends Model
*/
public function socialAudience(): HasMany
{
return $this->hasMany(\Core\Mod\Social\Models\Audience::class);
return $this->hasMany(Audience::class);
}
/**
@ -331,7 +369,7 @@ class Workspace extends Model
*/
public function socialFacebookInsights(): HasMany
{
return $this->hasMany(\Core\Mod\Social\Models\FacebookInsight::class);
return $this->hasMany(FacebookInsight::class);
}
/**
@ -339,7 +377,7 @@ class Workspace extends Model
*/
public function socialInstagramInsights(): HasMany
{
return $this->hasMany(\Core\Mod\Social\Models\InstagramInsight::class);
return $this->hasMany(InstagramInsight::class);
}
/**
@ -347,7 +385,7 @@ class Workspace extends Model
*/
public function socialPinterestAnalytics(): HasMany
{
return $this->hasMany(\Core\Mod\Social\Models\PinterestAnalytic::class);
return $this->hasMany(PinterestAnalytic::class);
}
/**
@ -376,7 +414,7 @@ class Workspace extends Model
*/
public function analyticsSites(): HasMany
{
return $this->hasMany(\Core\Mod\Analytics\Models\Website::class);
return $this->hasMany(Website::class);
}
/**
@ -384,7 +422,7 @@ class Workspace extends Model
*/
public function socialAnalyticsWebsites(): HasMany
{
return $this->hasMany(\Core\Mod\Analytics\Models\AnalyticsWebsite::class);
return $this->hasMany(AnalyticsWebsite::class);
}
/**
@ -392,7 +430,7 @@ class Workspace extends Model
*/
public function analyticsGoals(): HasMany
{
return $this->hasMany(\Core\Mod\Analytics\Models\Goal::class);
return $this->hasMany(Goal::class);
}
/**
@ -400,7 +438,7 @@ class Workspace extends Model
*/
public function socialAnalyticsGoals(): HasMany
{
return $this->hasMany(\Core\Mod\Analytics\Models\AnalyticsGoal::class);
return $this->hasMany(AnalyticsGoal::class);
}
// TrustHost Relationships
@ -410,7 +448,7 @@ class Workspace extends Model
*/
public function trustWidgets(): HasMany
{
return $this->hasMany(\Core\Mod\Trust\Models\Campaign::class);
return $this->hasMany(Campaign::class);
}
/**
@ -418,7 +456,7 @@ class Workspace extends Model
*/
public function trustNotifications(): HasMany
{
return $this->hasMany(\Core\Mod\Trust\Models\Notification::class);
return $this->hasMany(Notification::class);
}
// NotifyHost Relationships
@ -428,7 +466,7 @@ class Workspace extends Model
*/
public function notificationSites(): HasMany
{
return $this->hasMany(\Core\Mod\Notify\Models\PushWebsite::class);
return $this->hasMany(PushWebsite::class);
}
/**
@ -436,7 +474,7 @@ class Workspace extends Model
*/
public function pushCampaigns(): HasMany
{
return $this->hasMany(\Core\Mod\Notify\Models\PushCampaign::class);
return $this->hasMany(PushCampaign::class);
}
/**
@ -444,7 +482,7 @@ class Workspace extends Model
*/
public function pushFlows(): HasMany
{
return $this->hasMany(\Core\Mod\Notify\Models\PushFlow::class);
return $this->hasMany(PushFlow::class);
}
/**
@ -452,7 +490,7 @@ class Workspace extends Model
*/
public function pushSegments(): HasMany
{
return $this->hasMany(\Core\Mod\Notify\Models\PushSegment::class);
return $this->hasMany(PushSegment::class);
}
// API & Webhooks Relationships
@ -462,7 +500,7 @@ class Workspace extends Model
*/
public function apiKeys(): HasMany
{
return $this->hasMany(\Core\Mod\Api\Models\ApiKey::class);
return $this->hasMany(ApiKey::class);
}
/**
@ -470,7 +508,7 @@ class Workspace extends Model
*/
public function webhookEndpoints(): HasMany
{
return $this->hasMany(\Core\Mod\Api\Models\WebhookEndpoint::class);
return $this->hasMany(WebhookEndpoint::class);
}
/**
@ -488,7 +526,7 @@ class Workspace extends Model
*/
public function treePlantings(): HasMany
{
return $this->hasMany(\Core\Mod\Trees\Models\TreePlanting::class);
return $this->hasMany(TreePlanting::class);
}
/**
@ -519,7 +557,7 @@ class Workspace extends Model
*/
public function contentItems(): HasMany
{
return $this->hasMany(\Core\Mod\Content\Models\ContentItem::class);
return $this->hasMany(ContentItem::class);
}
/**
@ -527,7 +565,7 @@ class Workspace extends Model
*/
public function contentAuthors(): HasMany
{
return $this->hasMany(\Core\Mod\Content\Models\ContentAuthor::class);
return $this->hasMany(ContentAuthor::class);
}
// Commerce Relationships (defined in app Mod\Commerce)
@ -537,7 +575,7 @@ class Workspace extends Model
*/
public function subscriptions(): HasMany
{
return $this->hasMany(\Mod\Commerce\Models\Subscription::class);
return $this->hasMany(Subscription::class);
}
/**
@ -545,7 +583,7 @@ class Workspace extends Model
*/
public function invoices(): HasMany
{
return $this->hasMany(\Mod\Commerce\Models\Invoice::class);
return $this->hasMany(Invoice::class);
}
/**
@ -553,7 +591,7 @@ class Workspace extends Model
*/
public function paymentMethods(): HasMany
{
return $this->hasMany(\Mod\Commerce\Models\PaymentMethod::class);
return $this->hasMany(PaymentMethod::class);
}
/**
@ -561,7 +599,7 @@ class Workspace extends Model
*/
public function orders(): MorphMany
{
return $this->morphMany(\Mod\Commerce\Models\Order::class, 'orderable');
return $this->morphMany(Order::class, 'orderable');
}
// Helper Methods
@ -579,12 +617,12 @@ class Workspace extends Model
}
// Try to get from authenticated user's default workspace
if (auth()->check() && auth()->user() instanceof \Core\Tenant\Models\User) {
if (auth()->check() && auth()->user() instanceof User) {
return auth()->user()->defaultHostWorkspace();
}
// Try to resolve from subdomain via WorkspaceService
$workspaceService = app(\App\Services\WorkspaceService::class);
$workspaceService = app(WorkspaceService::class);
$slug = $workspaceService->currentSlug();
return static::where('slug', $slug)->first();
@ -609,7 +647,7 @@ class Workspace extends Model
/**
* Get usage summary for all features.
*/
public function getUsageSummary(): \Illuminate\Support\Collection
public function getUsageSummary(): Collection
{
return app(EntitlementService::class)->getUsageSummary($this);
}
@ -675,7 +713,7 @@ class Workspace extends Model
$existing->save();
// Send notification with the new plaintext token
$existing->notify(new \Core\Tenant\Notifications\WorkspaceInvitationNotification($existing, $plaintextToken));
$existing->notify(new WorkspaceInvitationNotification($existing, $plaintextToken));
return $existing;
}
@ -693,7 +731,7 @@ class Workspace extends Model
]);
// Send notification with the plaintext token (not the hashed one)
$invitation->notify(new \Core\Tenant\Notifications\WorkspaceInvitationNotification($invitation, $plaintextToken));
$invitation->notify(new WorkspaceInvitationNotification($invitation, $plaintextToken));
return $invitation;
}

View file

@ -4,6 +4,7 @@ declare(strict_types=1);
namespace Core\Tenant\Models;
use Core\Tenant\Database\Factories\WorkspaceInvitationFactory;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
@ -16,9 +17,9 @@ class WorkspaceInvitation extends Model
use HasFactory;
use Notifiable;
protected static function newFactory(): \Core\Tenant\Database\Factories\WorkspaceInvitationFactory
protected static function newFactory(): WorkspaceInvitationFactory
{
return \Core\Tenant\Database\Factories\WorkspaceInvitationFactory::new();
return WorkspaceInvitationFactory::new();
}
protected $fillable = [

View file

@ -4,6 +4,7 @@ declare(strict_types=1);
namespace Core\Tenant\Models;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
@ -20,10 +21,10 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
* @property int|null $team_id
* @property array|null $custom_permissions
* @property bool $is_default
* @property \Carbon\Carbon|null $joined_at
* @property Carbon|null $joined_at
* @property int|null $invited_by
* @property \Carbon\Carbon $created_at
* @property \Carbon\Carbon $updated_at
* @property Carbon $created_at
* @property Carbon $updated_at
*/
class WorkspaceMember extends Model
{

View file

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace Core\Tenant\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;

View file

@ -4,6 +4,7 @@ declare(strict_types=1);
namespace Core\Tenant\Models;
use Carbon\Carbon;
use Core\Tenant\Concerns\BelongsToWorkspace;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
@ -26,8 +27,8 @@ use Illuminate\Support\Str;
* @property bool $is_system
* @property string $colour
* @property int $sort_order
* @property \Carbon\Carbon $created_at
* @property \Carbon\Carbon $updated_at
* @property Carbon $created_at
* @property Carbon $updated_at
*/
class WorkspaceTeam extends Model
{

View file

@ -2,6 +2,8 @@
declare(strict_types=1);
use Core\Tenant\View\Modal\Admin\MemberManager;
use Core\Tenant\View\Modal\Admin\TeamManager;
use Illuminate\Support\Facades\Route;
/*
@ -15,10 +17,10 @@ use Illuminate\Support\Facades\Route;
Route::middleware(['web', 'auth', 'admin.domain'])->prefix('admin/tenant')->name('hub.admin.tenant.')->group(function () {
// Team Manager
Route::get('/teams', \Core\Tenant\View\Modal\Admin\TeamManager::class)
Route::get('/teams', TeamManager::class)
->name('teams');
// Member Manager
Route::get('/members', \Core\Tenant\View\Modal\Admin\MemberManager::class)
Route::get('/members', MemberManager::class)
->name('members');
});

View file

@ -8,6 +8,7 @@ declare(strict_types=1);
* Account management and workspace routes.
*/
use Core\Tenant\Controllers\WorkspaceInvitationController;
use Core\Tenant\View\Modal\Web\CancelDeletion;
use Core\Tenant\View\Modal\Web\ConfirmDeletion;
use Core\Tenant\View\Modal\Web\WorkspaceHome;
@ -41,7 +42,7 @@ Route::prefix('account')->name('account.')->group(function () {
|
*/
Route::get('/workspace/invitation/{token}', \Core\Tenant\Controllers\WorkspaceInvitationController::class)
Route::get('/workspace/invitation/{token}', WorkspaceInvitationController::class)
->name('workspace.invitation.accept');
/*

View file

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace Core\Tenant\Services;
/**

View file

@ -17,6 +17,7 @@ use Core\Tenant\Jobs\DispatchEntitlementWebhook;
use Core\Tenant\Models\EntitlementWebhook;
use Core\Tenant\Models\EntitlementWebhookDelivery;
use Core\Tenant\Models\Workspace;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Str;
@ -425,7 +426,7 @@ class EntitlementWebhookService
/**
* Get webhooks for a workspace.
*/
public function getWebhooksForWorkspace(Workspace $workspace): \Illuminate\Database\Eloquent\Collection
public function getWebhooksForWorkspace(Workspace $workspace): Collection
{
return EntitlementWebhook::query()
->forWorkspace($workspace)
@ -437,7 +438,7 @@ class EntitlementWebhookService
/**
* Get delivery history for a webhook.
*/
public function getDeliveryHistory(EntitlementWebhook $webhook, int $limit = 50): \Illuminate\Database\Eloquent\Collection
public function getDeliveryHistory(EntitlementWebhook $webhook, int $limit = 50): Collection
{
return $webhook->deliveries()
->latest('created_at')

View file

@ -5,6 +5,7 @@ declare(strict_types=1);
namespace Core\Tenant\Services;
use Core\Tenant\Enums\UserTier;
use Core\Tenant\Jobs\ComputeUserStats;
use Core\Tenant\Models\User;
use Illuminate\Support\Facades\Cache;
@ -44,7 +45,7 @@ class UserStatsService
// For page loads, return cached data immediately and queue refresh
if ($user->cached_stats) {
// Queue background refresh
dispatch(new \Core\Tenant\Jobs\ComputeUserStats($user->id))->onQueue('stats');
dispatch(new ComputeUserStats($user->id))->onQueue('stats');
return $user->cached_stats;
}

View file

@ -7,7 +7,9 @@ namespace Core\Tenant\View\Modal\Admin;
use Core\Tenant\Models\Workspace;
use Core\Tenant\Models\WorkspaceMember;
use Core\Tenant\Models\WorkspaceTeam;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Illuminate\Contracts\View\View;
use Illuminate\Database\Eloquent\Collection;
use Livewire\Attributes\Computed;
use Livewire\Component;
use Livewire\WithPagination;
@ -287,7 +289,7 @@ class MemberManager extends Component
// ─────────────────────────────────────────────────────────────────────────
#[Computed]
public function members(): \Illuminate\Contracts\Pagination\LengthAwarePaginator
public function members(): LengthAwarePaginator
{
return WorkspaceMember::query()
->with(['user', 'workspace', 'team', 'inviter'])
@ -308,13 +310,13 @@ class MemberManager extends Component
}
#[Computed]
public function workspaces(): \Illuminate\Database\Eloquent\Collection
public function workspaces(): Collection
{
return Workspace::orderBy('name')->get();
}
#[Computed]
public function teamsForFilter(): \Illuminate\Database\Eloquent\Collection
public function teamsForFilter(): Collection
{
$query = WorkspaceTeam::query();
@ -326,7 +328,7 @@ class MemberManager extends Component
}
#[Computed]
public function teamsForAssignment(): \Illuminate\Database\Eloquent\Collection
public function teamsForAssignment(): Collection
{
if ($this->assignMemberId) {
$member = WorkspaceMember::find($this->assignMemberId);
@ -337,11 +339,11 @@ class MemberManager extends Component
}
}
return new \Illuminate\Database\Eloquent\Collection;
return new Collection;
}
#[Computed]
public function teamsForBulkAssignment(): \Illuminate\Database\Eloquent\Collection
public function teamsForBulkAssignment(): Collection
{
// Only show teams from the current workspace filter
if ($this->workspaceFilter) {
@ -350,7 +352,7 @@ class MemberManager extends Component
->get();
}
return new \Illuminate\Database\Eloquent\Collection;
return new Collection;
}
#[Computed]

View file

@ -8,7 +8,9 @@ use Core\Tenant\Models\Workspace;
use Core\Tenant\Models\WorkspaceMember;
use Core\Tenant\Models\WorkspaceTeam;
use Core\Tenant\Services\WorkspaceTeamService;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Illuminate\Contracts\View\View;
use Illuminate\Database\Eloquent\Collection;
use Livewire\Attributes\Computed;
use Livewire\Component;
use Livewire\WithPagination;
@ -219,7 +221,7 @@ class TeamManager extends Component
// ─────────────────────────────────────────────────────────────────────────
#[Computed]
public function teams(): \Illuminate\Contracts\Pagination\LengthAwarePaginator
public function teams(): LengthAwarePaginator
{
return WorkspaceTeam::query()
->with(['workspace'])
@ -239,7 +241,7 @@ class TeamManager extends Component
}
#[Computed]
public function workspaces(): \Illuminate\Database\Eloquent\Collection
public function workspaces(): Collection
{
return Workspace::orderBy('name')->get();
}

View file

@ -1,9 +1,29 @@
<?php
declare(strict_types=1);
namespace Core\Tenant\View\Modal\Admin;
use Carbon\Carbon;
use Core\Mod\Analytics\Models\Website;
use Core\Mod\Api\Models\ApiKey;
use Core\Mod\Content\Models\ContentItem;
use Core\Mod\Notify\Models\PushWebsite;
use Core\Mod\Social\Models\Account;
use Core\Mod\Social\Models\Post;
use Core\Mod\Trust\Models\Campaign;
use Core\Mod\Web\Models\Page;
use Core\Mod\Web\Models\Project;
use Core\Tenant\Models\Boost;
use Core\Tenant\Models\EntitlementLog;
use Core\Tenant\Models\Feature;
use Core\Tenant\Models\Package;
use Core\Tenant\Models\UsageRecord;
use Core\Tenant\Models\User;
use Core\Tenant\Models\Workspace;
use Core\Tenant\Models\WorkspacePackage;
use Core\Tenant\Services\EntitlementService;
use Illuminate\Support\Facades\Schema;
use Livewire\Attributes\Computed;
use Livewire\Component;
@ -87,18 +107,18 @@ class WorkspaceDetails extends Component
public function resourceCounts(): array
{
$counts = [];
$schema = \Illuminate\Support\Facades\Schema::getFacadeRoot();
$schema = Schema::getFacadeRoot();
$resources = [
['relation' => 'bioPages', 'label' => 'Bio Pages', 'icon' => 'link', 'color' => 'blue', 'model' => \Core\Mod\Web\Models\Page::class],
['relation' => 'bioProjects', 'label' => 'Bio Projects', 'icon' => 'folder', 'color' => 'indigo', 'model' => \Core\Mod\Web\Models\Project::class],
['relation' => 'socialAccounts', 'label' => 'Social Accounts', 'icon' => 'share-nodes', 'color' => 'purple', 'model' => \Core\Mod\Social\Models\Account::class],
['relation' => 'socialPosts', 'label' => 'Social Posts', 'icon' => 'paper-plane', 'color' => 'pink', 'model' => \Core\Mod\Social\Models\Post::class],
['relation' => 'analyticsSites', 'label' => 'Analytics Sites', 'icon' => 'chart-line', 'color' => 'cyan', 'model' => \Core\Mod\Analytics\Models\Website::class],
['relation' => 'trustWidgets', 'label' => 'Trust Campaigns', 'icon' => 'shield-check', 'color' => 'emerald', 'model' => \Core\Mod\Trust\Models\Campaign::class],
['relation' => 'notificationSites', 'label' => 'Notification Sites', 'icon' => 'bell', 'color' => 'amber', 'model' => \Core\Mod\Notify\Models\PushWebsite::class],
['relation' => 'contentItems', 'label' => 'Content Items', 'icon' => 'file-lines', 'color' => 'slate', 'model' => \Core\Mod\Content\Models\ContentItem::class],
['relation' => 'apiKeys', 'label' => 'API Keys', 'icon' => 'key', 'color' => 'rose', 'model' => \Core\Mod\Api\Models\ApiKey::class],
['relation' => 'bioPages', 'label' => 'Bio Pages', 'icon' => 'link', 'color' => 'blue', 'model' => Page::class],
['relation' => 'bioProjects', 'label' => 'Bio Projects', 'icon' => 'folder', 'color' => 'indigo', 'model' => Project::class],
['relation' => 'socialAccounts', 'label' => 'Social Accounts', 'icon' => 'share-nodes', 'color' => 'purple', 'model' => Account::class],
['relation' => 'socialPosts', 'label' => 'Social Posts', 'icon' => 'paper-plane', 'color' => 'pink', 'model' => Post::class],
['relation' => 'analyticsSites', 'label' => 'Analytics Sites', 'icon' => 'chart-line', 'color' => 'cyan', 'model' => Website::class],
['relation' => 'trustWidgets', 'label' => 'Trust Campaigns', 'icon' => 'shield-check', 'color' => 'emerald', 'model' => Campaign::class],
['relation' => 'notificationSites', 'label' => 'Notification Sites', 'icon' => 'bell', 'color' => 'amber', 'model' => PushWebsite::class],
['relation' => 'contentItems', 'label' => 'Content Items', 'icon' => 'file-lines', 'color' => 'slate', 'model' => ContentItem::class],
['relation' => 'apiKeys', 'label' => 'API Keys', 'icon' => 'key', 'color' => 'rose', 'model' => ApiKey::class],
];
foreach ($resources as $resource) {
@ -125,7 +145,7 @@ class WorkspaceDetails extends Component
$activities = collect();
// Entitlement logs
if (class_exists(\Core\Tenant\Models\EntitlementLog::class)) {
if (class_exists(EntitlementLog::class)) {
try {
$logs = $this->workspace->entitlementLogs()
->with('user', 'feature')
@ -147,7 +167,7 @@ class WorkspaceDetails extends Component
}
// Usage records
if (class_exists(\Core\Tenant\Models\UsageRecord::class)) {
if (class_exists(UsageRecord::class)) {
try {
$usage = $this->workspace->usageRecords()
->with('user', 'feature')
@ -325,7 +345,7 @@ class WorkspaceDetails extends Component
#[Computed]
public function allPackages()
{
return \Core\Tenant\Models\Package::active()
return Package::active()
->ordered()
->get();
}
@ -333,7 +353,7 @@ class WorkspaceDetails extends Component
#[Computed]
public function allFeatures()
{
return \Core\Tenant\Models\Feature::active()
return Feature::active()
->orderBy('category')
->orderBy('sort_order')
->get();
@ -403,7 +423,7 @@ class WorkspaceDetails extends Component
public function resolvedEntitlements()
{
try {
return app(\Core\Tenant\Services\EntitlementService::class)
return app(EntitlementService::class)
->getUsageSummary($this->workspace);
} catch (\Exception $e) {
return collect();
@ -431,7 +451,7 @@ class WorkspaceDetails extends Component
return;
}
$package = \Core\Tenant\Models\Package::findOrFail($this->selectedPackageId);
$package = Package::findOrFail($this->selectedPackageId);
// Check if already assigned
$existing = $this->workspace->workspacePackages()
@ -446,7 +466,7 @@ class WorkspaceDetails extends Component
return;
}
\Core\Tenant\Models\WorkspacePackage::create([
WorkspacePackage::create([
'workspace_id' => $this->workspace->id,
'package_id' => $package->id,
'status' => 'active',
@ -461,7 +481,7 @@ class WorkspaceDetails extends Component
public function removePackage(int $workspacePackageId): void
{
$wp = \Core\Tenant\Models\WorkspacePackage::where('workspace_id', $this->workspace->id)
$wp = WorkspacePackage::where('workspace_id', $this->workspace->id)
->findOrFail($workspacePackageId);
$packageName = $wp->package?->name ?? 'Package';
@ -474,7 +494,7 @@ class WorkspaceDetails extends Component
public function suspendPackage(int $workspacePackageId): void
{
$wp = \Core\Tenant\Models\WorkspacePackage::where('workspace_id', $this->workspace->id)
$wp = WorkspacePackage::where('workspace_id', $this->workspace->id)
->findOrFail($workspacePackageId);
$wp->suspend();
@ -486,7 +506,7 @@ class WorkspaceDetails extends Component
public function reactivatePackage(int $workspacePackageId): void
{
$wp = \Core\Tenant\Models\WorkspacePackage::where('workspace_id', $this->workspace->id)
$wp = WorkspacePackage::where('workspace_id', $this->workspace->id)
->findOrFail($workspacePackageId);
$wp->reactivate();
@ -523,7 +543,7 @@ class WorkspaceDetails extends Component
return;
}
$feature = \Core\Tenant\Models\Feature::where('code', $this->selectedFeatureCode)->first();
$feature = Feature::where('code', $this->selectedFeatureCode)->first();
if (! $feature) {
$this->actionMessage = 'Feature not found.';
@ -534,26 +554,26 @@ class WorkspaceDetails extends Component
// Map type to boost type constant
$boostType = match ($this->entitlementType) {
'enable' => \Core\Tenant\Models\Boost::BOOST_TYPE_ENABLE,
'add_limit' => \Core\Tenant\Models\Boost::BOOST_TYPE_ADD_LIMIT,
'unlimited' => \Core\Tenant\Models\Boost::BOOST_TYPE_UNLIMITED,
default => \Core\Tenant\Models\Boost::BOOST_TYPE_ENABLE,
'enable' => Boost::BOOST_TYPE_ENABLE,
'add_limit' => Boost::BOOST_TYPE_ADD_LIMIT,
'unlimited' => Boost::BOOST_TYPE_UNLIMITED,
default => Boost::BOOST_TYPE_ENABLE,
};
$durationType = $this->entitlementDuration === 'permanent'
? \Core\Tenant\Models\Boost::DURATION_PERMANENT
: \Core\Tenant\Models\Boost::DURATION_DURATION;
? Boost::DURATION_PERMANENT
: Boost::DURATION_DURATION;
\Core\Tenant\Models\Boost::create([
Boost::create([
'workspace_id' => $this->workspace->id,
'feature_code' => $this->selectedFeatureCode,
'boost_type' => $boostType,
'duration_type' => $durationType,
'limit_value' => $this->entitlementType === 'add_limit' ? $this->entitlementLimit : null,
'consumed_quantity' => 0,
'status' => \Core\Tenant\Models\Boost::STATUS_ACTIVE,
'status' => Boost::STATUS_ACTIVE,
'starts_at' => now(),
'expires_at' => $this->entitlementExpiresAt ? \Carbon\Carbon::parse($this->entitlementExpiresAt) : null,
'expires_at' => $this->entitlementExpiresAt ? Carbon::parse($this->entitlementExpiresAt) : null,
'metadata' => ['granted_by' => auth()->id(), 'granted_at' => now()->toDateTimeString()],
]);
@ -565,7 +585,7 @@ class WorkspaceDetails extends Component
public function removeBoost(int $boostId): void
{
$boost = \Core\Tenant\Models\Boost::where('workspace_id', $this->workspace->id)
$boost = Boost::where('workspace_id', $this->workspace->id)
->findOrFail($boostId);
$featureCode = $boost->feature_code;

View file

@ -1,10 +1,20 @@
<?php
declare(strict_types=1);
namespace Core\Tenant\View\Modal\Admin;
use Core\Mod\Analytics\Models\Website;
use Core\Mod\Notify\Models\PushWebsite;
use Core\Mod\Social\Models\Account;
use Core\Mod\Trust\Models\Campaign;
use Core\Mod\Web\Models\Page;
use Core\Mod\Web\Models\Project;
use Core\Tenant\Models\User;
use Core\Tenant\Models\Workspace;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Str;
use Livewire\Attributes\Computed;
use Livewire\Component;
use Livewire\WithPagination;
@ -117,15 +127,15 @@ class WorkspaceManager extends Component
// Check each relation's model exists and has workspace_id column
$checks = [
'bioPages' => ['model' => \Core\Mod\Web\Models\Page::class, 'table' => 'pages'],
'bioProjects' => ['model' => \Core\Mod\Web\Models\Project::class, 'table' => 'page_projects'],
'socialAccounts' => ['model' => \Core\Mod\Social\Models\Account::class, 'table' => 'social_accounts'],
'analyticsSites' => ['model' => \Core\Mod\Analytics\Models\Website::class, 'table' => 'analytics_websites'],
'trustWidgets' => ['model' => \Core\Mod\Trust\Models\Campaign::class, 'table' => 'trust_campaigns'],
'notificationSites' => ['model' => \Core\Mod\Notify\Models\PushWebsite::class, 'table' => 'push_websites'],
'bioPages' => ['model' => Page::class, 'table' => 'pages'],
'bioProjects' => ['model' => Project::class, 'table' => 'page_projects'],
'socialAccounts' => ['model' => Account::class, 'table' => 'social_accounts'],
'analyticsSites' => ['model' => Website::class, 'table' => 'analytics_websites'],
'trustWidgets' => ['model' => Campaign::class, 'table' => 'trust_campaigns'],
'notificationSites' => ['model' => PushWebsite::class, 'table' => 'push_websites'],
];
$schema = \Illuminate\Support\Facades\Schema::getFacadeRoot();
$schema = Schema::getFacadeRoot();
foreach ($checks as $relation => $info) {
if (class_exists($info['model'])) {
@ -153,16 +163,16 @@ class WorkspaceManager extends Component
public function resourceTypes(): array
{
$types = [];
$schema = \Illuminate\Support\Facades\Schema::getFacadeRoot();
$schema = Schema::getFacadeRoot();
// Only include resource types for models that exist and have valid relations
$checks = [
'bio_pages' => ['model' => \Core\Mod\Web\Models\Page::class, 'table' => 'pages', 'label' => 'Bio Pages', 'relation' => 'bioPages', 'icon' => 'link'],
'bio_projects' => ['model' => \Core\Mod\Web\Models\Project::class, 'table' => 'page_projects', 'label' => 'Bio Projects', 'relation' => 'bioProjects', 'icon' => 'folder'],
'social_accounts' => ['model' => \Core\Mod\Social\Models\Account::class, 'table' => 'social_accounts', 'label' => 'Social Accounts', 'relation' => 'socialAccounts', 'icon' => 'share-nodes'],
'analytics_sites' => ['model' => \Core\Mod\Analytics\Models\Website::class, 'table' => 'analytics_websites', 'label' => 'Analytics Sites', 'relation' => 'analyticsSites', 'icon' => 'chart-line'],
'trust_widgets' => ['model' => \Core\Mod\Trust\Models\Campaign::class, 'table' => 'trust_campaigns', 'label' => 'Trust Campaigns', 'relation' => 'trustWidgets', 'icon' => 'shield-check'],
'notification_sites' => ['model' => \Core\Mod\Notify\Models\PushWebsite::class, 'table' => 'push_websites', 'label' => 'Notification Sites', 'relation' => 'notificationSites', 'icon' => 'bell'],
'bio_pages' => ['model' => Page::class, 'table' => 'pages', 'label' => 'Bio Pages', 'relation' => 'bioPages', 'icon' => 'link'],
'bio_projects' => ['model' => Project::class, 'table' => 'page_projects', 'label' => 'Bio Projects', 'relation' => 'bioProjects', 'icon' => 'folder'],
'social_accounts' => ['model' => Account::class, 'table' => 'social_accounts', 'label' => 'Social Accounts', 'relation' => 'socialAccounts', 'icon' => 'share-nodes'],
'analytics_sites' => ['model' => Website::class, 'table' => 'analytics_websites', 'label' => 'Analytics Sites', 'relation' => 'analyticsSites', 'icon' => 'chart-line'],
'trust_widgets' => ['model' => Campaign::class, 'table' => 'trust_campaigns', 'label' => 'Trust Campaigns', 'relation' => 'trustWidgets', 'icon' => 'shield-check'],
'notification_sites' => ['model' => PushWebsite::class, 'table' => 'push_websites', 'label' => 'Notification Sites', 'relation' => 'notificationSites', 'icon' => 'bell'],
];
foreach ($checks as $key => $info) {
@ -530,7 +540,7 @@ class WorkspaceManager extends Component
'icon' => 'link',
'color' => 'blue',
'fields' => ['name', 'slug'],
'model' => \Core\Mod\Web\Models\Page::class,
'model' => Page::class,
'defaults' => ['type' => 'page', 'is_enabled' => true],
],
'social_accounts' => [
@ -538,7 +548,7 @@ class WorkspaceManager extends Component
'icon' => 'share-nodes',
'color' => 'purple',
'fields' => ['name'],
'model' => \Core\Mod\Social\Models\Account::class,
'model' => Account::class,
'defaults' => ['provider' => 'manual', 'status' => 'active'],
],
'analytics_sites' => [
@ -546,7 +556,7 @@ class WorkspaceManager extends Component
'icon' => 'chart-line',
'color' => 'cyan',
'fields' => ['name', 'url'],
'model' => \Core\Mod\Analytics\Models\Website::class,
'model' => Website::class,
'defaults' => ['tracking_enabled' => true, 'is_enabled' => true],
],
'trust_widgets' => [
@ -554,7 +564,7 @@ class WorkspaceManager extends Component
'icon' => 'shield-check',
'color' => 'emerald',
'fields' => ['name'],
'model' => \Core\Mod\Trust\Models\Campaign::class,
'model' => Campaign::class,
'defaults' => ['status' => 'draft'],
],
'notification_sites' => [
@ -562,7 +572,7 @@ class WorkspaceManager extends Component
'icon' => 'bell',
'color' => 'amber',
'fields' => ['name', 'url'],
'model' => \Core\Mod\Notify\Models\PushWebsite::class,
'model' => PushWebsite::class,
'defaults' => ['status' => 'active'],
],
];
@ -615,7 +625,7 @@ class WorkspaceManager extends Component
// Add slug for bio pages
if (in_array('slug', $config['fields']) && $this->provisionSlug) {
$data['url'] = \Illuminate\Support\Str::slug($this->provisionSlug);
$data['url'] = Str::slug($this->provisionSlug);
}
// Add URL-related fields if applicable

View file

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace Core\Tenant\View\Modal\Web;
use Core\Tenant\Models\AccountDeletionRequest;

View file

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace Core\Tenant\View\Modal\Web;
use Core\Tenant\Models\AccountDeletionRequest;

View file

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace Core\Tenant\View\Modal\Web;
use Core\Mod\Content\Services\ContentRender;

View file

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace Core\Tenant\Tests\Feature;
use Core\Tenant\Models\User;

View file

@ -2,6 +2,8 @@
declare(strict_types=1);
use Core\Api\RateLimit\RateLimit;
use Core\Tenant\Controllers\EntitlementApiController;
use Core\Tenant\Models\EntitlementLog;
use Core\Tenant\Models\Feature;
use Core\Tenant\Models\Package;
@ -10,10 +12,11 @@ use Core\Tenant\Models\Workspace;
use Core\Tenant\Models\WorkspacePackage;
use Core\Tenant\Services\EntitlementService;
use Illuminate\Auth\Events\Registered;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Event;
uses(\Illuminate\Foundation\Testing\RefreshDatabase::class);
uses(RefreshDatabase::class);
beforeEach(function () {
Cache::flush();
@ -1076,8 +1079,8 @@ describe('Error Response Format', function () {
describe('Rate Limiting', function () {
it('controller has rate limit attribute', function () {
$reflection = new \ReflectionClass(\Core\Tenant\Controllers\EntitlementApiController::class);
$attributes = $reflection->getAttributes(\Core\Api\RateLimit\RateLimit::class);
$reflection = new ReflectionClass(EntitlementApiController::class);
$attributes = $reflection->getAttributes(RateLimit::class);
expect($attributes)->toHaveCount(1);

View file

@ -15,9 +15,11 @@ use Core\Tenant\Models\Workspace;
use Core\Tenant\Models\WorkspacePackage;
use Core\Tenant\Services\EntitlementResult;
use Core\Tenant\Services\EntitlementService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Cache;
uses(\Illuminate\Foundation\Testing\RefreshDatabase::class);
uses(RefreshDatabase::class);
beforeEach(function () {
// Clear cache before each test
@ -426,7 +428,7 @@ describe('EntitlementService', function () {
$summary = $this->service->getUsageSummary($this->workspace);
expect($summary)->toBeInstanceOf(\Illuminate\Support\Collection::class)
expect($summary)->toBeInstanceOf(Collection::class)
->and($summary->has('ai'))->toBeTrue()
->and($summary->has('tier'))->toBeTrue()
->and($summary->has('social'))->toBeTrue();
@ -1101,7 +1103,7 @@ describe('Namespace Entitlements', function () {
$summary = $this->service->getNamespaceUsageSummary($this->userNamespace);
expect($summary)->toBeInstanceOf(\Illuminate\Support\Collection::class)
expect($summary)->toBeInstanceOf(Collection::class)
->and($summary->has('content'))->toBeTrue()
->and($summary->has('features'))->toBeTrue();
});

View file

@ -2,16 +2,18 @@
declare(strict_types=1);
use Core\Mod\Api\Guards\AccessTokenGuard;
use Core\Tenant\Models\User;
use Core\Tenant\Models\UserToken;
use Illuminate\Http\Request;
test('can authenticate with valid bearer token', function () {
$user = User::factory()->create();
$result = $user->createToken('Test Token');
// Test the guard directly by invoking it with a mock request
$guard = new \Core\Mod\Api\Guards\AccessTokenGuard(app('auth'));
$request = \Illuminate\Http\Request::create('/test', 'GET');
$guard = new AccessTokenGuard(app('auth'));
$request = Request::create('/test', 'GET');
$request->headers->set('Authorization', "Bearer {$result['token']}");
$authenticatedUser = $guard($request);
@ -57,8 +59,8 @@ test('token last_used_at is updated on successful authentication', function () {
expect($tokenModel->last_used_at)->toBeNull();
// Test the guard directly by invoking it with a mock request
$guard = new \Core\Mod\Api\Guards\AccessTokenGuard(app('auth'));
$request = \Illuminate\Http\Request::create('/test', 'GET');
$guard = new AccessTokenGuard(app('auth'));
$request = Request::create('/test', 'GET');
$request->headers->set('Authorization', "Bearer {$result['token']}");
$guard($request);

View file

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace Core\Tenant\Tests\Feature;
use Core\Mod\Hub\View\Modal\Admin\Profile;

View file

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
use Core\Tenant\Models\Boost;
use Core\Tenant\Models\EntitlementLog;
use Core\Tenant\Models\Feature;
@ -9,10 +11,11 @@ use Core\Tenant\Models\User;
use Core\Tenant\Models\Workspace;
use Core\Tenant\Notifications\BoostExpiredNotification;
use Core\Tenant\Services\EntitlementService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Notification;
uses(\Illuminate\Foundation\Testing\RefreshDatabase::class);
uses(RefreshDatabase::class);
beforeEach(function () {
Cache::flush();

View file

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace Core\Tenant\Tests\Feature;
use Core\Mod\Hub\View\Modal\Admin\Settings;

View file

@ -2,9 +2,13 @@
declare(strict_types=1);
use Carbon\Carbon;
use Core\Tenant\Models\User;
use Core\Tenant\Models\UserTwoFactorAuth;
use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
beforeEach(function () {
Cache::flush();
@ -15,7 +19,7 @@ describe('TwoFactorAuthenticatable Trait', function () {
describe('twoFactorAuth() relationship', function () {
it('returns HasOne relationship', function () {
expect($this->user->twoFactorAuth())->toBeInstanceOf(
\Illuminate\Database\Eloquent\Relations\HasOne::class
HasOne::class
);
});
@ -315,7 +319,7 @@ describe('UserTwoFactorAuth Model', function () {
'recovery_codes' => $codes,
]);
expect($twoFactorAuth->recovery_codes)->toBeInstanceOf(\Illuminate\Support\Collection::class)
expect($twoFactorAuth->recovery_codes)->toBeInstanceOf(Collection::class)
->and($twoFactorAuth->recovery_codes->toArray())->toBe($codes);
});
@ -329,7 +333,7 @@ describe('UserTwoFactorAuth Model', function () {
'confirmed_at' => $confirmedAt,
]);
expect($twoFactorAuth->confirmed_at)->toBeInstanceOf(\Carbon\Carbon::class);
expect($twoFactorAuth->confirmed_at)->toBeInstanceOf(Carbon::class);
});
it('encrypts secret at rest', function () {
@ -348,7 +352,7 @@ describe('UserTwoFactorAuth Model', function () {
// But the raw database value should be encrypted (base64 JSON with iv, value, mac)
// Note: DB column is 'secret', not 'secret_key'
$rawValue = \Illuminate\Support\Facades\DB::table('user_two_factor_auth')
$rawValue = DB::table('user_two_factor_auth')
->where('id', $twoFactorAuth->id)
->value('secret');
@ -372,7 +376,7 @@ describe('UserTwoFactorAuth Model', function () {
expect($twoFactorAuth->recovery_codes->toArray())->toBe($codes);
// But the raw database value should be encrypted
$rawValue = \Illuminate\Support\Facades\DB::table('user_two_factor_auth')
$rawValue = DB::table('user_two_factor_auth')
->where('id', $twoFactorAuth->id)
->value('recovery_codes');

View file

@ -7,6 +7,7 @@ use Core\Tenant\Notifications\WaitlistInviteNotification;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Notification;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\Str;
use Livewire\Livewire;
use Website\Host\View\Modal\Waitlist;
@ -130,7 +131,7 @@ describe('Waitlist Entry Model', function () {
expect($entry->invite_code)->toBeNull();
$entry->update([
'invite_code' => \Illuminate\Support\Str::random(16),
'invite_code' => Str::random(16),
'invited_at' => now(),
]);

View file

@ -14,6 +14,7 @@ use Core\Tenant\Scopes\WorkspaceScope;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Http\Request;
use Illuminate\Support\Str;
use Tests\TestCase;
/**
@ -208,7 +209,7 @@ class WorkspaceSecurityTest extends TestCase
$this->expectExceptionMessage('create');
Account::create([
'uuid' => \Illuminate\Support\Str::uuid(),
'uuid' => Str::uuid(),
'provider' => 'twitter',
'provider_id' => '12345',
'name' => 'Test Account',
@ -223,7 +224,7 @@ class WorkspaceSecurityTest extends TestCase
// Should succeed because workspace_id is explicitly provided
$account = Account::create([
'uuid' => \Illuminate\Support\Str::uuid(),
'uuid' => Str::uuid(),
'workspace_id' => $this->workspace->id,
'provider' => 'twitter',
'provider_id' => '12345',
@ -240,7 +241,7 @@ class WorkspaceSecurityTest extends TestCase
request()->attributes->set('workspace_model', $this->workspace);
$account = Account::create([
'uuid' => \Illuminate\Support\Str::uuid(),
'uuid' => Str::uuid(),
'provider' => 'twitter',
'provider_id' => '12345',
'name' => 'Test Account',

View file

@ -1,12 +1,16 @@
<?php
declare(strict_types=1);
namespace Core\Tenant\Tests\Feature;
use Core\Mod\Analytics\Models\Website;
use Core\Mod\Social\Models\Account;
use Core\Tenant\Models\User;
use Core\Tenant\Models\Workspace;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Str;
use Tests\TestCase;
/**
@ -45,12 +49,12 @@ class WorkspaceTenancyTest extends TestCase
$workspace = Workspace::factory()->create();
// Test that all relationship methods exist and return correct type
$this->assertInstanceOf(\Illuminate\Database\Eloquent\Relations\HasMany::class, $workspace->socialAccounts());
$this->assertInstanceOf(\Illuminate\Database\Eloquent\Relations\HasMany::class, $workspace->socialPosts());
$this->assertInstanceOf(\Illuminate\Database\Eloquent\Relations\HasMany::class, $workspace->analyticsSites());
$this->assertInstanceOf(\Illuminate\Database\Eloquent\Relations\HasMany::class, $workspace->trustWidgets());
$this->assertInstanceOf(\Illuminate\Database\Eloquent\Relations\HasMany::class, $workspace->notificationSites());
$this->assertInstanceOf(\Illuminate\Database\Eloquent\Relations\HasMany::class, $workspace->pushCampaigns());
$this->assertInstanceOf(HasMany::class, $workspace->socialAccounts());
$this->assertInstanceOf(HasMany::class, $workspace->socialPosts());
$this->assertInstanceOf(HasMany::class, $workspace->analyticsSites());
$this->assertInstanceOf(HasMany::class, $workspace->trustWidgets());
$this->assertInstanceOf(HasMany::class, $workspace->notificationSites());
$this->assertInstanceOf(HasMany::class, $workspace->pushCampaigns());
// NOTE: bioPages relationship has been moved to Host UK app's Mod\Bio module
}
@ -114,7 +118,7 @@ class WorkspaceTenancyTest extends TestCase
// When creating a model with BelongsToWorkspace trait,
// it should auto-assign the current user's workspace
$account = Account::create([
'uuid' => \Illuminate\Support\Str::uuid(),
'uuid' => Str::uuid(),
'provider' => 'twitter',
'provider_id' => '12345',
'name' => 'Test Account',

View file

@ -4,6 +4,7 @@ declare(strict_types=1);
namespace Tests;
use Core\Tenant\Boot;
use Orchestra\Testbench\TestCase as BaseTestCase;
abstract class TestCase extends BaseTestCase
@ -11,7 +12,7 @@ abstract class TestCase extends BaseTestCase
protected function getPackageProviders($app): array
{
return [
\Core\Tenant\Boot::class,
Boot::class,
];
}
}