feat: implement AI styling foundation and billing scope rework
This commit is contained in:
51
app/Models/AiEditOutput.php
Normal file
51
app/Models/AiEditOutput.php
Normal file
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class AiEditOutput extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'request_id',
|
||||
'photo_id',
|
||||
'storage_disk',
|
||||
'storage_path',
|
||||
'mime_type',
|
||||
'width',
|
||||
'height',
|
||||
'bytes',
|
||||
'checksum',
|
||||
'provider_asset_id',
|
||||
'provider_url',
|
||||
'is_primary',
|
||||
'safety_state',
|
||||
'safety_reasons',
|
||||
'generated_at',
|
||||
'metadata',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'is_primary' => 'boolean',
|
||||
'safety_reasons' => 'array',
|
||||
'metadata' => 'array',
|
||||
'generated_at' => 'datetime',
|
||||
];
|
||||
}
|
||||
|
||||
public function request(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(AiEditRequest::class, 'request_id');
|
||||
}
|
||||
|
||||
public function photo(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Photo::class);
|
||||
}
|
||||
}
|
||||
103
app/Models/AiEditRequest.php
Normal file
103
app/Models/AiEditRequest.php
Normal file
@@ -0,0 +1,103 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
class AiEditRequest extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
public const STATUS_QUEUED = 'queued';
|
||||
|
||||
public const STATUS_PROCESSING = 'processing';
|
||||
|
||||
public const STATUS_SUCCEEDED = 'succeeded';
|
||||
|
||||
public const STATUS_FAILED = 'failed';
|
||||
|
||||
public const STATUS_BLOCKED = 'blocked';
|
||||
|
||||
public const STATUS_CANCELED = 'canceled';
|
||||
|
||||
protected $fillable = [
|
||||
'tenant_id',
|
||||
'event_id',
|
||||
'photo_id',
|
||||
'style_id',
|
||||
'requested_by_user_id',
|
||||
'provider',
|
||||
'provider_model',
|
||||
'status',
|
||||
'safety_state',
|
||||
'prompt',
|
||||
'negative_prompt',
|
||||
'input_image_path',
|
||||
'requested_by_device_id',
|
||||
'requested_by_session_id',
|
||||
'idempotency_key',
|
||||
'safety_reasons',
|
||||
'failure_code',
|
||||
'failure_message',
|
||||
'queued_at',
|
||||
'started_at',
|
||||
'completed_at',
|
||||
'expires_at',
|
||||
'metadata',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'safety_reasons' => 'array',
|
||||
'metadata' => 'array',
|
||||
'queued_at' => 'datetime',
|
||||
'started_at' => 'datetime',
|
||||
'completed_at' => 'datetime',
|
||||
'expires_at' => 'datetime',
|
||||
];
|
||||
}
|
||||
|
||||
public function tenant(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Tenant::class);
|
||||
}
|
||||
|
||||
public function event(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Event::class);
|
||||
}
|
||||
|
||||
public function photo(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Photo::class);
|
||||
}
|
||||
|
||||
public function style(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(AiStyle::class, 'style_id');
|
||||
}
|
||||
|
||||
public function requestedBy(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'requested_by_user_id');
|
||||
}
|
||||
|
||||
public function outputs(): HasMany
|
||||
{
|
||||
return $this->hasMany(AiEditOutput::class, 'request_id');
|
||||
}
|
||||
|
||||
public function providerRuns(): HasMany
|
||||
{
|
||||
return $this->hasMany(AiProviderRun::class, 'request_id');
|
||||
}
|
||||
|
||||
public function usageLedgers(): HasMany
|
||||
{
|
||||
return $this->hasMany(AiUsageLedger::class, 'request_id');
|
||||
}
|
||||
}
|
||||
63
app/Models/AiEditingSetting.php
Normal file
63
app/Models/AiEditingSetting.php
Normal file
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Throwable;
|
||||
|
||||
class AiEditingSetting extends Model
|
||||
{
|
||||
protected $guarded = [];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'is_enabled' => 'boolean',
|
||||
'queue_auto_dispatch' => 'boolean',
|
||||
'queue_max_polls' => 'integer',
|
||||
'blocked_terms' => 'array',
|
||||
];
|
||||
}
|
||||
|
||||
protected static function booted(): void
|
||||
{
|
||||
static::saved(fn () => static::flushCache());
|
||||
static::deleted(fn () => static::flushCache());
|
||||
}
|
||||
|
||||
public static function current(): self
|
||||
{
|
||||
/** @var self */
|
||||
return Cache::remember('ai_editing.settings', now()->addMinutes(10), static function (): self {
|
||||
try {
|
||||
return static::query()->firstOrCreate(['id' => 1], static::defaults());
|
||||
} catch (Throwable) {
|
||||
return new static(static::defaults());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public static function defaults(): array
|
||||
{
|
||||
return [
|
||||
'is_enabled' => true,
|
||||
'default_provider' => (string) config('ai-editing.default_provider', 'runware'),
|
||||
'fallback_provider' => null,
|
||||
'runware_mode' => (string) config('ai-editing.providers.runware.mode', 'live'),
|
||||
'queue_auto_dispatch' => (bool) config('ai-editing.queue.auto_dispatch', false),
|
||||
'queue_name' => (string) config('ai-editing.queue.name', 'default'),
|
||||
'queue_max_polls' => max(1, (int) config('ai-editing.queue.max_polls', 6)),
|
||||
'blocked_terms' => array_values(array_filter((array) config('ai-editing.safety.prompt.blocked_terms', []))),
|
||||
'status_message' => null,
|
||||
];
|
||||
}
|
||||
|
||||
public static function flushCache(): void
|
||||
{
|
||||
Cache::forget('ai_editing.settings');
|
||||
}
|
||||
}
|
||||
56
app/Models/AiProviderRun.php
Normal file
56
app/Models/AiProviderRun.php
Normal file
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class AiProviderRun extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
public const STATUS_PENDING = 'pending';
|
||||
|
||||
public const STATUS_RUNNING = 'running';
|
||||
|
||||
public const STATUS_SUCCEEDED = 'succeeded';
|
||||
|
||||
public const STATUS_FAILED = 'failed';
|
||||
|
||||
protected $fillable = [
|
||||
'request_id',
|
||||
'provider',
|
||||
'attempt',
|
||||
'provider_task_id',
|
||||
'status',
|
||||
'http_status',
|
||||
'started_at',
|
||||
'finished_at',
|
||||
'duration_ms',
|
||||
'cost_usd',
|
||||
'tokens_input',
|
||||
'tokens_output',
|
||||
'request_payload',
|
||||
'response_payload',
|
||||
'error_message',
|
||||
'metadata',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'request_payload' => 'array',
|
||||
'response_payload' => 'array',
|
||||
'metadata' => 'array',
|
||||
'cost_usd' => 'decimal:5',
|
||||
'started_at' => 'datetime',
|
||||
'finished_at' => 'datetime',
|
||||
];
|
||||
}
|
||||
|
||||
public function request(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(AiEditRequest::class, 'request_id');
|
||||
}
|
||||
}
|
||||
44
app/Models/AiStyle.php
Normal file
44
app/Models/AiStyle.php
Normal file
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
class AiStyle extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'key',
|
||||
'name',
|
||||
'category',
|
||||
'description',
|
||||
'prompt_template',
|
||||
'negative_prompt_template',
|
||||
'provider',
|
||||
'provider_model',
|
||||
'requires_source_image',
|
||||
'is_premium',
|
||||
'is_active',
|
||||
'sort',
|
||||
'metadata',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'requires_source_image' => 'boolean',
|
||||
'is_premium' => 'boolean',
|
||||
'is_active' => 'boolean',
|
||||
'sort' => 'integer',
|
||||
'metadata' => 'array',
|
||||
];
|
||||
}
|
||||
|
||||
public function editRequests(): HasMany
|
||||
{
|
||||
return $this->hasMany(AiEditRequest::class, 'style_id');
|
||||
}
|
||||
}
|
||||
60
app/Models/AiUsageLedger.php
Normal file
60
app/Models/AiUsageLedger.php
Normal file
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class AiUsageLedger extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
public const TYPE_DEBIT = 'debit';
|
||||
|
||||
public const TYPE_CREDIT = 'credit';
|
||||
|
||||
public const TYPE_REFUND = 'refund';
|
||||
|
||||
public const TYPE_ADJUSTMENT = 'adjustment';
|
||||
|
||||
protected $fillable = [
|
||||
'tenant_id',
|
||||
'event_id',
|
||||
'request_id',
|
||||
'entry_type',
|
||||
'quantity',
|
||||
'unit_cost_usd',
|
||||
'amount_usd',
|
||||
'currency',
|
||||
'package_context',
|
||||
'notes',
|
||||
'recorded_at',
|
||||
'metadata',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'unit_cost_usd' => 'decimal:5',
|
||||
'amount_usd' => 'decimal:5',
|
||||
'recorded_at' => 'datetime',
|
||||
'metadata' => 'array',
|
||||
];
|
||||
}
|
||||
|
||||
public function tenant(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Tenant::class);
|
||||
}
|
||||
|
||||
public function event(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Event::class);
|
||||
}
|
||||
|
||||
public function request(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(AiEditRequest::class, 'request_id');
|
||||
}
|
||||
}
|
||||
@@ -88,6 +88,11 @@ class Event extends Model
|
||||
return $this->hasMany(Photo::class);
|
||||
}
|
||||
|
||||
public function aiEditRequests(): HasMany
|
||||
{
|
||||
return $this->hasMany(AiEditRequest::class);
|
||||
}
|
||||
|
||||
public function taskCollections(): BelongsToMany
|
||||
{
|
||||
return $this->belongsToMany(
|
||||
|
||||
@@ -149,6 +149,11 @@ class Photo extends Model
|
||||
return $this->hasMany(PhotoShareLink::class);
|
||||
}
|
||||
|
||||
public function aiEditRequests(): HasMany
|
||||
{
|
||||
return $this->hasMany(AiEditRequest::class);
|
||||
}
|
||||
|
||||
public static function supportsFilenameColumn(): bool
|
||||
{
|
||||
return static::hasColumn('filename');
|
||||
|
||||
Reference in New Issue
Block a user