Commit fd08eae7 authored by Mahmoud Aglan's avatar Mahmoud Aglan

init

parents
.git
.github
.vscode
.idea
node_modules
vendor
storage/logs/*
storage/framework/cache/*
storage/framework/sessions/*
storage/framework/views/*
tests
.env
.env.local
.env.testing
docs/
*.md
!CLAUDE.md
docker-compose.yml
docker-compose.override.yml
# Local Development Override
# These values override the hardcoded production defaults in config files.
# Copy this to .env for local development.
APP_NAME="UGC Heaven"
APP_ENV=local
APP_DEBUG=true
APP_URL=http://localhost:8000
APP_KEY=base64:eaoG4Gp567JOfBeAlQ0qCOknVsQ531ZJATEMFN9gj6w=
DB_CONNECTION=pgsql
DB_HOST=127.0.0.1
DB_PORT=5432
DB_DATABASE=ugc_heaven
DB_USERNAME=postgres
DB_PASSWORD=postgres
REDIS_HOST=127.0.0.1
REDIS_PORT=6379
SESSION_DRIVER=redis
CACHE_STORE=redis
QUEUE_CONNECTION=sync
LOG_CHANNEL=daily
MAIL_MAILER=log
PEERTUBE_URL=https://ugcvideoserver.caprover.al-arcade.com
PEERTUBE_CLIENT_ID=b148jgk5ffimzszm8cqa4fut2o5u2jf7
PEERTUBE_CLIENT_SECRET=sRXMboZI4941vn1cs3GSJIhtyCAVli32
PEERTUBE_ADMIN_USERNAME=root
PEERTUBE_ADMIN_PASSWORD=Alarcade123#
/node_modules
/public/build
/public/hot
/public/storage
/storage/*.key
/vendor
.env
.env.backup
.env.production
.env.local
.env.testing
.phpactor.json
.phpunit.result.cache
Homestead.json
Homestead.yaml
auth.json
npm-debug.log
yarn-error.log
/.fleet
/.idea
/.vscode
/.claude/memory
database/database.sqlite
This diff is collapsed.
# ============================================
# UGC Heaven — Production Dockerfile
# CapRover GitLab deploy: push → build → live
# ============================================
# Stage 1: Composer dependencies
FROM composer:2 AS composer
WORKDIR /app
COPY composer.json composer.lock ./
RUN composer install --no-dev --no-scripts --no-autoloader --prefer-dist
COPY . .
RUN composer dump-autoload --optimize --no-dev
# Stage 2: Frontend assets
FROM node:20-alpine AS frontend
WORKDIR /app
COPY package.json package-lock.json* ./
RUN npm ci --production=false
COPY . .
RUN npm run build
# Stage 3: Production image
FROM php:8.2-fpm-alpine
# Install system dependencies
RUN apk add --no-cache \
nginx \
supervisor \
curl \
zip \
unzip \
libpq-dev \
libzip-dev \
icu-dev \
freetype-dev \
libjpeg-turbo-dev \
libpng-dev \
libwebp-dev \
oniguruma-dev \
&& rm -rf /var/cache/apk/*
# Install PHP extensions
RUN docker-php-ext-configure gd --with-freetype --with-jpeg --with-webp \
&& docker-php-ext-install \
pdo \
pdo_pgsql \
pgsql \
zip \
intl \
gd \
mbstring \
opcache \
pcntl \
bcmath
# Install Redis extension
RUN apk add --no-cache --virtual .build-deps $PHPIZE_DEPS \
&& pecl install redis \
&& docker-php-ext-enable redis \
&& apk del .build-deps
# Configure PHP for production
RUN mv "$PHP_INI_DIR/php.ini-production" "$PHP_INI_DIR/php.ini"
COPY docker/php.ini "$PHP_INI_DIR/conf.d/99-ugcheaven.ini"
# Configure OPcache
RUN echo "opcache.enable=1" >> "$PHP_INI_DIR/conf.d/99-ugcheaven.ini" \
&& echo "opcache.memory_consumption=256" >> "$PHP_INI_DIR/conf.d/99-ugcheaven.ini" \
&& echo "opcache.max_accelerated_files=20000" >> "$PHP_INI_DIR/conf.d/99-ugcheaven.ini" \
&& echo "opcache.validate_timestamps=0" >> "$PHP_INI_DIR/conf.d/99-ugcheaven.ini"
# Setup working directory
WORKDIR /var/www/html
# Copy application
COPY --from=composer /app/vendor ./vendor
COPY --from=frontend /app/public/build ./public/build
COPY . .
# Set permissions
RUN chown -R www-data:www-data /var/www/html \
&& chmod -R 755 /var/www/html/storage \
&& chmod -R 755 /var/www/html/bootstrap/cache
# Copy config files
COPY docker/nginx.conf /etc/nginx/http.d/default.conf
COPY docker/supervisord.conf /etc/supervisord.conf
COPY docker/entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
# Setup cron
RUN echo "* * * * * cd /var/www/html && php artisan schedule:run >> /dev/null 2>&1" > /etc/crontabs/www-data
# Expose port 80 (CapRover routes to this)
EXPOSE 80
# Health check
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
CMD curl -f http://localhost/api/health || exit 1
ENTRYPOINT ["/entrypoint.sh"]
<?php
namespace App\Http\Controllers;
abstract class Controller
{
//
}
<?php
namespace App\Models;
use App\Shared\Traits\HasUuid;
use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
class User extends Authenticatable implements MustVerifyEmail
{
use HasFactory, Notifiable, SoftDeletes, HasUuid;
protected $fillable = [
'uuid',
'name',
'email',
'password',
'role',
'status',
'email_verified_at',
'phone',
'avatar',
'locale',
'last_login_at',
'last_login_ip',
];
protected $hidden = [
'id',
'password',
'remember_token',
];
protected function casts(): array
{
return [
'email_verified_at' => 'datetime',
'last_login_at' => 'datetime',
'password' => 'hashed',
];
}
public function getRouteKeyName(): string
{
return 'uuid';
}
public function creatorProfile()
{
return $this->hasOne(\App\Modules\Creators\Models\CreatorProfile::class);
}
public function companyProfile()
{
return $this->hasOne(\App\Modules\Companies\Models\CompanyProfile::class);
}
public function isAdmin(): bool
{
return $this->role === 'admin';
}
public function isCreator(): bool
{
return $this->role === 'creator';
}
public function isCompany(): bool
{
return $this->role === 'company';
}
public function isActive(): bool
{
return $this->status === 'active';
}
}
<?php
namespace App\Providers;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\URL;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
public function register(): void
{
//
}
public function boot(): void
{
Model::preventLazyLoading(!app()->isProduction());
Model::unguard(false);
if (app()->isProduction()) {
URL::forceScheme('https');
}
}
}
<?php
namespace App\Providers;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Route;
use Illuminate\Support\ServiceProvider;
class ModuleServiceProvider extends ServiceProvider
{
public function register(): void
{
$this->registerModuleProviders();
}
public function boot(): void
{
$this->loadModuleRoutes();
$this->loadModuleMigrations();
}
private function registerModuleProviders(): void
{
$modulesPath = app_path('Modules');
if (!File::isDirectory($modulesPath)) {
return;
}
foreach (File::directories($modulesPath) as $modulePath) {
$moduleName = basename($modulePath);
$providerClass = "App\\Modules\\{$moduleName}\\Providers\\{$moduleName}ServiceProvider";
if (class_exists($providerClass)) {
$this->app->register($providerClass);
}
}
}
private function loadModuleRoutes(): void
{
$modulesPath = app_path('Modules');
if (!File::isDirectory($modulesPath)) {
return;
}
foreach (File::directories($modulesPath) as $modulePath) {
$routesPath = $modulePath . '/Routes';
if (File::isDirectory($routesPath)) {
$webRoute = $routesPath . '/web.php';
$apiRoute = $routesPath . '/api.php';
if (File::exists($webRoute)) {
Route::middleware('web')->group($webRoute);
}
if (File::exists($apiRoute)) {
Route::middleware('api')->prefix('api')->group($apiRoute);
}
}
}
}
private function loadModuleMigrations(): void
{
$modulesPath = app_path('Modules');
if (!File::isDirectory($modulesPath)) {
return;
}
foreach (File::directories($modulesPath) as $modulePath) {
$migrationsPath = $modulePath . '/Database/Migrations';
if (File::isDirectory($migrationsPath)) {
$this->loadMigrationsFrom($migrationsPath);
}
}
}
}
<?php
namespace App\Shared\Exceptions;
use Exception;
class InvalidStatusTransitionException extends Exception
{
}
<?php
namespace App\Shared\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class EnsureCompanyApproved
{
public function handle(Request $request, Closure $next): Response
{
$user = $request->user();
if (!$user || $user->role !== 'company') {
abort(403);
}
if (!$user->companyProfile || $user->companyProfile->status !== 'approved') {
abort(403, __('company.not_approved'));
}
return $next($request);
}
}
<?php
namespace App\Shared\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class EnsureProfileComplete
{
public function handle(Request $request, Closure $next, int $minPercent = 50): Response
{
$user = $request->user();
if (!$user) {
abort(401);
}
$profile = match ($user->role) {
'creator' => $user->creatorProfile,
'company' => $user->companyProfile,
default => null,
};
if ($profile && $profile->completion_percentage < $minPercent) {
if ($request->expectsJson()) {
return response()->json([
'success' => false,
'message' => __('profile.incomplete'),
'error_code' => 'PROFILE_INCOMPLETE',
], 403);
}
return redirect()->route('profile.edit');
}
return $next($request);
}
}
<?php
namespace App\Shared\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class EnsureRole
{
public function handle(Request $request, Closure $next, string ...$roles): Response
{
if (!$request->user() || !in_array($request->user()->role, $roles, true)) {
abort(403, __('auth.unauthorized_role'));
}
return $next($request);
}
}
<?php
namespace App\Shared\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class SetLocale
{
public function handle(Request $request, Closure $next): Response
{
$locale = session('locale', config('app.locale', 'en'));
if (in_array($locale, config('app.supported_locales', ['en', 'ar']))) {
app()->setLocale($locale);
}
return $next($request);
}
}
<?php
namespace App\Shared\Traits;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Resources\Json\JsonResource;
use Illuminate\Http\Resources\Json\ResourceCollection;
trait ApiResponse
{
protected function success(
mixed $data = null,
string $message = '',
int $code = 200,
array $meta = []
): JsonResponse {
$response = [
'success' => true,
'data' => $data,
'message' => $message,
];
if (!empty($meta)) {
$response['meta'] = $meta;
}
return response()->json($response, $code);
}
protected function successResource(
JsonResource $resource,
string $message = '',
int $code = 200
): JsonResponse {
return response()->json([
'success' => true,
'data' => $resource,
'message' => $message,
], $code);
}
protected function successCollection(
ResourceCollection $collection,
string $message = ''
): JsonResponse {
$paginated = $collection->response()->getData(true);
return response()->json([
'success' => true,
'data' => $paginated['data'],
'message' => $message,
'meta' => $paginated['meta'] ?? [],
]);
}
protected function error(
string $message,
int $code = 400,
?string $errorCode = null,
array $errors = []
): JsonResponse {
$response = [
'success' => false,
'message' => $message,
];
if ($errorCode) {
$response['error_code'] = $errorCode;
}
if (!empty($errors)) {
$response['errors'] = $errors;
}
return response()->json($response, $code);
}
}
<?php
namespace App\Shared\Traits;
use App\Shared\Exceptions\InvalidStatusTransitionException;
trait HasStatusTransitions
{
public function transitionTo(string $newStatus): void
{
if (!static::canTransition($this->status, $newStatus)) {
throw new InvalidStatusTransitionException(
"Cannot transition from '{$this->status}' to '{$newStatus}'"
);
}
$this->status = $newStatus;
$this->save();
}
public static function canTransition(string $from, string $to): bool
{
$allowed = static::allowedTransitions();
return isset($allowed[$from]) && in_array($to, $allowed[$from], true);
}
abstract protected static function allowedTransitions(): array;
}
<?php
namespace App\Shared\Traits;
use Illuminate\Support\Str;
trait HasUuid
{
protected static function bootHasUuid(): void
{
static::creating(function ($model) {
if (empty($model->uuid)) {
$model->uuid = (string) Str::uuid();
}
});
}
}
#!/usr/bin/env php
<?php
use Symfony\Component\Console\Input\ArgvInput;
define('LARAVEL_START', microtime(true));
// Register the Composer autoloader...
require __DIR__.'/vendor/autoload.php';
// Bootstrap Laravel and handle the command...
$status = (require_once __DIR__.'/bootstrap/app.php')
->handleCommand(new ArgvInput);
exit($status);
<?php
use Illuminate\Foundation\Application;
use Illuminate\Foundation\Configuration\Exceptions;
use Illuminate\Foundation\Configuration\Middleware;
return Application::configure(basePath: dirname(__DIR__))
->withRouting(
web: __DIR__.'/../routes/web.php',
api: __DIR__.'/../routes/api.php',
commands: __DIR__.'/../routes/console.php',
health: '/up',
)
->withMiddleware(function (Middleware $middleware) {
$middleware->web(append: [
\App\Shared\Middleware\SetLocale::class,
]);
$middleware->alias([
'role' => \App\Shared\Middleware\EnsureRole::class,
'company.approved' => \App\Shared\Middleware\EnsureCompanyApproved::class,
'profile.complete' => \App\Shared\Middleware\EnsureProfileComplete::class,
]);
})
->withExceptions(function (Exceptions $exceptions) {
//
})->create();
<?php
return [
App\Providers\AppServiceProvider::class,
App\Providers\ModuleServiceProvider::class,
];
{
"schemaVersion": 2,
"dockerfilePath": "./Dockerfile"
}
{
"$schema": "https://getcomposer.org/schema.json",
"name": "ugc-heaven/platform",
"type": "project",
"description": "UGC Heaven - The Ultimate UGC Marketplace Platform",
"keywords": ["ugc", "marketplace", "creators", "brands"],
"license": "proprietary",
"require": {
"php": "^8.2",
"blade-ui-kit/blade-icons": "^1.6",
"laravel/framework": "^11.31",
"laravel/tinker": "^2.9",
"livewire/livewire": "^3.0"
},
"require-dev": {
"fakerphp/faker": "^1.23",
"laravel/pail": "^1.1",
"laravel/pint": "^1.13",
"mockery/mockery": "^1.6",
"nunomaduro/collision": "^8.1",
"phpunit/phpunit": "^11.0.1"
},
"autoload": {
"psr-4": {
"App\\": "app/",
"Database\\Factories\\": "database/factories/",
"Database\\Seeders\\": "database/seeders/"
}
},
"autoload-dev": {
"psr-4": {
"Tests\\": "tests/"
}
},
"scripts": {
"post-autoload-dump": [
"Illuminate\\Foundation\\ComposerScripts::postAutoloadDump",
"@php artisan package:discover --ansi"
],
"post-update-cmd": [
"@php artisan vendor:publish --tag=laravel-assets --ansi --force"
],
"post-root-package-install": [
"@php -r \"file_exists('.env') || copy('.env.example', '.env');\""
]
},
"extra": {
"laravel": {
"dont-discover": []
}
},
"config": {
"optimize-autoloader": true,
"preferred-install": "dist",
"sort-packages": true,
"allow-plugins": {
"pestphp/pest-plugin": true,
"php-http/discovery": true
}
},
"minimum-stability": "stable",
"prefer-stable": true
}
This diff is collapsed.
<?php
return [
'name' => env('APP_NAME', 'UGC Heaven'),
'env' => env('APP_ENV', 'production'),
'debug' => (bool) env('APP_DEBUG', false),
'url' => env('APP_URL', 'https://ugcheaven.caprover.al-arcade.com'),
'timezone' => 'UTC',
'locale' => env('APP_LOCALE', 'en'),
'fallback_locale' => 'en',
'faker_locale' => 'en_US',
'supported_locales' => ['en', 'ar'],
'cipher' => 'AES-256-CBC',
'key' => env('APP_KEY', 'base64:eaoG4Gp567JOfBeAlQ0qCOknVsQ531ZJATEMFN9gj6w='),
'previous_keys' => [],
'maintenance' => [
'driver' => 'file',
'store' => 'database',
],
];
<?php
return [
'defaults' => [
'guard' => 'web',
'passwords' => 'users',
],
'guards' => [
'web' => [
'driver' => 'session',
'provider' => 'users',
],
],
'providers' => [
'users' => [
'driver' => 'eloquent',
'model' => App\Models\User::class,
],
],
'passwords' => [
'users' => [
'provider' => 'users',
'table' => 'password_reset_tokens',
'expire' => 60,
'throttle' => 60,
],
],
'password_timeout' => 10800,
];
<?php
return [
'default' => env('CACHE_STORE', 'redis'),
'stores' => [
'array' => [
'driver' => 'array',
'serialize' => false,
],
'file' => [
'driver' => 'file',
'path' => storage_path('framework/cache/data'),
'lock_path' => storage_path('framework/cache/data'),
],
'redis' => [
'driver' => 'redis',
'connection' => 'cache',
'lock_connection' => 'default',
],
],
'prefix' => 'ugc_heaven_cache_',
];
<?php
return [
'default' => env('DB_CONNECTION', 'pgsql'),
'connections' => [
'sqlite' => [
'driver' => 'sqlite',
'database' => env('DB_DATABASE', database_path('database.sqlite')),
'prefix' => '',
'foreign_key_constraints' => true,
],
'pgsql' => [
'driver' => 'pgsql',
'host' => env('DB_HOST', 'srv-captain--ugc-heaven-db'),
'port' => env('DB_PORT', '5432'),
'database' => env('DB_DATABASE', 'ugc_heaven'),
'username' => env('DB_USERNAME', 'ugcadmin'),
'password' => env('DB_PASSWORD', 'UgcH3aven2024!'),
'charset' => 'utf8',
'prefix' => '',
'prefix_indexes' => true,
'search_path' => 'public',
'sslmode' => 'prefer',
],
],
'migrations' => [
'table' => 'migrations',
'update_date_on_publish' => true,
],
'redis' => [
'client' => 'phpredis',
'options' => [
'cluster' => 'redis',
'prefix' => 'ugc_heaven_',
],
'default' => [
'host' => env('REDIS_HOST', 'srv-captain--ugc-heaven-redis'),
'password' => env('REDIS_PASSWORD', null),
'port' => env('REDIS_PORT', '6379'),
'database' => '0',
],
'cache' => [
'host' => env('REDIS_HOST', 'srv-captain--ugc-heaven-redis'),
'password' => env('REDIS_PASSWORD', null),
'port' => env('REDIS_PORT', '6379'),
'database' => '1',
],
'session' => [
'host' => env('REDIS_HOST', 'srv-captain--ugc-heaven-redis'),
'password' => env('REDIS_PASSWORD', null),
'port' => env('REDIS_PORT', '6379'),
'database' => '2',
],
],
];
<?php
return [
'default' => env('FILESYSTEM_DISK', 'local'),
'disks' => [
'local' => [
'driver' => 'local',
'root' => storage_path('app/private'),
'serve' => true,
'throw' => false,
],
'public' => [
'driver' => 'local',
'root' => storage_path('app/public'),
'url' => '/storage',
'visibility' => 'public',
'throw' => false,
],
],
'links' => [
public_path('storage') => storage_path('app/public'),
],
];
<?php
use Monolog\Handler\NullHandler;
use Monolog\Handler\StreamHandler;
use Monolog\Processor\PsrLogMessageProcessor;
return [
'default' => env('LOG_CHANNEL', 'daily'),
'deprecations' => [
'channel' => 'null',
'trace' => false,
],
'channels' => [
'stack' => [
'driver' => 'stack',
'channels' => ['daily'],
'ignore_exceptions' => false,
],
'single' => [
'driver' => 'single',
'path' => storage_path('logs/laravel.log'),
'level' => 'info',
'replace_placeholders' => true,
],
'daily' => [
'driver' => 'daily',
'path' => storage_path('logs/laravel.log'),
'level' => 'info',
'days' => 14,
'replace_placeholders' => true,
],
'stderr' => [
'driver' => 'monolog',
'level' => 'debug',
'handler' => StreamHandler::class,
'with' => [
'stream' => 'php://stderr',
],
'processors' => [PsrLogMessageProcessor::class],
],
'null' => [
'driver' => 'monolog',
'handler' => NullHandler::class,
],
'emergency' => [
'path' => storage_path('logs/laravel.log'),
],
],
];
<?php
return [
'default' => env('MAIL_MAILER', 'smtp'),
'mailers' => [
'smtp' => [
'transport' => 'smtp',
'host' => env('MAIL_HOST', 'srv-captain--poste-io'),
'port' => env('MAIL_PORT', 587),
'encryption' => env('MAIL_ENCRYPTION', 'tls'),
'username' => env('MAIL_USERNAME', 'noreply@al-arcade.com'),
'password' => env('MAIL_PASSWORD', 'Alarcade123#'),
'timeout' => null,
'local_domain' => 'ugcheaven.caprover.al-arcade.com',
],
'log' => [
'transport' => 'log',
'channel' => null,
],
'array' => [
'transport' => 'array',
],
'failover' => [
'transport' => 'failover',
'mailers' => [
'smtp',
'log',
],
],
],
'from' => [
'address' => env('MAIL_FROM_ADDRESS', 'noreply@al-arcade.com'),
'name' => env('MAIL_FROM_NAME', 'UGC Heaven'),
],
];
<?php
return [
'default' => env('QUEUE_CONNECTION', 'redis'),
'connections' => [
'sync' => [
'driver' => 'sync',
],
'redis' => [
'driver' => 'redis',
'connection' => 'default',
'queue' => 'default',
'retry_after' => 90,
'block_for' => null,
'after_commit' => false,
],
],
'batching' => [
'database' => 'pgsql',
'table' => 'job_batches',
],
'failed' => [
'driver' => 'database-uuids',
'database' => 'pgsql',
'table' => 'failed_jobs',
],
];
<?php
return [
'peertube' => [
'url' => env('PEERTUBE_URL', 'https://ugcvideoserver.caprover.al-arcade.com'),
'client_id' => env('PEERTUBE_CLIENT_ID', 'b148jgk5ffimzszm8cqa4fut2o5u2jf7'),
'client_secret' => env('PEERTUBE_CLIENT_SECRET', 'sRXMboZI4941vn1cs3GSJIhtyCAVli32'),
'admin_username' => env('PEERTUBE_ADMIN_USERNAME', 'root'),
'admin_password' => env('PEERTUBE_ADMIN_PASSWORD', 'Alarcade123#'),
],
];
<?php
return [
'driver' => env('SESSION_DRIVER', 'redis'),
'lifetime' => 120,
'expire_on_close' => false,
'encrypt' => false,
'files' => storage_path('framework/sessions'),
'connection' => 'session',
'table' => 'sessions',
'store' => null,
'lottery' => [2, 100],
'cookie' => 'ugc_heaven_session',
'path' => '/',
'domain' => env('SESSION_DOMAIN', null),
'secure' => true,
'http_only' => true,
'same_site' => 'lax',
'partitioned' => false,
];
<?php
namespace Database\Factories;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Str;
/**
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\User>
*/
class UserFactory extends Factory
{
/**
* The current password being used by the factory.
*/
protected static ?string $password;
/**
* Define the model's default state.
*
* @return array<string, mixed>
*/
public function definition(): array
{
return [
'name' => fake()->name(),
'email' => fake()->unique()->safeEmail(),
'email_verified_at' => now(),
'password' => static::$password ??= Hash::make('password'),
'remember_token' => Str::random(10),
];
}
/**
* Indicate that the model's email address should be unverified.
*/
public function unverified(): static
{
return $this->state(fn (array $attributes) => [
'email_verified_at' => null,
]);
}
}
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
if (DB::connection()->getDriverName() === 'pgsql') {
DB::statement('CREATE EXTENSION IF NOT EXISTS "uuid-ossp"');
DB::statement('CREATE EXTENSION IF NOT EXISTS "pg_trgm"');
DB::statement('CREATE EXTENSION IF NOT EXISTS "pgcrypto"');
}
Schema::create('users', function (Blueprint $table) {
$table->id();
$table->uuid('uuid')->unique();
$table->string('name');
$table->string('email')->unique();
$table->timestamp('email_verified_at')->nullable();
$table->string('password');
$table->string('role', 20)->default('creator');
$table->string('status', 20)->default('active');
$table->string('phone', 30)->nullable();
$table->string('avatar')->nullable();
$table->string('locale', 5)->default('en');
$table->timestamp('last_login_at')->nullable();
$table->string('last_login_ip', 45)->nullable();
$table->rememberToken();
$table->timestamps();
$table->softDeletes();
$table->index('role');
$table->index('status');
$table->index('created_at');
});
if (DB::connection()->getDriverName() === 'pgsql') {
DB::statement("ALTER TABLE users ADD CONSTRAINT users_role_check CHECK (role IN ('admin', 'creator', 'company'))");
DB::statement("ALTER TABLE users ADD CONSTRAINT users_status_check CHECK (status IN ('active', 'suspended', 'banned', 'deactivated'))");
}
Schema::create('password_reset_tokens', function (Blueprint $table) {
$table->string('email')->primary();
$table->string('token');
$table->timestamp('created_at')->nullable();
});
}
public function down(): void
{
Schema::dropIfExists('password_reset_tokens');
Schema::dropIfExists('users');
}
};
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('jobs', function (Blueprint $table) {
$table->id();
$table->string('queue')->index();
$table->longText('payload');
$table->unsignedTinyInteger('attempts');
$table->unsignedInteger('reserved_at')->nullable();
$table->unsignedInteger('available_at');
$table->unsignedInteger('created_at');
});
Schema::create('job_batches', function (Blueprint $table) {
$table->string('id')->primary();
$table->string('name');
$table->integer('total_jobs');
$table->integer('pending_jobs');
$table->integer('failed_jobs');
$table->longText('failed_job_ids');
$table->mediumText('options')->nullable();
$table->integer('cancelled_at')->nullable();
$table->integer('created_at');
$table->integer('finished_at')->nullable();
});
Schema::create('failed_jobs', function (Blueprint $table) {
$table->id();
$table->string('uuid')->unique();
$table->text('connection');
$table->text('queue');
$table->longText('payload');
$table->longText('exception');
$table->timestamp('failed_at')->useCurrent();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('jobs');
Schema::dropIfExists('job_batches');
Schema::dropIfExists('failed_jobs');
}
};
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('platform_settings', function (Blueprint $table) {
$table->id();
$table->string('group', 50)->index();
$table->string('key', 100);
$table->text('value')->nullable();
$table->string('type', 20)->default('string');
$table->timestamps();
$table->unique(['group', 'key']);
});
Schema::create('activity_logs', function (Blueprint $table) {
$table->id();
$table->uuid('uuid')->unique();
$table->foreignId('user_id')->nullable()->constrained()->nullOnDelete();
$table->string('action', 100);
$table->string('subject_type')->nullable();
$table->unsignedBigInteger('subject_id')->nullable();
$table->jsonb('properties')->nullable();
$table->string('ip_address', 45)->nullable();
$table->string('user_agent')->nullable();
$table->timestamp('created_at')->useCurrent();
$table->index(['subject_type', 'subject_id']);
$table->index('action');
$table->index('created_at');
});
Schema::create('notifications', function (Blueprint $table) {
$table->uuid('id')->primary();
$table->string('type');
$table->morphs('notifiable');
$table->text('data');
$table->timestamp('read_at')->nullable();
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('notifications');
Schema::dropIfExists('activity_logs');
Schema::dropIfExists('platform_settings');
}
};
<?php
namespace Database\Seeders;
use App\Models\User;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Str;
class AdminSeeder extends Seeder
{
public function run(): void
{
User::firstOrCreate(
['email' => 'admin@al-arcade.com'],
[
'uuid' => Str::uuid()->toString(),
'name' => 'Super Admin',
'password' => Hash::make('Alarcade123#'),
'role' => 'admin',
'status' => 'active',
'email_verified_at' => now(),
'locale' => 'en',
]
);
}
}
<?php
namespace Database\Seeders;
use Illuminate\Database\Seeder;
class DatabaseSeeder extends Seeder
{
public function run(): void
{
$this->call([
PlatformSettingsSeeder::class,
AdminSeeder::class,
]);
}
}
<?php
namespace Database\Seeders;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\DB;
class PlatformSettingsSeeder extends Seeder
{
public function run(): void
{
$settings = [
['group' => 'branding', 'key' => 'platform_name', 'value' => 'UGC Heaven', 'type' => 'string'],
['group' => 'branding', 'key' => 'tagline', 'value' => 'Where Brands Meet Creators', 'type' => 'string'],
['group' => 'branding', 'key' => 'logo_url', 'value' => '/images/logo.svg', 'type' => 'string'],
['group' => 'branding', 'key' => 'favicon_url', 'value' => '/images/favicon.ico', 'type' => 'string'],
['group' => 'theme', 'key' => 'color_primary', 'value' => '#6366F1', 'type' => 'string'],
['group' => 'theme', 'key' => 'color_primary_hover', 'value' => '#4F46E5', 'type' => 'string'],
['group' => 'theme', 'key' => 'color_secondary', 'value' => '#06B6D4', 'type' => 'string'],
['group' => 'theme', 'key' => 'color_accent', 'value' => '#F59E0B', 'type' => 'string'],
['group' => 'theme', 'key' => 'gradient_primary', 'value' => 'linear-gradient(135deg, #6366F1 0%, #8B5CF6 100%)', 'type' => 'string'],
['group' => 'theme', 'key' => 'gradient_secondary', 'value' => 'linear-gradient(135deg, #06B6D4 0%, #0EA5E9 100%)', 'type' => 'string'],
['group' => 'theme', 'key' => 'dark_mode_enabled', 'value' => '1', 'type' => 'boolean'],
['group' => 'typography', 'key' => 'font_heading_en', 'value' => 'Plus Jakarta Sans', 'type' => 'string'],
['group' => 'typography', 'key' => 'font_body_en', 'value' => 'Inter', 'type' => 'string'],
['group' => 'typography', 'key' => 'font_heading_ar', 'value' => 'Tajawal', 'type' => 'string'],
['group' => 'typography', 'key' => 'font_body_ar', 'value' => 'Tajawal', 'type' => 'string'],
['group' => 'features', 'key' => 'registration_enabled', 'value' => '1', 'type' => 'boolean'],
['group' => 'features', 'key' => 'creator_registration_enabled', 'value' => '1', 'type' => 'boolean'],
['group' => 'features', 'key' => 'company_registration_enabled', 'value' => '1', 'type' => 'boolean'],
['group' => 'features', 'key' => 'portfolio_enabled', 'value' => '1', 'type' => 'boolean'],
['group' => 'features', 'key' => 'messaging_enabled', 'value' => '1', 'type' => 'boolean'],
['group' => 'features', 'key' => 'reviews_enabled', 'value' => '1', 'type' => 'boolean'],
['group' => 'limits', 'key' => 'max_upload_size_mb', 'value' => '2048', 'type' => 'integer'],
['group' => 'limits', 'key' => 'max_portfolio_videos', 'value' => '50', 'type' => 'integer'],
['group' => 'limits', 'key' => 'max_campaign_applications', 'value' => '500', 'type' => 'integer'],
['group' => 'seo', 'key' => 'meta_title', 'value' => 'UGC Heaven - Where Brands Meet Creators', 'type' => 'string'],
['group' => 'seo', 'key' => 'meta_description', 'value' => 'The ultimate UGC marketplace connecting companies with talented content creators.', 'type' => 'string'],
];
foreach ($settings as $setting) {
DB::table('platform_settings')->updateOrInsert(
['group' => $setting['group'], 'key' => $setting['key']],
array_merge($setting, ['created_at' => now(), 'updated_at' => now()])
);
}
}
}
#!/bin/sh
set -e
echo "=== UGC Heaven: Starting deployment ==="
cd /var/www/html
# Ensure storage directories exist
mkdir -p storage/framework/{cache,sessions,views}
mkdir -p storage/app/public
mkdir -p storage/logs
chown -R www-data:www-data storage bootstrap/cache
# Create storage symlink
php artisan storage:link --force 2>/dev/null || true
# Run migrations (non-interactive)
echo "=== Running migrations ==="
php artisan migrate --force
# Cache configuration for performance
echo "=== Caching config ==="
php artisan config:cache
php artisan route:cache
php artisan view:cache
php artisan event:cache
# Seed default data (only if needed — uses --no-interaction)
echo "=== Checking seed data ==="
php artisan db:seed --class=PlatformSettingsSeeder --force 2>/dev/null || true
php artisan db:seed --class=AdminSeeder --force 2>/dev/null || true
echo "=== UGC Heaven: Ready ==="
# Start supervisor (manages PHP-FPM, Nginx, Queue, Cron)
exec supervisord -c /etc/supervisord.conf
server {
listen 80;
server_name _;
root /var/www/html/public;
index index.php;
charset utf-8;
# Security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
# Max upload size (for video uploads)
client_max_body_size 2G;
client_body_timeout 600s;
# Gzip compression
gzip on;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml text/javascript image/svg+xml;
gzip_min_length 1000;
gzip_vary on;
# Static file caching
location ~* \.(css|js|jpg|jpeg|png|gif|ico|svg|webp|woff|woff2|ttf|eot)$ {
expires 30d;
add_header Cache-Control "public, immutable";
try_files $uri =404;
}
# Main routing
location / {
try_files $uri $uri/ /index.php?$query_string;
}
# PHP-FPM
location ~ \.php$ {
fastcgi_pass 127.0.0.1:9000;
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
include fastcgi_params;
fastcgi_read_timeout 600;
fastcgi_buffer_size 32k;
fastcgi_buffers 16 16k;
}
# Deny access to hidden files
location ~ /\. {
deny all;
}
# Deny access to sensitive files
location ~ /(composer\.|artisan|package\.json|webpack\.mix\.js) {
deny all;
}
# Health check (no logging)
location = /api/health {
access_log off;
try_files $uri /index.php?$query_string;
}
}
; UGC Heaven PHP Configuration
; Upload limits (2GB for video)
upload_max_filesize = 2G
post_max_size = 2G
max_file_uploads = 20
; Memory and execution
memory_limit = 512M
max_execution_time = 600
max_input_time = 600
; Session
session.save_handler = redis
session.save_path = "tcp://srv-captain--ugc-heaven-redis:6379"
; Timezone
date.timezone = UTC
; Error handling (production)
display_errors = Off
display_startup_errors = Off
log_errors = On
error_log = /var/www/html/storage/logs/php-errors.log
; Security
expose_php = Off
allow_url_fopen = On
allow_url_include = Off
[supervisord]
nodaemon=true
logfile=/var/log/supervisord.log
pidfile=/var/run/supervisord.pid
[program:php-fpm]
command=php-fpm -F
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
autorestart=true
priority=10
[program:nginx]
command=nginx -g "daemon off;"
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
autorestart=true
priority=20
[program:queue-worker]
command=php /var/www/html/artisan queue:work redis --sleep=3 --tries=3 --max-time=3600 --memory=256
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
autorestart=true
priority=30
numprocs=2
process_name=%(program_name)s_%(process_num)02d
[program:cron]
command=crond -f -l 8
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
autorestart=true
priority=40
# 01 — System Overview
## Platform Identity
**UGC Heaven** is a professional UGC (User-Generated Content) marketplace connecting brands/companies with content creators. MENA-first, Arabic + English from day one. Trust and professionalism are the core brand values.
---
## Tech Stack
| Layer | Technology |
|-------|-----------|
| Backend | PHP 8.2+ (Laravel 11) |
| Database | PostgreSQL 16 |
| Video Hosting | PeerTube 8.2.1 (self-hosted) |
| Cache/Queue | Redis |
| Search | PostgreSQL Full-Text + pg_trgm |
| File Storage | S3-compatible (CapRover volume or MinIO) |
| Deployment | CapRover (Docker Swarm) |
| Real-time | Laravel Reverb (WebSockets) |
| Email | Laravel Mail (SMTP) |
| Auth | Laravel Sanctum (API tokens) + session-based (web) |
---
## Three User Types (Single Codebase)
### 1. SuperAdmin
- Full platform control
- Sees everything, manages everything
- Cannot be created via registration (seeded only)
- Has dedicated `/admin` panel
### 2. Company
- Registers as a company/brand
- Posts campaigns, discovers creators, manages projects
- Can have multiple team members (future)
- Has dedicated `/company` dashboard
### 3. Creator
- Registers as a content creator
- Builds portfolio, applies to campaigns, delivers content
- Has dedicated `/creator` dashboard
---
## Core Architecture Principles
### 1. Modular by Domain
Each business domain is a self-contained Laravel module:
```
app/
├── Modules/
│ ├── Auth/
│ ├── Users/
│ ├── Companies/
│ ├── Creators/
│ ├── Portfolios/
│ ├── Campaigns/
│ ├── Applications/
│ ├── Invitations/
│ ├── Projects/
│ ├── Deliverables/
│ ├── VideoReview/
│ ├── Messaging/
│ ├── Notifications/
│ ├── Reviews/
│ ├── Reputation/
│ ├── Reporting/
│ ├── Matching/
│ ├── Admin/
│ └── PeerTube/
```
Each module contains:
```
Modules/Campaigns/
├── Models/
├── Controllers/
├── Services/
├── Requests/ (Form Requests / Validation)
├── Resources/ (API Resources / Transformers)
├── Policies/ (Authorization)
├── Events/
├── Listeners/
├── Jobs/
├── Routes/
├── Database/
│ ├── Migrations/
│ └── Seeders/
└── Tests/
```
### 2. Service Layer Pattern
Controllers are thin. Business logic lives in Services:
```php
// Controller: validate + delegate
// Service: orchestrate business logic
// Repository (optional): complex queries
// Model: data + relationships + scopes
```
### 3. Event-Driven Side Effects
Never chain side effects directly. Use events:
```
ApplicationAccepted →
├── CreateProject
├── NotifyCreator
├── UpdateCampaignStats
└── LogActivity
```
### 4. Policy-Based Authorization
Every action has a policy. No inline `if (user->role === 'admin')` checks:
```php
$this->authorize('approve', $application);
```
### 5. API-First Design
Every feature works via API first, then web views consume the same API/services.
---
## URL Structure
```
/ → Landing page
/login → Login (all types)
/register → Choose: company or creator
/register/company → Company registration
/register/creator → Creator registration
/admin/... → SuperAdmin panel
/company/... → Company dashboard
/creator/... → Creator dashboard
/creators/{username} → Public creator profile
/campaigns/{slug} → Public campaign page
/companies/{slug} → Public company page
/api/v1/... → REST API
```
---
## Database Naming Conventions
- Tables: `snake_case`, plural (`campaigns`, `creator_profiles`)
- Columns: `snake_case` (`created_at`, `company_id`)
- Foreign keys: `{singular_table}_id` (`campaign_id`, `user_id`)
- Pivot tables: alphabetical (`campaign_creator`, `project_user`)
- Soft deletes on everything user-facing
- UUIDs as public identifiers, auto-increment IDs internally
- All timestamps in UTC, display in user timezone
---
## Localization Strategy
- All strings through `__()` / `@lang()`
- Language files: `lang/en/`, `lang/ar/`
- Database content: separate `_translations` table or JSON column
- RTL support baked into CSS from day one
- Currency: support multiple (EGP, SAR, AED, USD)
- Date format: respect locale
---
## Security Baseline
- CSRF on all forms
- Rate limiting on auth + API endpoints
- Input sanitization (XSS prevention)
- SQL injection prevention (Eloquent parameterized queries)
- File upload validation (type, size, mime sniffing)
- Content Security Policy headers
- CORS configured for known origins only
- Audit log for all admin actions
- Two-factor authentication (optional, future)
# 02 — Authentication & User System
## Registration Flows
### Creator Registration
**Fields:**
| Field | Type | Required | Validation |
|-------|------|----------|------------|
| first_name | string | yes | min:2, max:50, alpha_spaces |
| last_name | string | yes | min:2, max:50, alpha_spaces |
| email | email | yes | unique:users, valid MX record |
| password | string | yes | min:8, confirmed, mixed case + number |
| username | string | yes | min:3, max:30, unique, alphanumeric + underscores only, no reserved words |
| phone | string | no | valid phone format with country code |
| country | string | yes | valid ISO country code |
| agreed_to_terms | boolean | yes | must be true |
**Flow:**
1. User fills form → validate → create user (role: creator) + creator_profile
2. Send verification email
3. Redirect to `/creator/onboarding`
4. User CANNOT access platform features until email verified
**Edge Cases:**
- Email already exists but unverified → resend verification, don't create duplicate
- Email already exists and verified → show "already registered" with login link
- Username taken → suggest 3 alternatives (append numbers/underscores)
- Reserved usernames: `admin`, `support`, `ugcheaven`, `company`, `creator`, `api`, `www`, `mail`, `help`, `null`, `undefined`
- Disposable email domains → reject with message
- Password in breach list (HaveIBeenPwned API, optional) → warn but allow
---
### Company Registration
**Fields:**
| Field | Type | Required | Validation |
|-------|------|----------|------------|
| company_name | string | yes | min:2, max:100 |
| contact_person_name | string | yes | min:2, max:100 |
| email | email | yes | unique:users, valid MX record, prefer business email |
| password | string | yes | min:8, confirmed, mixed case + number |
| phone | string | yes | valid phone format |
| country | string | yes | valid ISO country code |
| industry | string | yes | from predefined list |
| website | url | no | valid URL format |
| company_size | enum | yes | solo, 2-10, 11-50, 51-200, 201-500, 500+ |
| agreed_to_terms | boolean | yes | must be true |
**Flow:**
1. User fills form → validate → create user (role: company) + company_profile
2. Send verification email
3. Company status: `pending_review` (NOT immediately active)
4. Admin reviews and approves/rejects
5. On approval → email sent, company can use platform
6. On rejection → email sent with reason, can re-apply
**Edge Cases:**
- Free email (gmail, yahoo, etc.) → warning but not blocking (small businesses use these)
- Duplicate company name → allow (different companies can share names), unique slug generated
- Company rejected and wants to re-apply → allow, previous rejection visible to admin
- Company approved then found to be fraudulent → suspend flow (see below)
---
## Login
**Fields:**
| Field | Type | Required |
|-------|------|----------|
| email | email | yes |
| password | string | yes |
| remember_me | boolean | no |
**Flow:**
1. Validate credentials
2. Check user status (active, suspended, banned, pending)
3. If active → create session, redirect to role-appropriate dashboard
4. If suspended → show suspension message + reason + appeal link
5. If banned → show banned message (no appeal)
6. If email unverified → show "verify email" message + resend link
7. If company pending_review → show "under review" message
**Edge Cases:**
- 5 failed attempts in 15 minutes → lock account for 30 minutes + email notification
- Login from new device/location → email notification (informational, not blocking)
- Login from blocked country (if geo-restriction enabled) → reject
- Remember me → 30-day session, without → 2-hour session
- Concurrent sessions → allow up to 5, oldest killed when 6th created
- User deleted (soft) → "Account not found" (same message as wrong email for security)
---
## Password Reset
**Flow:**
1. User enters email → always show "if account exists, email sent" (prevent enumeration)
2. Token expires in 60 minutes
3. Token is single-use
4. On reset: kill all existing sessions
**Edge Cases:**
- Multiple reset requests → invalidate previous tokens
- Token used after expiry → "expired" message + new reset link
- User tries to reset while account is banned → silently ignore (no email sent)
- Rate limit: max 3 reset emails per hour per email address
---
## Email Verification
**Flow:**
1. Signed URL sent to email (expires in 24 hours)
2. Click verifies email, redirects to dashboard
3. Resend available (max 5 per hour)
**Edge Cases:**
- User changes email → re-verify new email, old email stays active until new confirmed
- Verification link expired → show "expired" + auto-resend button
- User verified but manually unverified by admin → must re-verify
---
## User Statuses
| Status | Can Login | Can Use Platform | Visible to Others |
|--------|-----------|-----------------|-------------------|
| active | yes | yes | yes |
| unverified | yes (limited) | no (verify email wall) | no |
| pending_review | yes (limited) | no (waiting wall) | no |
| suspended | no | no | hidden |
| banned | no | no | hidden |
| deactivated | no | no | hidden |
| deleted (soft) | no | no | no |
---
## User Status Transitions
```
registered → unverified → active
→ pending_review (company) → active
→ rejected → can re-register
active → suspended (by admin, with reason + duration)
→ banned (by admin, permanent)
→ deactivated (by user, self-service)
suspended → active (after duration expires OR admin lifts)
deactivated → active (user re-activates within 30 days)
deactivated (30+ days) → deleted (soft, automated)
```
---
## Roles & Permissions
### Role Hierarchy
```
superadmin > company > creator
```
### Permission Model
Using Spatie Laravel Permission or custom:
**SuperAdmin Permissions (all of the below plus):**
- manage_users
- manage_companies
- manage_creators
- manage_campaigns
- manage_projects
- moderate_content
- view_analytics
- manage_settings
- manage_reports
- impersonate_users
**Company Permissions:**
- manage_own_profile
- create_campaigns
- manage_own_campaigns
- view_applications
- manage_applications
- invite_creators
- manage_own_projects
- review_deliverables
- send_messages
- leave_reviews
- report_users
**Creator Permissions:**
- manage_own_profile
- manage_portfolio
- browse_campaigns
- apply_to_campaigns
- respond_to_invitations
- manage_own_deliverables
- upload_submissions
- send_messages
- leave_reviews
- report_users
---
## Session Management
- Sessions stored in Redis (for easy invalidation)
- Session data: user_id, role, ip, user_agent, last_active_at
- Admin can view all active sessions for a user
- User can view and kill their own sessions
- Force logout: admin can kill all sessions for a user
---
## Audit Log
Every auth event logged:
| Event | Data Captured |
|-------|--------------|
| login_success | ip, user_agent, location |
| login_failed | ip, user_agent, email_attempted |
| logout | ip |
| password_reset_requested | ip |
| password_reset_completed | ip |
| email_verified | ip |
| account_suspended | admin_id, reason, duration |
| account_banned | admin_id, reason |
| account_deactivated | ip |
| account_reactivated | ip |
| role_changed | admin_id, old_role, new_role |
| impersonation_started | admin_id, target_user_id |
| impersonation_ended | admin_id |
# 03 — Creator Profiles
## Profile Completeness System
Creators have a **profile completion score** (0-100%). This gates visibility:
- < 50% → not visible in search, cannot apply to campaigns
- 50-79% → visible but marked "incomplete profile"
- 80%+ → fully visible, can apply, eligible for recommendations
### Completion Score Breakdown:
| Section | Weight | Required for 50% |
|---------|--------|-------------------|
| Basic info (name, bio, photo) | 20% | yes |
| Country + Language | 10% | yes |
| At least 1 niche selected | 10% | yes |
| At least 1 skill selected | 10% | yes |
| At least 1 portfolio video | 25% | no |
| Social links (at least 1) | 10% | no |
| Equipment listed | 10% | no |
| Availability set | 5% | no |
---
## Profile Fields
### Basic Information
| Field | Type | Required | Validation | Notes |
|-------|------|----------|------------|-------|
| display_name | string | yes | min:2, max:50 | Shown everywhere |
| username | string | yes | unique, slug-safe | Set at registration, changeable once per 30 days |
| bio | text | yes | min:20, max:500 | Plain text, no HTML |
| profile_picture | image | yes | jpg/png/webp, max 5MB, min 200x200px | Auto-cropped to square, stored in 3 sizes |
| cover_image | image | no | jpg/png/webp, max 10MB, min 1200x400px | Wide banner format |
| date_of_birth | date | no | must be 18+ | Used for age range filtering, never shown publicly |
| gender | enum | no | male, female, prefer_not_to_say | Used for filtering when companies need specific gender |
### Location & Language
| Field | Type | Required | Validation |
|-------|------|----------|------------|
| country | string | yes | valid ISO 3166-1 |
| city | string | no | max:100 |
| timezone | string | no | valid IANA timezone |
| languages | array | yes (min 1) | from predefined list, max 10 |
| language_proficiency | enum per lang | yes | native, fluent, conversational, basic |
### Professional Information
| Field | Type | Required | Validation |
|-------|------|----------|------------|
| experience_level | enum | yes | beginner (0-1yr), intermediate (1-3yr), advanced (3-5yr), expert (5+yr) |
| content_niches | array | yes (min 1) | from predefined list, max 10 |
| skills | array | yes (min 1) | from predefined list, max 15 |
| video_styles | array | no | from predefined list, max 10 |
| hourly_rate | decimal | no | min:0, max:10000, nullable means "negotiable" |
| project_min_budget | decimal | no | min:0 |
### Content Niches (Predefined List)
```
gaming, technology, apps_software, fitness_health, food_cooking,
beauty, fashion, travel, education, finance, business, automotive,
pets_animals, lifestyle, parenting, home_decor, sports, music,
entertainment, science, diy_crafts, books, photography,
real_estate, saas, ecommerce, crypto_web3
```
### Skills (Predefined List)
```
video_creation, script_writing, voiceover, acting, video_editing,
motion_graphics, product_photography, livestreaming, storytelling,
comedy, unboxing, tutorials, reviews, testimonials, vlogs,
short_form_content, long_form_content, animation, sound_design,
color_grading, thumbnail_design, social_media_management
```
### Video Styles (Predefined List)
```
talking_head, cinematic, vlog_style, screen_recording, tutorial,
unboxing, testimonial, product_demo, lifestyle, comedy_skit,
before_after, day_in_life, asmr, reaction, interview,
stop_motion, timelapse, drone_footage
```
### Equipment
| Field | Type | Required | Validation |
|-------|------|----------|------------|
| camera_type | enum | no | smartphone, dslr, mirrorless, cinema, webcam |
| camera_models | array | no | free text, max 5 entries, max 50 chars each |
| microphone | enum | no | phone_builtin, lav, shotgun, condenser, dynamic, none |
| lighting | enum | no | natural, ring_light, softbox, professional, none |
| has_green_screen | boolean | no | default false |
| has_studio | boolean | no | default false |
| editing_software | array | no | from list: premiere, final_cut, davinci, capcut, filmora, after_effects, other |
### Social Links
| Field | Type | Required | Validation |
|-------|------|----------|------------|
| tiktok | url | no | must match tiktok.com/@username pattern |
| instagram | url | no | must match instagram.com/username pattern |
| youtube | url | no | must match youtube.com/@username or /channel/ pattern |
| facebook | url | no | must match facebook.com/ pattern |
| x_twitter | url | no | must match x.com/username or twitter.com/username |
| snapchat | string | no | username only, max:30 |
| linkedin | url | no | must match linkedin.com/in/ pattern |
| website | url | no | any valid URL |
**Edge Cases for Social Links:**
- URL format varies (with/without www, http/https) → normalize on save
- Platform changes URL structure → validate loosely, store normalized
- User puts full URL in username field → extract username automatically
- Duplicate detection: same social linked to multiple creators → warning only (agencies exist)
### Availability
| Field | Type | Required | Validation |
|-------|------|----------|------------|
| availability_status | enum | no | available, busy, vacation, not_accepting |
| available_from | date | no | must be today or future |
| weekly_capacity_hours | integer | no | 1-80 |
| preferred_project_length | enum | no | one_off, short (1-2 weeks), medium (2-8 weeks), long (2+ months) |
| response_time | enum | no | within_hours, within_day, within_2_days, within_week |
---
## Profile Visibility Rules
| Condition | Visible in Search | Visible via Direct Link |
|-----------|-------------------|------------------------|
| Profile < 50% complete | no | yes (with "incomplete" banner) |
| Profile 50%+ complete | yes | yes |
| User suspended | no | no (404) |
| User deactivated | no | no (404) |
| User banned | no | no (404) |
| Availability = "not_accepting" | yes (marked unavailable) | yes |
---
## Profile Update Rules
- Username: changeable once per 30 days. Old username released after 90 days. Redirect from old username for 90 days.
- Profile picture: immediately live (no moderation queue for now, flag for future)
- Bio: immediate, but flagged for moderation if contains URLs or phone numbers
- All changes logged in activity_log (for admin audit)
- Rate limit: max 50 profile updates per hour (prevent automated scraping/spam)
---
## Profile Verification (Trust Badge)
**Levels:**
1. **Unverified** — just registered
2. **Email Verified** — confirmed email (automatic after verification)
3. **Identity Verified** — submitted government ID (future feature)
4. **Pro Verified** — manual review by admin (proven track record)
**Verification Badge Display:**
- Shown on profile, in search results, in applications
- Companies can filter by verification level
---
## Profile Analytics (Creator's Own Dashboard)
| Metric | Description |
|--------|-------------|
| profile_views | How many times profile was viewed (unique per day) |
| search_appearances | How many times appeared in search results |
| campaign_matches | How many campaigns matched their profile |
| application_success_rate | accepted / total applications |
| invitation_count | How many invitations received |
| response_rate | % of messages responded to within 24h |
| avg_response_time | Average time to respond to messages |
---
## Edge Cases & Special Scenarios
1. **Creator changes country** → affects campaign matching, recalculate recommendations
2. **Creator removes all niches** → profile drops below 50%, hidden from search until fixed
3. **Creator uploads NSFW profile picture** → moderation flag (future: AI detection)
4. **Creator bio contains competitor URLs** → no blocking, but flag for review
5. **Creator adds 50 social links via API spam** → max 1 per platform enforced
6. **Two creators claim same social profile** → no blocking (can't verify ownership without OAuth)
7. **Creator profile in Arabic, company searching in English** → search against both original and translated bio
8. **Creator sets hourly rate to $0** → treat as "negotiable" / "will discuss"
9. **Creator under 18 (date_of_birth check)** → reject registration, show message
10. **Creator wants to switch to company account** → not allowed. Must create new account. (Keep data separate)
# 04 — Company Profiles
## Company Registration → Approval Flow
```
Register → Email Verification → Pending Review → Admin Reviews
├── Approved → Active
└── Rejected → Can Re-apply
```
**Admin Review Checklist:**
- Company name is real (not gibberish)
- Website exists (if provided)
- Contact info seems legitimate
- Not a duplicate of existing company
- Not on any blacklist
---
## Profile Fields
### Basic Information
| Field | Type | Required | Validation | Notes |
|-------|------|----------|------------|-------|
| company_name | string | yes | min:2, max:100 | Display name |
| slug | string | auto | auto-generated from name, unique | URL-friendly identifier |
| logo | image | yes | jpg/png/webp/svg, max 5MB, min 200x200px | Square format, stored in 3 sizes |
| cover_image | image | no | jpg/png/webp, max 10MB, min 1200x400px | Banner |
| description | text | yes | min:50, max:2000 | Company description |
| short_description | string | no | max:160 | Used in search results / cards |
| website | url | no | valid URL, must resolve (HEAD check) |
| industry | enum | yes | from predefined list |
| country | string | yes | valid ISO 3166-1 |
| city | string | no | max:100 |
| founded_year | integer | no | 1900-current_year |
| company_size | enum | yes | solo, 2-10, 11-50, 51-200, 201-500, 500+ |
| company_type | enum | no | startup, agency, enterprise, non_profit, government |
### Industries (Predefined List)
```
gaming, technology, saas, ecommerce, fintech, healthtech,
food_beverage, beauty_cosmetics, fashion, automotive,
real_estate, education, media_entertainment, sports,
travel_hospitality, retail, telecommunications,
advertising_marketing, consulting, manufacturing, other
```
### Contact Information
| Field | Type | Required | Validation |
|-------|------|----------|------------|
| contact_email | email | yes | valid email (may differ from login email) |
| contact_phone | string | no | valid phone with country code |
| contact_person_name | string | yes | min:2, max:100 |
| contact_person_role | string | no | max:50 (e.g., "Marketing Manager") |
### Branding Assets
| Field | Type | Required | Validation |
|-------|------|----------|------------|
| brand_colors | array | no | max 5, valid hex codes |
| brand_guidelines_file | file | no | PDF only, max 50MB |
| brand_assets | files | no | zip/pdf/png/jpg, max 100MB total |
| tone_of_voice | enum | no | professional, casual, playful, bold, minimal, luxury |
### Social Presence
| Field | Type | Required | Validation |
|-------|------|----------|------------|
| linkedin | url | no | linkedin.com/company/ pattern |
| instagram | url | no | instagram.com/ pattern |
| tiktok | url | no | tiktok.com/@ pattern |
| youtube | url | no | youtube.com/ pattern |
| x_twitter | url | no | x.com/ pattern |
| facebook | url | no | facebook.com/ pattern |
---
## Company Statuses
| Status | Can Login | Can Post Campaigns | Visible | Notes |
|--------|-----------|-------------------|---------|-------|
| pending_review | yes (limited view) | no | no | Waiting for admin |
| active | yes | yes | yes | Normal operation |
| suspended | no | no | hidden | Temporary, with reason |
| banned | no | no | hidden | Permanent |
| deactivated | no | no | hidden | Self-service |
---
## Company Verification Tiers
| Tier | Badge | Requirements | Benefits |
|------|-------|-------------|----------|
| Basic | none | Registered + approved | Can post campaigns |
| Verified | blue badge-check icon | Admin manual verification | Higher in search, trust badge |
| Premium | gold star icon | Paid plan OR high activity | Priority support, featured placement |
**Verification criteria (admin manual):**
- Real website with matching branding
- LinkedIn company page exists
- Has posted at least 3 campaigns
- At least 1 completed project with positive review
- OR: admin manually verifies via video call / documents
---
## Company Dashboard Sections
### Overview
- Active campaigns count
- Active projects count
- Pending applications count
- Pending deliverables to review
- Total spend (all time)
- Average creator rating given
### My Campaigns
- Draft / Active / Paused / Completed / Cancelled tabs
- Quick stats per campaign (applications, views, deadline)
### My Projects
- In Progress / Waiting Review / Completed tabs
- Creator info, progress, deadline
### Discover Creators
- Search + filters
- Saved creators (favorites)
- Recent views
### Messages
- Conversations with creators
- Unread count
### Reviews
- Reviews I've given
- Reviews I've received
### Settings
- Company profile
- Team members (future)
- Billing (future)
- Notifications preferences
---
## Company-Specific Edge Cases
1. **Company changes name** → slug stays the same (SEO continuity). Admin can approve slug change separately.
2. **Company uploads brand guidelines as .exe** → reject, only PDF allowed
3. **Company description in only Arabic** → fine, but suggest adding English version
4. **Company has 0 campaigns after 6 months** → send re-engagement email, no automatic action
5. **Company creates campaign while suspended** → impossible (policy check blocks)
6. **Company account used by multiple people** → sessions table shows multiple locations, no blocking (team use is valid)
7. **Company rejected, creates new account with same name** → allow, admin will see previous rejection in review
8. **Company wants to merge two accounts** → admin-only action, involves migrating campaigns/projects
9. **Company profile has no logo** → cannot post campaigns (logo required for public visibility)
10. **Company's website returns 404** → flag for admin, don't block (sites go down temporarily)
---
## Company Activity Feed (Admin View)
All company actions logged:
- Campaign created/edited/published/paused/cancelled
- Application accepted/rejected
- Invitation sent
- Deliverable approved/revision requested
- Message sent
- Review posted
- Profile updated
- Login/logout events
---
## Company Public Profile
What creators see when viewing a company:
- Logo, name, description
- Industry, location, size
- Verification badge
- Active campaigns (public ones)
- Stats: total projects completed, avg creator rating, member since
- Reviews from creators
- Response time badge (based on message response speed)
This diff is collapsed.
# 06 — Applications & Invitations
## Two Paths to a Project
```
Path A: Creator → applies to Campaign → Company accepts → Project
Path B: Company → invites Creator → Creator accepts → Project
```
Both converge into the same Project entity.
---
## APPLICATIONS (Creator → Campaign)
### Application Fields
| Field | Type | Required | Validation | Notes |
|-------|------|----------|------------|-------|
| campaign_id | FK | yes | must exist, must be published | |
| creator_id | FK | auto | current user | |
| cover_message | text | yes | min:50, max:1000 | Why they're a good fit |
| portfolio_items | array of FK | no | max 5, must belong to creator | Reference specific portfolio pieces |
| proposed_budget | decimal | no | min:0 | Creator's price (if campaign is negotiable) |
| proposed_timeline | text | no | max:200 | Creator's delivery estimate |
| availability_confirmed | boolean | yes | must be true | "I can deliver by the deadline" |
| questions_for_company | text | no | max:500 | Any questions before starting |
### Application Status Flow
```
submitted → viewed → shortlisted → accepted → project_created
→ rejected
→ withdrawn (by creator)
```
| Status | Description | Who Changes | Creator Sees |
|--------|-------------|-------------|-------------|
| submitted | Just applied | auto | "Applied" |
| viewed | Company opened application | auto | "Viewed" (check icon) |
| shortlisted | Company marked as potential | company | "Shortlisted" (star icon) |
| accepted | Company wants to work together | company | "Accepted" (check-circle icon) |
| rejected | Company passed | company | "Not selected" |
| withdrawn | Creator cancelled | creator | "Withdrawn" |
### Application Rules
1. **One application per creator per campaign** — cannot apply twice
2. **Cannot apply to own company's campaign** — (if somehow both roles exist)
3. **Cannot apply if profile < 50% complete** — must complete profile first
4. **Cannot apply if currently suspended** — blocked
5. **Cannot apply after deadline** — form disabled
6. **Cannot apply to invite-only campaigns** — button not shown
7. **Cannot apply if campaign is paused/closed/cancelled** — blocked
### Application Actions
**Creator Can:**
- Submit application
- Edit application (only while status = submitted, before company views)
- Withdraw application (any time before accepted)
- View own application status
**Company Can:**
- View all applications for their campaign
- Filter/sort applications
- Mark as shortlisted
- Accept application (creates project)
- Reject application (with optional reason)
- Bulk reject (for mass cleanup)
- Download applicant list (CSV export)
### Application Notifications
| Event | Notify Who | Channel |
|-------|-----------|---------|
| New application | Company | in-app + email |
| Application viewed | Creator | in-app only |
| Shortlisted | Creator | in-app + email |
| Accepted | Creator | in-app + email + push |
| Rejected | Creator | in-app + email |
| Company message on application | Creator | in-app + email |
### Application Edge Cases
1. **Creator applies, then gets suspended** → application hidden from company view, status set to "withdrawn (system)"
2. **Company accepts more creators than campaign.creator_count** → allowed (soft limit, just a target)
3. **Creator withdraws after being shortlisted** → company notified, status = withdrawn
4. **Company rejects then wants to accept** → allowed (status can go from rejected → accepted, rare but valid)
5. **Campaign cancelled after applications received** → all applications set to "cancelled" status, creators notified
6. **Creator applies to 50 campaigns simultaneously** → allowed, no limit (but rate limit form submission to prevent spam)
7. **Application cover message is just "hi"** → min:50 validation prevents this
8. **Creator has no portfolio but applies** → allowed if profile is 50%+ (portfolio not strictly required)
9. **Company never responds to applications** → after 30 days with no action, creator sees "no response" indicator
10. **Duplicate application attempt via API** → 422 error "You have already applied to this campaign"
---
## INVITATIONS (Company → Creator)
### Invitation Fields
| Field | Type | Required | Validation | Notes |
|-------|------|----------|------------|-------|
| company_id | FK | auto | current company | |
| creator_id | FK | yes | must exist, must be active | |
| campaign_id | FK | no | if linked to specific campaign | Can invite without campaign |
| message | text | yes | min:20, max:1000 | Personal message to creator |
| proposed_budget | decimal | no | min:0 | What company is offering |
| proposed_scope | text | no | max:500 | Brief description of work |
| deadline | date | no | must be future | Expected delivery |
| expires_at | datetime | yes | default: 7 days from now | Auto-expire if no response |
### Invitation Status Flow
```
sent → viewed → accepted → project_created
→ declined
→ expired (no response by expires_at)
→ cancelled (by company)
```
| Status | Description | Creator Sees |
|--------|-------------|-------------|
| sent | Invitation created | "New Invitation" |
| viewed | Creator opened it | (no change visible) |
| accepted | Creator wants to work | "Accepted" |
| declined | Creator said no | "Declined" |
| expired | No response in time | "Expired" |
| cancelled | Company withdrew | "Cancelled" |
### Invitation Rules
1. **One active invitation per company-creator pair per campaign** — no spam
2. **Cannot invite suspended/banned creators** — blocked
3. **Cannot invite if company is not active** — blocked
4. **Max 50 invitations per company per day** — rate limit
5. **Cannot invite creator who already applied to same campaign** — show "already applied" instead
6. **Expired invitations count toward daily limit** — prevents spam-then-expire abuse
### Invitation Actions
**Company Can:**
- Send invitation
- Cancel invitation (before accepted)
- Resend invitation (resets expiry, max 2 resends)
- Bulk invite from search results (up to 10 at once)
- View invitation history
**Creator Can:**
- View invitation details
- Accept invitation (triggers project creation or negotiation)
- Decline invitation (with optional reason)
- Ask questions (via messaging, before accepting)
### Invitation Notifications
| Event | Notify Who | Channel |
|-------|-----------|---------|
| New invitation | Creator | in-app + email + push |
| Invitation accepted | Company | in-app + email |
| Invitation declined | Company | in-app + email |
| Invitation expiring (24h left) | Creator | in-app + email |
| Invitation expired | Company | in-app |
| Invitation cancelled | Creator | in-app |
### Invitation Edge Cases
1. **Company invites creator whose availability = "not_accepting"** → allowed but show warning to company "this creator may not be available"
2. **Creator declines, company sends another invitation** → allowed, but after 3 declines show "this creator has declined multiple times"
3. **Invitation expires, company resends** → counts as new invitation (new expiry), max 2 resends total
4. **Creator accepts but company got suspended between send and accept** → auto-cancel project, notify creator
5. **Creator blocks company** → future invitations silently hidden (company still sees "sent" but creator never sees it)
6. **Company sends 100 invitations with same generic message** → spam detection: if same message used >10 times in 24h, flag for review
7. **Invitation to a campaign that's now closed** → still valid (invitation bypasses campaign deadline)
8. **Creator accepts invitation to campaign they also applied to** → application auto-withdrawn, invitation acceptance takes priority
---
## From Acceptance to Project
When either path results in acceptance:
**Application Accepted:**
1. Application status → "accepted"
2. Project created with:
- campaign_id
- company_id (from campaign)
- creator_id (from application)
- Deliverables copied from campaign requirements
- Timeline from campaign
- Budget from campaign (or negotiated)
3. Other applicants NOT auto-rejected (company may accept multiple)
**Invitation Accepted:**
1. Invitation status → "accepted"
2. Project created with:
- campaign_id (if invitation was campaign-linked)
- company_id (from invitation)
- creator_id (from invitation)
- Deliverables from invitation scope (or campaign if linked)
- Timeline from invitation
- Budget from invitation
In both cases → Project enters "not_started" status, both parties notified.
# 07 — Projects
## What is a Project?
A Project is the working relationship between ONE company and ONE creator for a specific piece of work. It's created when an application is accepted or an invitation is accepted.
Every project has clear deliverables, a timeline, and a structured review process.
---
## Project Lifecycle
```
not_started → in_progress → waiting_review → approved → completed
↗ ↘
revision_requested ←←←←←←←←←
in_progress (creator fixes)
Any state → cancelled (by either party or admin)
Any state → disputed (escalated to admin)
```
### Status Definitions
| Status | Description | Who Triggers |
|--------|-------------|-------------|
| not_started | Project created, hasn't begun | auto (on creation) |
| in_progress | Creator is actively working | creator (marks "started") |
| waiting_review | Creator submitted deliverable(s) | auto (on submission) |
| revision_requested | Company wants changes | company |
| approved | All deliverables accepted | company (or auto after all approved) |
| completed | Project fully done, both parties confirm | auto (after approval + confirmation period) |
| cancelled | Terminated early | either party OR admin |
| disputed | Escalated to admin for resolution | either party |
| on_hold | Temporarily paused by mutual agreement | either party (other must confirm) |
---
## Project Fields
| Field | Type | Source | Notes |
|-------|------|--------|-------|
| uuid | uuid | auto | Public identifier |
| campaign_id | FK | nullable | Not all projects come from campaigns |
| company_id | FK | required | |
| creator_id | FK | required | |
| application_id | FK | nullable | If originated from application |
| invitation_id | FK | nullable | If originated from invitation |
| title | string | from campaign or invitation | Editable by company |
| description | text | from campaign or invitation | Editable by company |
| status | enum | see above | |
| budget_amount | decimal | from acceptance | |
| budget_currency | string | from acceptance | |
| started_at | datetime | when creator marks started | |
| deadline | date | from campaign/invitation | |
| completed_at | datetime | when marked completed | |
| cancelled_at | datetime | if cancelled | |
| cancelled_by | FK (user) | who cancelled | |
| cancellation_reason | text | required on cancel | |
| revision_count | integer | starts at 0 | Tracks total revisions used |
| max_revisions | integer | from campaign | After max, company can still request (flagged) |
---
## Project Dashboard (Both Sides See)
### Header
- Project title
- Status badge (colored)
- Company logo + name | Creator avatar + name
- Deadline with countdown (red if < 3 days)
- Budget amount
### Tabs
1. **Overview** — description, requirements, timeline
2. **Deliverables** — list of required deliverables + their statuses
3. **Activity** — timeline of all events (submissions, reviews, messages)
4. **Files** — all shared files (brand assets, submissions, raw footage)
5. **Messages** — project-specific conversation thread
---
## Project Actions
### Creator Can:
- Mark project as "started" (not_started → in_progress)
- Submit deliverables
- Upload files
- Send messages in project thread
- Request deadline extension (company must approve)
- Request to cancel (requires reason, company must confirm OR admin intervenes)
- Escalate to dispute
### Company Can:
- View all deliverables and submissions
- Approve deliverable
- Request revision (with feedback)
- Approve entire project (all deliverables done)
- Cancel project (with reason, before completion)
- Extend deadline
- Escalate to dispute
### Admin Can:
- View any project
- Force status change
- Resolve disputes
- Cancel project on behalf of either party
- Add admin notes (not visible to either party)
---
## Deadline Management
### Deadline Extension Flow:
```
Creator requests extension → Company receives notification
→ Company approves → deadline updated, both notified
→ Company rejects → creator must deliver by original date
→ No response in 48h → auto-reminder to company
```
### Overdue Projects:
- Deadline passes while status = in_progress or not_started:
1. Day 0: both parties notified "project is overdue"
2. Day 3: admin flagged for review
3. Day 7: admin may intervene
4. Day 14: auto-escalate to dispute if no resolution
---
## Project Cancellation Rules
### Creator Cancels:
- Before starting (not_started) → minimal impact, no penalty
- After starting (in_progress) → noted on profile, company can leave review
- After submitting (waiting_review) → unusual, flagged for admin
### Company Cancels:
- Before creator starts → no impact
- After creator starts → creator can leave review, admin notified
- After deliverable submitted → requires admin approval (creator did the work)
### Cancellation Cooldown:
- If a user cancels 3+ projects in 30 days → admin notified for pattern review
- Cancellation rate shown in reputation (see doc 11)
---
## Project Edge Cases
1. **Creator submits, company never reviews** → after 7 days of no response, creator can escalate. After 14 days, admin can auto-approve.
2. **Company requests revisions beyond max_revisions** → allowed but: (a) creator sees "extra revision" warning, (b) creator can decline additional revisions, (c) metric tracked for admin
3. **Both parties want to cancel** → instant mutual cancellation, no penalties
4. **Creator's account gets suspended mid-project** → project put on hold, admin reviews. Company notified.
5. **Company's account gets suspended mid-project** → project put on hold, admin reviews. Creator's work protected.
6. **Project created but deadline already passed** → shouldn't happen (validation). If somehow occurs, auto-flag for admin.
7. **Creator submits wrong file type** → deliverable validation catches this (see doc 08)
8. **Project has 0 deliverables defined** → can't happen (at least 1 required on creation)
9. **Creator marks "started" but never submits** → overdue flow kicks in at deadline
10. **Company approves then wants to un-approve** → not allowed. Must create new revision request explaining why.
11. **Project dispute while deliverable is in review** → project frozen, neither party can submit/review until admin resolves
12. **Creator delivers 3 of 5 videos, then goes silent** → partial completion tracked per deliverable, overdue flow for remaining
---
## Project Activity Log
Every action creates an activity entry:
```
[2024-03-15 14:30] Creator started working on project
[2024-03-18 09:15] Creator submitted "Product Review Video" (v1)
[2024-03-19 11:00] Company viewed submission
[2024-03-19 11:30] Company requested revision: "Please re-record intro..."
[2024-03-20 16:45] Creator submitted "Product Review Video" (v2)
[2024-03-21 10:00] Company approved "Product Review Video"
[2024-03-21 10:01] All deliverables approved — project completed
```
This is immutable (no editing/deleting activity entries).
# 08 — Deliverables & Video Review System
## Deliverable Structure
Each project has 1+ deliverables. Each deliverable is a discrete item the creator must deliver.
---
## Deliverable Fields
| Field | Type | Required | Validation |
|-------|------|----------|------------|
| project_id | FK | yes | |
| title | string | yes | min:5, max:100 |
| description | text | no | max:1000 |
| type | enum | yes | video, image, script, raw_footage, audio, document |
| sort_order | integer | yes | display position |
| status | enum | auto | see below |
| specifications | JSON | no | type-specific specs |
| max_revisions | integer | no | override project-level default |
| revision_count | integer | auto | starts at 0 |
| due_date | date | no | must be ≤ project deadline |
| approved_at | datetime | auto | when company approves |
| approved_by | FK | auto | which company user approved |
### Type-Specific Specifications (JSON)
**Video:**
```json
{
"min_duration_seconds": 30,
"max_duration_seconds": 60,
"aspect_ratio": "9:16",
"resolution_min": "1080p",
"format": ["mp4", "mov"],
"includes_captions": true,
"includes_music": false,
"includes_raw": false
}
```
**Image:**
```json
{
"min_width": 1080,
"min_height": 1080,
"aspect_ratio": "1:1",
"format": ["jpg", "png"],
"count": 3
}
```
**Script:**
```json
{
"max_words": 500,
"language": "ar",
"format": ["pdf", "docx", "txt"]
}
```
---
## Deliverable Status Flow
```
pending → submitted → under_review → approved
→ revision_requested → submitted (loop)
pending → overdue (deadline passed, nothing submitted)
```
| Status | Description | Visual |
|--------|-------------|--------|
| pending | Awaiting creator submission | Gray |
| submitted | Creator uploaded, waiting for review | Blue |
| under_review | Company is actively reviewing | Yellow |
| revision_requested | Company wants changes | Orange |
| approved | Company accepted this deliverable | Green |
| overdue | Past due_date with no submission | Red |
---
## Submission System
### Creating a Submission
Each time a creator submits (or resubmits after revision), a new **Submission** record is created. This preserves version history.
| Field | Type | Required | Validation |
|-------|------|----------|------------|
| deliverable_id | FK | yes | |
| version | integer | auto | 1, 2, 3... (auto-increment per deliverable) |
| creator_id | FK | auto | |
| type | enum | yes | matches deliverable type |
| notes | text | no | max:500, creator's notes about this version |
| submitted_at | datetime | auto | |
### Submission Files
| Field | Type | Required | Validation |
|-------|------|----------|------------|
| submission_id | FK | yes | |
| file_type | enum | yes | video, image, document, audio, archive |
| file_path | string | yes | S3/storage path |
| file_name | string | yes | original filename |
| file_size | integer | yes | bytes |
| mime_type | string | yes | validated mime |
| peertube_uuid | string | nullable | if video, the PeerTube video UUID |
| peertube_embed_url | string | nullable | for video player embedding |
| thumbnail_path | string | nullable | auto-generated or from PeerTube |
| duration_seconds | integer | nullable | for video/audio |
| width | integer | nullable | for images/video |
| height | integer | nullable | for images/video |
| metadata | JSON | nullable | additional file metadata |
---
## Video Upload Flow (PeerTube Integration)
### Upload Process:
```
Creator clicks "Upload Video"
→ File uploaded to application server first (temp storage)
→ Server validates (size, format, duration)
→ Server uploads to PeerTube via API (resumable for large files)
→ PeerTube processes (transcoding)
→ Server receives PeerTube webhook/polls for status
→ Submission record created with peertube_uuid
→ Creator sees "Processing..." then "Ready"
→ Company can view via embedded PeerTube player
```
### PeerTube Upload Configuration:
- Privacy: `3` (Private) — only accessible via direct embed URL
- Channel: one channel per company (auto-created)
- Tags: project_id, deliverable_id (for organization)
- Name: `{project_title} - {deliverable_title} v{version}`
### Video Validation Rules:
| Rule | Value | Error Message |
|------|-------|---------------|
| Max file size | 2GB | "Video must be under 2GB" |
| Min duration | 5 seconds | "Video too short" |
| Max duration | based on deliverable spec | "Video exceeds maximum duration" |
| Allowed formats | mp4, mov, webm, mkv, avi | "Unsupported video format" |
| Min resolution | 720p | "Video resolution too low" |
### Video Processing States:
```
uploading → processing (PeerTube transcoding) → ready → available_for_review
→ failed (PeerTube error) → retry (max 3 attempts)
```
---
## Review System (Company Side)
### Viewing a Submission
- Video plays in embedded PeerTube player
- Side panel shows submission notes
- Below: review actions
### Review Actions:
1. **Approve** — deliverable marked approved, creator notified
2. **Request Revision** — must provide feedback
### Revision Request Fields:
| Field | Type | Required | Validation |
|-------|------|----------|------------|
| deliverable_id | FK | yes | |
| submission_id | FK | yes | which version this feedback is for |
| feedback | text | yes | min:20, max:2000 |
| timestamp_comments | array | no | see below |
| priority | enum | no | minor, major, critical |
| revision_deadline | date | no | when the revision is expected |
---
## Timestamp Comments (Video-Specific)
Companies can leave feedback at specific points in a video:
| Field | Type | Required | Validation |
|-------|------|----------|------------|
| submission_id | FK | yes | |
| timestamp_seconds | decimal | yes | 0 to video_duration |
| comment | text | yes | min:5, max:500 |
| type | enum | no | suggestion, issue, praise |
| resolved | boolean | auto | starts false |
### Timestamp Comment UX:
- Company watches video
- Clicks at a specific moment → video pauses
- Types comment → comment anchored to that timestamp
- Creator sees comments as markers on video timeline
- Creator can mark comments as "resolved" in next version
### Edge Cases for Timestamp Comments:
1. **Video re-uploaded with different duration** → old timestamp comments preserved but may not align. Show with "[alert-triangle icon] from previous version" label.
2. **Comment at 0:00** → valid (intro feedback)
3. **Comment at last second** → valid (outro feedback)
4. **50+ comments on one video** → paginate in sidebar, all markers visible on timeline
5. **Comment references a frame that doesn't exist in new version** → keep comment, mark as "may no longer apply"
---
## Revision Flow
```
Company requests revision:
1. Deliverable status → revision_requested
2. revision_count incremented
3. Creator notified with feedback
4. Creator uploads new version (new submission, version+1)
5. Deliverable status → submitted
6. Company reviews again
7. Repeat or approve
```
### Revision Limits:
- Each deliverable has max_revisions (default: 2 from campaign, overridable)
- When revision_count reaches max:
- Company CAN still request more
- Creator sees: "This is an extra revision beyond the agreed limit"
- Creator can: accept and revise, OR decline the extra revision
- If declined → company can: approve current version, escalate to dispute, or cancel
### Revision Edge Cases:
1. **Company approves then realizes mistake** → cannot un-approve. Must contact admin.
2. **Creator submits revision without addressing feedback** → no system enforcement (subjective), company can request again
3. **Revision requested but creator goes silent** → overdue flow after deadline
4. **Company gives contradictory feedback across versions** → creator can note this in submission notes, escalate if needed
5. **Version history has 10+ versions** → UI must handle gracefully (scrollable list, comparison view)
---
## Version Comparison
For each deliverable, the review page shows:
- Current version (video player or file preview)
- Version history dropdown (v1, v2, v3...)
- Side-by-side comparison (for images)
- For videos: can switch between versions in player
---
## Deliverable Approval → Project Completion
```
When ALL deliverables in a project are approved:
→ Project status auto-changes to "approved"
→ 48-hour confirmation period starts
→ If no disputes raised in 48h → project "completed"
→ Both parties prompted to leave reviews
```
---
## File Storage Architecture
```
/projects/{project_uuid}/
├── brand_assets/ (company uploads)
│ ├── guidelines.pdf
│ └── product_photos/
├── deliverables/
│ ├── {deliverable_uuid}/
│ │ ├── v1/
│ │ │ ├── video.mp4 (or PeerTube reference)
│ │ │ └── raw/ (if raw footage required)
│ │ ├── v2/
│ │ └── v3/
│ └── {deliverable_uuid_2}/
└── messages/ (message attachments)
└── {message_id}/
```
---
## Bulk Operations
### Company Managing Multiple Deliverables:
- "Approve All" button (only if all are in submitted/under_review state)
- "Download All" — zip all latest approved versions
- "Request Revisions" — can apply same note to multiple deliverables
### Creator Submitting Multiple Files:
- Drag-and-drop multiple files → auto-assign to correct deliverables (by naming convention or manual mapping)
- Batch upload progress indicator
# 09 — Creator Portfolios
## What is a Portfolio?
A creator's portfolio is their showreel — the videos and content that demonstrate their abilities. It's the primary tool companies use to evaluate creators before hiring.
Videos are stored on PeerTube (same instance as deliverables, different channel).
---
## Portfolio Structure
```
Creator
└── Portfolio
├── Collection: "Gaming Content"
│ ├── Video 1
│ ├── Video 2
│ └── Video 3
├── Collection: "Product Reviews"
│ ├── Video 4
│ └── Video 5
└── Uncategorized
└── Video 6
```
---
## Portfolio Item Fields
| Field | Type | Required | Validation |
|-------|------|----------|------------|
| creator_id | FK | auto | |
| collection_id | FK | no | nullable = uncategorized |
| title | string | yes | min:3, max:100 |
| description | text | no | max:500 |
| type | enum | yes | video, image, link |
| peertube_uuid | string | conditional | required for video type |
| peertube_embed_url | string | conditional | auto-generated |
| thumbnail_url | string | auto | from PeerTube or uploaded |
| external_url | url | conditional | for link type (YouTube, TikTok, etc.) |
| external_platform | enum | conditional | youtube, tiktok, instagram, other |
| file_path | string | conditional | for image type |
| duration_seconds | integer | nullable | for videos |
| tags | array | no | max 10, from predefined + custom |
| industry | enum | no | from predefined list |
| brand_worked_with | string | no | max:100, free text |
| is_featured | boolean | no | max 3 featured items |
| visibility | enum | yes | public, private, unlisted |
| sort_order | integer | auto | within collection |
| views_count | integer | auto | unique views |
| created_at | datetime | auto | |
---
## Portfolio Collections
| Field | Type | Required | Validation |
|-------|------|----------|------------|
| creator_id | FK | auto | |
| name | string | yes | min:2, max:50 |
| description | text | no | max:200 |
| sort_order | integer | auto | |
| visibility | enum | yes | public, private |
**Rules:**
- Max 20 collections per creator
- Max 50 items per collection
- Max 100 total portfolio items per creator
- Uncategorized is implicit (items without collection_id)
---
## Portfolio Video Upload Flow
```
Creator clicks "Add to Portfolio"
→ Choose: Upload new video OR Link external video
Upload:
→ File uploaded to temp storage
→ Validated (format, size, duration)
→ Uploaded to PeerTube (creator's portfolio channel)
→ PeerTube privacy: Unlisted (accessible via embed but not in PeerTube search)
→ Processing → Ready
→ Portfolio item created
External Link:
→ Creator pastes YouTube/TikTok/Instagram URL
→ System extracts metadata (title, thumbnail, duration via oEmbed)
→ Portfolio item created with external_url
→ Thumbnail stored locally (in case external source changes)
```
### PeerTube Portfolio Channel Strategy:
- Each creator gets ONE PeerTube channel: `portfolio_{creator_username}`
- Auto-created on first portfolio upload
- All portfolio videos go here (privacy: unlisted)
- Channel is not publicly listed on PeerTube instance
---
## Portfolio Visibility
| Visibility | Who Can See | Where It Appears |
|------------|-------------|-----------------|
| public | Everyone | Profile, search, applications |
| unlisted | Anyone with direct link | Applications (if creator shares) |
| private | Creator only | Nowhere (draft/archive) |
---
## Featured Items
- Creator can mark up to 3 items as "featured"
- Featured items appear at top of their public profile
- Featured items are prioritized in search results card preview
---
## Portfolio in Applications
When applying to a campaign, creators can:
- Reference specific portfolio items (attach up to 5)
- These are shown inline in the company's application review
- Company can view full portfolio from the application page
---
## Portfolio Analytics (Creator View)
| Metric | Description |
|--------|-------------|
| total_views | Sum of all portfolio item views |
| most_viewed | Which items get the most attention |
| views_from_companies | Views where viewer was a company user |
| views_from_campaigns | Views triggered from application review |
| views_this_week | Trend indicator |
---
## Portfolio Edge Cases
1. **Creator uploads 4K video (huge file)** → max 2GB enforced, suggest compression tips in error message
2. **External link (YouTube) video gets deleted** → periodic check (weekly cron), mark as "unavailable", notify creator
3. **Creator uploads copyrighted content** → no automated check (future: fingerprinting). Reportable by others.
4. **Creator's PeerTube channel gets full** → PeerTube quotas configured per-user (generous: 50GB per creator for portfolio)
5. **Portfolio item embedded on external site** → allowed if public/unlisted (PeerTube handles embed permissions)
6. **Creator wants to reorder items** → drag-and-drop UI, sort_order updated in batch
7. **Creator deletes portfolio item that's referenced in an active application** → reference preserved as "item no longer available", company notified
8. **Company downloads/screen-records portfolio video** → can't prevent, but watermark option (future)
9. **Portfolio video has no sound** → valid (some UGC is silent/music-only)
10. **Creator uploads 100 items then switches to private** → allowed, all 100 hidden from search
11. **External URL changes format** → normalize on save, re-validate periodically
12. **Thumbnail generation fails** → show placeholder, retry in background
# 10 — Messaging System
## Overview
Messaging connects companies and creators within the platform. All communication is tracked, searchable, and moderatable.
---
## Conversation Model
```
Conversation (thread)
├── Participants: [Company User, Creator]
├── Context: campaign_id / project_id / invitation_id (nullable)
├── Messages[]
│ ├── Message 1 (text)
│ ├── Message 2 (text + attachment)
│ └── Message 3 (system message)
└── Metadata (unread counts, last_message_at, etc.)
```
---
## Conversation Types
| Type | Context | Auto-Created |
|------|---------|-------------|
| campaign_inquiry | campaign_id | no (creator initiates) |
| application_discussion | application_id | no (either party initiates) |
| invitation_discussion | invitation_id | no (either party initiates) |
| project_thread | project_id | yes (auto-created with project) |
| direct_message | none | no (company initiates) |
---
## Conversation Fields
| Field | Type | Required | Notes |
|-------|------|----------|-------|
| uuid | uuid | auto | public ID |
| type | enum | yes | see above |
| campaign_id | FK | nullable | |
| project_id | FK | nullable | |
| application_id | FK | nullable | |
| invitation_id | FK | nullable | |
| company_id | FK | yes | |
| creator_id | FK | yes | |
| status | enum | yes | active, archived, blocked |
| last_message_at | datetime | auto | for sorting |
| created_at | datetime | auto | |
---
## Message Fields
| Field | Type | Required | Validation |
|-------|------|----------|------------|
| conversation_id | FK | yes | |
| sender_id | FK | yes | current user |
| sender_type | enum | auto | company, creator, system |
| body | text | yes (unless attachment only) | min:1, max:5000 |
| type | enum | yes | text, system, file_share |
| is_read | boolean | auto | starts false |
| read_at | datetime | nullable | when recipient reads |
| edited_at | datetime | nullable | if message edited |
| deleted_at | datetime | nullable | soft delete |
| metadata | JSON | nullable | system message data |
---
## Message Attachments
| Field | Type | Required | Validation |
|-------|------|----------|------------|
| message_id | FK | yes | |
| file_name | string | yes | original name |
| file_path | string | yes | storage path |
| file_size | integer | yes | max 25MB per file |
| mime_type | string | yes | validated |
| file_type | enum | auto | image, video, document, audio, other |
| thumbnail_path | string | nullable | for images/videos |
**Allowed attachments:**
- Images: jpg, png, webp, gif (max 10MB)
- Videos: mp4, mov, webm (max 25MB — for quick clips, not deliverables)
- Documents: pdf, doc, docx, txt, xls, xlsx (max 25MB)
- Audio: mp3, wav, aac (max 25MB)
- Max 5 attachments per message
---
## System Messages
Auto-generated messages in project threads:
```
[System] Project started — Creator began working
[System] Deliverable submitted — "Product Review Video" v1
[System] Revision requested — Company left feedback
[System] Deliverable approved — "Product Review Video"
[System] Project completed
[System] Deadline extended to March 30, 2024
```
These use type: "system" and cannot be replied to directly.
---
## Read Receipts
- When recipient opens conversation → all unread messages marked as read
- Sender sees "Read" indicator (double-check icon) with timestamp
- Bulk: opening conversation marks ALL unread in that conversation as read
---
## Real-Time Features (WebSocket)
Using Laravel Reverb:
- New message appears instantly (no page refresh)
- Typing indicator ("Company is typing...")
- Online/offline status indicator
- Unread badge updates in real-time
### WebSocket Channels:
```
private-conversation.{conversation_uuid} → messages, typing, read receipts
private-user.{user_id} → new conversation notifications, unread counts
```
---
## Messaging Rules
1. **Creator cannot message company without context** → must have: active application, active invitation, active project, OR company messaged first
2. **Company can message any creator** → direct_message type (but rate limited)
3. **Messages in project thread** → always available to both parties even after project completion (for records)
4. **Blocked user** → cannot send messages, existing messages stay visible
5. **Suspended user** → messages frozen, other party sees "user suspended" in thread
6. **Deleted account** → messages preserved but sender shown as "Deleted User"
---
## Message Actions
| Action | Who | Conditions |
|--------|-----|-----------|
| Send message | both | conversation.status = active |
| Edit message | sender | within 15 minutes of sending, not system message |
| Delete message | sender | any time, soft delete, shows "message deleted" |
| Report message | recipient | opens report flow |
| Share file | both | see attachment rules |
| React (emoji) | both | single reaction per user per message |
---
## Conversation List (Inbox)
### Sorting:
- Default: last_message_at DESC (newest activity first)
- Unread first (toggle)
### Filtering:
- All / Unread / Campaigns / Projects / Direct
- Search by participant name or message content
### Conversation Card Shows:
- Other party's avatar + name
- Context badge (Campaign: X / Project: Y)
- Last message preview (truncated 80 chars)
- Timestamp
- Unread count badge
---
## Anti-Spam & Safety
| Rule | Limit |
|------|-------|
| Messages per minute (per user) | 10 |
| Messages per hour (per user) | 100 |
| New conversations per day (company → creators) | 50 |
| Max message length | 5000 chars |
| Max attachments per message | 5 |
| Max attachment size | 25MB |
### Content Filtering:
- Phone numbers in first 3 messages → flag for review (prevents taking deals off-platform)
- External URLs in first 3 messages → flag for review
- Same message sent to 10+ different creators → flag as spam
- Messages containing known scam patterns → auto-flag
---
## Messaging Edge Cases
1. **Company sends message, creator hasn't accepted invitation yet** → allowed for invitation_discussion type
2. **Creator replies to expired invitation thread** → allowed (conversation stays open)
3. **Message with attachment but empty body** → allowed (attachment is the message)
4. **User sends message then immediately blocks other** → message delivered, then conversation blocked
5. **Project completed but parties want to discuss future work** → project thread stays open, or new direct_message conversation
6. **Message edit after other party read it** → show "(edited)" indicator, original NOT shown
7. **100+ messages in a thread** → paginate (load 50, scroll for more)
8. **User offline for days** → messages queue, show all on next login
9. **Attachment is malware** → server-side virus scan before storage (ClamAV or similar)
10. **Message in Arabic, company speaks English** → no auto-translate (future feature), both parties responsible for communication
11. **Company team member changes** → all project threads accessible to any company team member (future: when teams implemented)
# 11 — Reviews & Reputation System
## Review System
Reviews are mutual — both parties review each other after project completion.
---
## When Reviews Are Triggered
```
Project status → "completed"
→ 48-hour grace period
→ Both parties receive "Leave a review" prompt
→ Review window: 30 days after project completion
→ After 30 days: window closes, no review possible
```
---
## Review Fields (Company Reviews Creator)
| Field | Type | Required | Validation |
|-------|------|----------|------------|
| project_id | FK | yes | must be completed |
| reviewer_id | FK | auto | company user |
| reviewee_id | FK | auto | creator |
| overall_rating | integer | yes | 1-5 stars |
| communication_rating | integer | yes | 1-5 |
| professionalism_rating | integer | yes | 1-5 |
| quality_rating | integer | yes | 1-5 |
| reliability_rating | integer | yes | 1-5 (met deadlines?) |
| comment | text | no | min:20, max:1000 (if provided) |
| would_work_again | boolean | yes | |
| is_public | boolean | yes | default true |
---
## Review Fields (Creator Reviews Company)
| Field | Type | Required | Validation |
|-------|------|----------|------------|
| project_id | FK | yes | must be completed |
| reviewer_id | FK | auto | creator |
| reviewee_id | FK | auto | company user |
| overall_rating | integer | yes | 1-5 stars |
| communication_rating | integer | yes | 1-5 |
| clarity_rating | integer | yes | 1-5 (clear requirements?) |
| professionalism_rating | integer | yes | 1-5 |
| fairness_rating | integer | yes | 1-5 (fair feedback/revisions?) |
| payment_rating | integer | yes | 1-5 (paid on time?) — future |
| comment | text | no | min:20, max:1000 (if provided) |
| would_work_again | boolean | yes | |
| is_public | boolean | yes | default true |
---
## Review Rules
1. **One review per party per project** — cannot review same project twice
2. **Cannot review yourself** — enforced at DB level
3. **Review only after completion** — not during active project
4. **Cannot edit review after 24 hours** — prevents retaliation edits
5. **Private reviews** — still count toward reputation scores but comment not shown publicly
6. **Review text moderation** — flagged if contains profanity, threats, or personal info
7. **Minimum project value for review weight** — reviews from larger projects weighted slightly higher in reputation
8. **Both reviews visible simultaneously** — to prevent retaliation. Reviews hidden until BOTH parties review, OR 14 days pass (whichever first)
---
## Review Display
### On Creator Profile:
- Average overall rating (stars)
- Rating breakdown (communication, professionalism, quality, reliability)
- Total review count
- Recent reviews (company name, rating, comment, date)
- "Would work again" percentage
### On Company Profile:
- Average overall rating (stars)
- Rating breakdown (communication, clarity, professionalism, fairness)
- Total review count
- Recent reviews (creator name or anonymous, rating, comment, date)
- "Would work again" percentage
---
## Review Edge Cases
1. **Creator got 1-star but company was clearly at fault** → creator can flag review for admin review
2. **Company leaves review, creator doesn't** → company review visible after 14 days regardless
3. **Project cancelled — can they review?** → only if project was "in_progress" or later when cancelled. Not for "not_started" cancellations.
4. **Revenge review (1-star with no substance)** → admin can hide reviews that violate guidelines
5. **Company fires creator mid-project, then leaves bad review** → allowed (cancellation reviews have different weight)
6. **Creator has 100 reviews, new bad one barely affects average** → weighted average favoring recent reviews
7. **Review contains competitor mentions** → no blocking but flag for review
8. **Fake reviews (company creates fake projects to boost creator)** → pattern detection: same IP, rapid project cycles, admin flagged
---
## Reputation System
Reputation is a calculated composite score based on multiple signals. It's NOT just reviews.
---
## Creator Reputation Score (0-100)
| Factor | Weight | Calculation |
|--------|--------|-------------|
| Review average | 30% | (avg_rating / 5) × 100 |
| Completion rate | 25% | completed_projects / total_projects × 100 |
| On-time delivery rate | 15% | on_time_projects / completed_projects × 100 |
| Response rate | 10% | messages_responded_within_24h / total_received × 100 |
| Response time | 5% | inverse of avg response time (faster = higher) |
| Profile completeness | 5% | profile_completion_percentage |
| Account age | 5% | logarithmic (diminishing returns after 1 year) |
| Revision rate | 5% | lower is better (fewer revisions needed = higher quality) |
### Reputation Tiers:
| Score | Tier | Badge |
|-------|------|-------|
| 0-39 | New | — (no badge) |
| 40-59 | Rising | Bronze |
| 60-79 | Established | Silver |
| 80-89 | Top Rated | Gold |
| 90-100 | Elite | Diamond |
---
## Company Reputation Score (0-100)
| Factor | Weight | Calculation |
|--------|--------|-------------|
| Review average | 35% | from creator reviews |
| Project completion rate | 20% | completed / total (not cancelled) |
| Response time to applications | 15% | avg time to first action on applications |
| Payment reliability | 15% | future: % paid on time |
| Account age + volume | 10% | projects completed × time factor |
| Dispute rate | 5% | lower = better |
### Company Badges:
| Condition | Badge |
|-----------|-------|
| Verified + 80+ reputation | "Trusted Employer" |
| 10+ completed projects | "Active Employer" |
| Avg response < 24h | "Quick Responder" |
| 0 cancelled projects | "Reliable" |
---
## Reputation Decay
- If creator inactive for 90+ days → score slowly decays (1 point/month)
- If company hasn't posted in 6+ months → remove "Active" badges
- Decay stops at the score floor (never below their actual performance data)
---
## Reputation Edge Cases
1. **New creator has 0 reviews** → show "New" badge, score starts at 50 (neutral)
2. **Creator has 1 review (5 stars)** → show rating but note "1 review" (don't display as "perfect")
3. **Creator's only project was cancelled by company** → don't penalize creator's completion rate for company-initiated cancellations
4. **Gaming the system: rapid small projects for reviews** → weight by project value/complexity
5. **Creator inactive but has great history** → preserve score (decay is slow), show "last active: X ago"
6. **Company with 50 projects, 2 bad reviews** → weighted correctly, 2 bad out of 50 = minor impact
7. **Score calculation timing** → recalculate on every relevant event (review, completion, etc.), cache result
8. **Admin overrides** → admin can manually adjust reputation with reason (logged in audit)
# 12 — Notifications System
## Notification Channels
| Channel | Description | Delivery Speed |
|---------|-------------|---------------|
| In-App | Bell icon badge, dropdown list | Instant (WebSocket) |
| Email | Full email with action buttons | Near-instant (queued) |
| Push (future) | Browser/mobile push notifications | Instant |
---
## Notification Model
| Field | Type | Notes |
|-------|------|-------|
| id | bigint | auto |
| user_id | FK | recipient |
| type | string | class name / event type |
| title | string | short title |
| body | text | message content |
| action_url | string | where to go when clicked |
| action_label | string | "View Application", "Review Deliverable", etc. |
| icon | string | icon identifier |
| category | enum | campaigns, projects, messages, applications, invitations, reviews, system |
| is_read | boolean | default false |
| read_at | datetime | nullable |
| emailed | boolean | whether email was sent |
| data | JSON | additional context data |
| created_at | datetime | |
---
## Notification Events (Complete List)
### Campaign Events
| Event | Recipient | Title Template | Email? |
|-------|-----------|---------------|--------|
| campaign_published | matching creators | "New campaign matches your profile" | yes |
| campaign_updated | applicants | "Campaign '{title}' was updated" | yes (if major) |
| campaign_paused | applicants | "Campaign '{title}' is paused" | yes |
| campaign_closed | applicants not yet accepted | "Campaign '{title}' is no longer accepting applications" | yes |
| campaign_cancelled | all related | "Campaign '{title}' was cancelled" | yes |
| campaign_deadline_24h | applicants | "Campaign '{title}' closes in 24 hours" | yes |
### Application Events
| Event | Recipient | Title Template | Email? |
|-------|-----------|---------------|--------|
| application_received | company | "New application from {creator_name}" | yes |
| application_viewed | creator | "Your application was viewed by {company_name}" | no (in-app only) |
| application_shortlisted | creator | "You've been shortlisted for '{campaign_title}'" | yes |
| application_accepted | creator | "You've been accepted for '{campaign_title}'" | yes |
| application_rejected | creator | "Update on your application to '{campaign_title}'" | yes |
| application_withdrawn | company | "{creator_name} withdrew their application" | no |
### Invitation Events
| Event | Recipient | Title Template | Email? |
|-------|-----------|---------------|--------|
| invitation_received | creator | "New invitation from {company_name}" | yes |
| invitation_accepted | company | "{creator_name} accepted your invitation" | yes |
| invitation_declined | company | "{creator_name} declined your invitation" | yes |
| invitation_expiring_24h | creator | "Invitation from {company_name} expires tomorrow" | yes |
| invitation_expired | company | "Your invitation to {creator_name} expired" | no |
### Project Events
| Event | Recipient | Title Template | Email? |
|-------|-----------|---------------|--------|
| project_created | both | "New project: '{title}' started" | yes |
| project_started | company | "{creator_name} started working" | no |
| project_deadline_3d | both | "Project '{title}' due in 3 days" | yes |
| project_deadline_24h | both | "Project '{title}' due tomorrow" | yes |
| project_overdue | both | "Project '{title}' is overdue" | yes |
| project_completed | both | "Project '{title}' completed!" | yes |
| project_cancelled | other party | "Project '{title}' was cancelled" | yes |
| project_disputed | both + admin | "Dispute opened on '{title}'" | yes |
| deadline_extension_requested | company | "{creator_name} requested deadline extension" | yes |
| deadline_extension_approved | creator | "Deadline extended to {new_date}" | yes |
| deadline_extension_rejected | creator | "Deadline extension was declined" | yes |
### Deliverable Events
| Event | Recipient | Title Template | Email? |
|-------|-----------|---------------|--------|
| deliverable_submitted | company | "{creator_name} submitted '{deliverable_title}' (v{n})" | yes |
| deliverable_approved | creator | "'{deliverable_title}' was approved!" | yes |
| revision_requested | creator | "Revision requested for '{deliverable_title}'" | yes |
| all_deliverables_approved | both | "All deliverables approved for '{project_title}'" | yes |
### Message Events
| Event | Recipient | Title Template | Email? |
|-------|-----------|---------------|--------|
| new_message | other party | "New message from {sender_name}" | yes (if offline 5min+) |
| message_attachment | other party | "{sender_name} shared a file" | no |
### Review Events
| Event | Recipient | Title Template | Email? |
|-------|-----------|---------------|--------|
| review_prompt | both | "Leave a review for '{project_title}'" | yes |
| review_received | reviewee | "You received a new review" | yes |
| review_reminder_7d | non-reviewer | "Reminder: review pending for '{project_title}'" | yes |
### System Events
| Event | Recipient | Title Template | Email? |
|-------|-----------|---------------|--------|
| account_verified | user | "Your email has been verified" | no |
| company_approved | company | "Your company has been approved!" | yes |
| company_rejected | company | "Your company application needs attention" | yes |
| account_suspended | user | "Your account has been suspended" | yes |
| account_reactivated | user | "Your account is active again" | yes |
| profile_incomplete_reminder | creator | "Complete your profile to start applying" | yes (after 7 days) |
| security_login_new_device | user | "New login from {device} in {location}" | yes |
---
## Notification Preferences
Users can control which notifications they receive and via which channel:
| Category | In-App | Email | Default |
|----------|--------|-------|---------|
| Applications | always | toggleable | both on |
| Invitations | always | toggleable | both on |
| Projects | always | toggleable | both on |
| Deliverables | always | toggleable | both on |
| Messages | always | toggleable | email on |
| Campaigns (recommendations) | toggleable | toggleable | in-app only |
| Reviews | always | toggleable | both on |
| Security | always | always | both (non-negotiable) |
| Marketing | toggleable | toggleable | off |
**Rules:**
- Security notifications CANNOT be disabled
- In-app notifications always fire (but user can mute categories from appearing in dropdown)
- Email digest option: instead of individual emails, receive daily/weekly summary
---
## Email Templates
All emails follow consistent template:
```
Header: UGC Heaven logo
Subject: [UGC Heaven] {notification_title}
Body:
- Greeting: "Hi {first_name},"
- Context: what happened
- Action button: primary CTA
- Secondary text: additional details
Footer:
- Unsubscribe link (per category)
- "Manage notification settings"
- Company info
```
### Email Languages:
- Detect user's language preference
- Send in their preferred language (ar or en)
- RTL layout for Arabic emails
---
## Notification Batching
To prevent email fatigue:
- If 5+ notifications of same type fire within 10 minutes → batch into one email
- Example: "You received 7 new applications" instead of 7 separate emails
- Messages: only email if user hasn't been online for 5+ minutes (they might see it in-app)
- Never more than 10 emails per hour per user (hard cap)
- Digest mode: aggregate into daily email at user's preferred time
---
## Notification Edge Cases
1. **User has 500 unread notifications** → show count as "99+" in badge, paginate list
2. **Notification for deleted entity** → action_url leads to 404 → show "This item no longer exists" gracefully
3. **Notification sent, user changes email** → already-sent emails go to old address (correct behavior)
4. **User marks all as read** → single action, bulk update
5. **Same notification fires twice (race condition)** → deduplicate by type + user + entity_id within 1 minute window
6. **Email bounce** → track bounces, after 3 bounces disable email notifications and warn user
7. **User in different timezone** → all timestamps shown in their timezone
8. **Notification for project between two users, both get notified** → separate notification records, same event
9. **Admin notification (internal)** → separate notification channel for admin alerts (disputes, reports, etc.)
10. **Push notification permission denied** → graceful degradation to in-app + email only
# 13 — SuperAdmin Panel
## Access
- URL: `/admin/*`
- Only users with role `superadmin` can access
- Middleware: `auth` + `role:superadmin`
- No public registration for admin — seeded or created by existing admin only
- Admin sessions: 2-hour timeout, re-authenticate for sensitive actions
---
## Dashboard (Overview)
### Key Metrics Cards:
- Total Users (creators + companies)
- New Users (this week)
- Active Campaigns
- Active Projects
- Pending Company Approvals
- Open Disputes
- Open Reports
- Revenue (future: when payments integrated)
### Charts:
- User growth (line chart, last 30 days / 12 months)
- Campaign creation rate
- Project completion rate
- Creator-to-company ratio
- Top categories/niches
### Quick Actions:
- Pending approvals (N)
- Open reports (N)
- Open disputes (N)
- Flagged content (N)
---
## User Management
### User List View:
- Table with: avatar, name, email, role, status, joined date, last active
- Filters: role, status, country, joined date range, verified/unverified
- Search: by name, email, username
- Bulk actions: suspend, ban, export CSV
### User Detail View:
- Full profile information
- Account status + history of status changes
- Activity log (last 100 actions)
- Active sessions (IP, device, location)
- Projects (as creator or company)
- Reviews given/received
- Reports (filed by them, filed against them)
- Messages (searchable, for moderation)
- Reputation score breakdown
- Financial history (future)
### User Actions:
| Action | Requires | Confirmation | Reversible |
|--------|----------|-------------|------------|
| Verify email manually | — | yes | no |
| Change role | reason | yes | yes |
| Suspend | reason + duration | yes | yes (auto or manual) |
| Ban | reason | double confirm | yes (by admin only) |
| Delete (soft) | reason | double confirm | yes (within 30 days) |
| Delete (hard) | reason | triple confirm + password | NO |
| Impersonate | — | yes + logged | yes (end impersonation) |
| Force logout | — | yes | N/A |
| Reset password | — | yes (sends email) | N/A |
| Merge accounts | select accounts | yes | NO |
### Impersonation:
- Admin sees the platform exactly as that user does
- Yellow banner at top: "Viewing as {username} — End Impersonation"
- All actions logged as "performed by admin via impersonation"
- Cannot impersonate another admin
- Cannot perform destructive actions while impersonating (safety)
---
## Company Management
### Company Queue:
- **Pending Review** tab: new registrations awaiting approval
- Show: company name, industry, website, country, registered date
- Actions: approve, reject (with reason), request more info
- **Active** tab: all approved companies
- **Suspended** tab: temporarily suspended
- **Rejected** tab: rejected applications (can be re-reviewed)
### Company Detail View:
- All profile fields
- Campaigns posted (with stats)
- Projects (with outcomes)
- Creators worked with
- Reviews received
- Reports against them
- Activity timeline
### Company Actions:
| Action | Effect |
|--------|--------|
| Approve | Company activated, can use platform |
| Reject | Company notified with reason, can re-apply |
| Suspend | All campaigns paused, active projects flagged |
| Ban | Everything terminated |
| Add verification badge | Trust tier upgraded |
| Remove verification | Trust tier downgraded |
| Feature company | Shown in "featured companies" section |
---
## Creator Management
### Creator List:
- Same as user management but filtered to creators
- Additional filters: niches, skills, reputation score range, portfolio count
- Additional columns: reputation score, completed projects, review avg
### Creator Actions:
| Action | Effect |
|--------|--------|
| Verify (Pro badge) | Manual verification, trust badge added |
| Remove verification | Badge removed |
| Feature creator | Shown in "featured creators" section |
| Hide from search | Invisible to companies (but profile still accessible via link) |
| Flag portfolio | Portfolio items hidden pending review |
---
## Campaign Management
### Campaign List:
- All campaigns, all statuses
- Filters: status, company, category, date range, budget range
- Flags: reported campaigns, campaigns with 0 applications
### Campaign Actions:
| Action | Effect |
|--------|--------|
| Feature | Shown at top of discover page |
| Unfeature | Removed from featured |
| Pause | Hidden from search, applicants notified |
| Unpause | Restored to active |
| Delete | Soft delete, all applicants notified |
| Edit | Admin can edit any campaign field |
---
## Project Management
### Project List:
- All projects, all statuses
- Filters: status, company, creator, date range, overdue, disputed
- Priority view: overdue + disputed projects at top
### Project Actions:
| Action | Effect |
|--------|--------|
| Force complete | Override status to completed |
| Force cancel | Cancel with admin reason |
| Extend deadline | Override deadline |
| Reassign (future) | Change creator (extreme edge case) |
| Add admin note | Internal note not visible to parties |
| Resolve dispute | Close dispute with resolution |
---
## Dispute Resolution
### Dispute Queue:
- Open disputes, sorted by age (oldest first)
- Show: project title, parties, reason, opened date
### Dispute Detail:
- Full project history
- All messages between parties
- All deliverable submissions + reviews
- Dispute reason (from opening party)
- Response from other party
- Admin notes
### Resolution Options:
| Resolution | Effect |
|-----------|--------|
| Side with creator | Project completed, creator not penalized |
| Side with company | Project cancelled, company not penalized |
| Mutual fault | Both parties penalized equally |
| No fault | Cancelled, no penalties |
| Custom resolution | Admin writes custom resolution, both notified |
---
## Moderation
### Content Queue:
Items flagged for review (auto-flagged or user-reported):
- Portfolio videos
- Profile images
- Campaign descriptions
- Messages (if reported)
- Reviews (if reported)
### Moderation Actions:
| Action | Effect |
|--------|--------|
| Approve | Remove flag, content visible |
| Remove | Content hidden, creator/company notified |
| Remove + warn | Content hidden, warning sent to user |
| Remove + suspend | Content hidden, user suspended |
| Escalate | Move to senior admin / legal |
### Auto-Moderation Rules (Future):
- Profile pictures: nudity detection
- Text content: profanity filter, phone number detection, email detection
- Videos: too short / corrupted detection
- Spam detection: same content posted repeatedly
---
## Reports Management
### Report Queue:
- All reports, unresolved first
- Show: reporter, reported entity, type, reason, date
### Report Types:
| Report Target | Reasons |
|--------------|---------|
| User | scam, harassment, spam, fake_profile, underage, impersonation |
| Campaign | scam, misleading, inappropriate, discriminatory |
| Message | harassment, spam, threats, inappropriate |
| Review | fake, harassment, irrelevant, personal_info |
| Portfolio | copyright, inappropriate, misleading |
### Report Resolution:
| Action | Effect |
|--------|--------|
| Valid — action taken | Content removed/user suspended, reporter notified |
| Valid — already resolved | No new action needed |
| Invalid | Report dismissed, reporter notified |
| Needs investigation | Assigned to admin for deeper look |
| Duplicate | Merged with existing report |
---
## Analytics Dashboard
### Platform Health:
- DAU/WAU/MAU
- Session duration
- Retention (7d, 30d)
- Funnel: register → complete profile → first application/campaign → first project → first completion
### Marketplace Health:
- Supply/demand ratio (creators vs. campaigns)
- Average time from campaign publish to first application
- Average time from application to project start
- Average project duration
- Completion rate
- Dispute rate
- Average review scores (trending)
### Geographic:
- Users by country (heatmap)
- Campaigns by country
- Cross-border projects
### Export:
- All analytics exportable as CSV
- Date range selection
- Scheduled reports (weekly email to admin)
---
## System Settings
### General:
- Platform name, logo, favicon
- Default language
- Supported languages
- Maintenance mode toggle
- Registration open/closed toggle
### Limits:
- Max file upload sizes
- Max portfolio items per creator
- Max campaigns per company (simultaneous)
- Rate limiting configuration
- Session duration
### Email:
- SMTP configuration
- Email templates preview/edit
- Test email sender
### Integrations:
- PeerTube connection status
- Redis connection status
- S3/storage status
- Webhook URLs (future)
---
## Audit Log (Admin Actions)
Every admin action logged:
| Field | Type |
|-------|------|
| admin_id | FK |
| action | string |
| target_type | string (user, campaign, project, etc.) |
| target_id | FK |
| details | JSON (what changed) |
| ip_address | string |
| user_agent | string |
| created_at | datetime |
**Retention:** audit logs kept for 2 years minimum, never auto-deleted.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
<?php
return [
'failed' => 'بيانات الاعتماد هذه لا تتطابق مع سجلاتنا.',
'password' => 'كلمة المرور المقدمة غير صحيحة.',
'throttle' => 'عدد كبير جدًا من محاولات تسجيل الدخول. يرجى المحاولة مرة أخرى خلال :seconds ثانية.',
'unauthorized_role' => 'ليس لديك صلاحية للوصول إلى هذا المورد.',
'login' => 'تسجيل الدخول',
'register' => 'تسجيل',
'logout' => 'تسجيل الخروج',
'email' => 'البريد الإلكتروني',
'password_label' => 'كلمة المرور',
'remember_me' => 'تذكرني',
'forgot_password' => 'نسيت كلمة المرور؟',
'reset_password' => 'إعادة تعيين كلمة المرور',
'confirm_password' => 'تأكيد كلمة المرور',
'already_registered' => 'لديك حساب بالفعل؟',
'verify_email' => 'تحقق من بريدك الإلكتروني',
'verification_sent' => 'تم إرسال رابط التحقق إلى بريدك الإلكتروني.',
'check_email' => 'قبل المتابعة، يرجى التحقق من بريدك الإلكتروني.',
'not_receive' => 'إذا لم تستلم البريد الإلكتروني',
'request_another' => 'اضغط هنا لطلب رابط آخر',
];
<?php
return [
'not_approved' => 'حساب شركتك قيد المراجعة.',
];
<?php
return [
'incomplete' => 'يرجى إكمال ملفك الشخصي للوصول إلى هذه الميزة.',
];
<?php
return [
'skip_to_content' => 'انتقل إلى المحتوى',
'loading' => 'جاري التحميل...',
'save' => 'حفظ',
'cancel' => 'إلغاء',
'delete' => 'حذف',
'edit' => 'تعديل',
'create' => 'إنشاء',
'search' => 'بحث',
'filter' => 'تصفية',
'sort' => 'ترتيب',
'back' => 'رجوع',
'next' => 'التالي',
'previous' => 'السابق',
'confirm' => 'تأكيد',
'close' => 'إغلاق',
'view' => 'عرض',
'download' => 'تحميل',
'upload' => 'رفع',
'submit' => 'إرسال',
'reset' => 'إعادة تعيين',
'no_results' => 'لا توجد نتائج',
'showing' => 'عرض :from إلى :to من :total نتيجة',
];
<?php
return [
'failed' => 'These credentials do not match our records.',
'password' => 'The provided password is incorrect.',
'throttle' => 'Too many login attempts. Please try again in :seconds seconds.',
'unauthorized_role' => 'You do not have permission to access this resource.',
'login' => 'Login',
'register' => 'Register',
'logout' => 'Logout',
'email' => 'Email',
'password_label' => 'Password',
'remember_me' => 'Remember me',
'forgot_password' => 'Forgot your password?',
'reset_password' => 'Reset Password',
'confirm_password' => 'Confirm Password',
'already_registered' => 'Already registered?',
'verify_email' => 'Verify your email address',
'verification_sent' => 'A verification link has been sent to your email address.',
'check_email' => 'Before proceeding, please check your email for a verification link.',
'not_receive' => 'If you did not receive the email',
'request_another' => 'click here to request another',
];
<?php
return [
'not_approved' => 'Your company account is pending approval.',
];
<?php
return [
'incomplete' => 'Please complete your profile to access this feature.',
];
<?php
return [
'skip_to_content' => 'Skip to content',
'loading' => 'Loading...',
'save' => 'Save',
'cancel' => 'Cancel',
'delete' => 'Delete',
'edit' => 'Edit',
'create' => 'Create',
'search' => 'Search',
'filter' => 'Filter',
'sort' => 'Sort',
'back' => 'Back',
'next' => 'Next',
'previous' => 'Previous',
'confirm' => 'Confirm',
'close' => 'Close',
'view' => 'View',
'download' => 'Download',
'upload' => 'Upload',
'submit' => 'Submit',
'reset' => 'Reset',
'no_results' => 'No results found',
'showing' => 'Showing :from to :to of :total results',
];
This diff is collapsed.
{
"private": true,
"type": "module",
"scripts": {
"build": "vite build",
"dev": "vite"
},
"devDependencies": {
"autoprefixer": "^10.4.20",
"axios": "^1.7.4",
"laravel-vite-plugin": "^1.2.0",
"postcss": "^8.4.47",
"tailwindcss": "^3.4.13",
"vite": "^6.0.11"
},
"dependencies": {
"alpinejs": "^3.14.0",
"lucide": "^0.460.0"
}
}
This diff is collapsed.
This diff is collapsed.
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};
This diff is collapsed.
<?php
use Illuminate\Http\Request;
define('LARAVEL_START', microtime(true));
// Determine if the application is in maintenance mode...
if (file_exists($maintenance = __DIR__.'/../storage/framework/maintenance.php')) {
require $maintenance;
}
// Register the Composer autoloader...
require __DIR__.'/../vendor/autoload.php';
// Bootstrap Laravel and handle the request...
(require_once __DIR__.'/../bootstrap/app.php')
->handleRequest(Request::capture());
User-agent: *
Disallow:
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
*
!private/
!public/
!.gitignore
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment