3266909346
Audit J5/D4/D5: the outbound_webhook_subscriptions table existed in schema but had zero code. Adds the OutboundWebhookSubscription model + factory and WebhookSettingsController with GET/PUT /api/tenants/me/webhook-settings (one subscription per tenant; secret generated + returned once on creation, bcrypt-hashed) and POST /api/webhooks/test (unsigned connectivity check — HMAC-signed event delivery is a separate post-MVP epic). Tenant-scoped via auth:sanctum + tenant middleware. phpstan-baseline.neon: additive-only entries for new test file (Pest\PendingCalls\TestCall false-positives — documented project pattern) and OutboundWebhookSubscriptionFactory method.childReturnType (same pattern as ProjectFactory/TenantFactory/UserFactory already in baseline). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
73 lines
2.1 KiB
PHP
73 lines
2.1 KiB
PHP
<?php
|
||
|
||
declare(strict_types=1);
|
||
|
||
namespace App\Models;
|
||
|
||
use Database\Factories\OutboundWebhookSubscriptionFactory;
|
||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||
use Illuminate\Database\Eloquent\Model;
|
||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||
|
||
/**
|
||
* Исходящая webhook-подписка тенанта (таблица outbound_webhook_subscriptions).
|
||
*
|
||
* Tenant-aware, RLS на уровне БД.
|
||
*
|
||
* secret_hash — bcrypt-хэш; оригинал секрета показывается ОДИН раз при
|
||
* создании. events — JSONB-массив, CHECK требует ≥1 элемента.
|
||
*
|
||
* NB: outbound-доставка событий (подписанные webhook'и) — пост-MVP; пока
|
||
* подписка хранит URL + секрет, а WebhookSettingsController::test делает
|
||
* unsigned connectivity-проверку.
|
||
*
|
||
* @mixin IdeHelperOutboundWebhookSubscription
|
||
*/
|
||
class OutboundWebhookSubscription extends Model
|
||
{
|
||
/** @use HasFactory<OutboundWebhookSubscriptionFactory> */
|
||
use HasFactory;
|
||
|
||
protected $fillable = [
|
||
'tenant_id',
|
||
'user_id',
|
||
'name',
|
||
'target_url',
|
||
'secret_hash',
|
||
'secret_prefix',
|
||
'events',
|
||
'custom_headers',
|
||
'is_active',
|
||
'paused_at',
|
||
];
|
||
|
||
protected $hidden = ['secret_hash'];
|
||
|
||
protected function casts(): array
|
||
{
|
||
return [
|
||
'events' => 'array',
|
||
'custom_headers' => 'array',
|
||
'is_active' => 'boolean',
|
||
'consecutive_failures' => 'integer',
|
||
'paused_at' => 'datetime',
|
||
'last_delivery_at' => 'datetime',
|
||
'last_failure_at' => 'datetime',
|
||
'created_at' => 'datetime',
|
||
'updated_at' => 'datetime',
|
||
];
|
||
}
|
||
|
||
/** @return BelongsTo<Tenant, $this> */
|
||
public function tenant(): BelongsTo
|
||
{
|
||
return $this->belongsTo(Tenant::class);
|
||
}
|
||
|
||
/** @return BelongsTo<User, $this> */
|
||
public function user(): BelongsTo
|
||
{
|
||
return $this->belongsTo(User::class);
|
||
}
|
||
}
|