TenantAtlas/apps/platform/app/Services/Alerts/AlertDestinationTestMessageService.php
ahmido ce0615a9c1 Spec 182: relocate Laravel platform to apps/platform (#213)
## Summary
- move the Laravel application into `apps/platform` and keep the repository root for orchestration, docs, and tooling
- update the local command model, Sail/Docker wiring, runtime paths, and ignore rules around the new platform location
- add relocation quickstart/contracts plus focused smoke coverage for bootstrap, command model, routes, and runtime behavior

## Validation
- `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/PlatformRelocation`
- integrated browser smoke validated `/up`, `/`, `/admin`, `/admin/choose-workspace`, and tenant route semantics for `200`, `403`, and `404`

## Remaining Rollout Checks
- validate Dokploy build context and working-directory assumptions against the new `apps/platform` layout
- confirm web, queue, and scheduler processes all start from the expected working directory in staging/production
- verify no legacy volume mounts or asset-publish paths still point at the old root-level `public/` or `storage/` locations

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #213
2026-04-08 08:40:47 +00:00

116 lines
3.7 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Services\Alerts;
use App\Jobs\Alerts\DeliverAlertsJob;
use App\Models\AlertDelivery;
use App\Models\AlertDestination;
use App\Models\User;
use App\Services\Audit\WorkspaceAuditLogger;
use App\Support\Audit\AuditActionId;
use Illuminate\Auth\Access\AuthorizationException;
class AlertDestinationTestMessageService
{
private const int RATE_LIMIT_SECONDS = 60;
public function __construct(
private WorkspaceAuditLogger $auditLogger,
) {}
/**
* Send a test message for the given alert destination.
*
* @return array{success: bool, message: string, delivery_id: int|null}
*/
public function sendTest(AlertDestination $destination, User $actor): array
{
if (! $actor->can('update', $destination)) {
throw new AuthorizationException('You do not have permission to send test messages for this destination.');
}
if (! $destination->is_enabled) {
return [
'success' => false,
'message' => 'This destination is currently disabled. Enable it before sending a test message.',
'delivery_id' => null,
];
}
if ($this->isRateLimited($destination)) {
return [
'success' => false,
'message' => 'A test message was sent recently. Please wait before trying again.',
'delivery_id' => null,
];
}
$delivery = $this->createTestDelivery($destination);
$this->auditLog($destination, $actor);
DeliverAlertsJob::dispatch((int) $destination->workspace_id);
return [
'success' => true,
'message' => 'Test message queued for delivery.',
'delivery_id' => (int) $delivery->getKey(),
];
}
private function isRateLimited(AlertDestination $destination): bool
{
return AlertDelivery::query()
->where('workspace_id', (int) $destination->workspace_id)
->where('alert_destination_id', (int) $destination->getKey())
->where('event_type', AlertDelivery::EVENT_TYPE_TEST)
->where('created_at', '>=', now()->subSeconds(self::RATE_LIMIT_SECONDS))
->exists();
}
private function createTestDelivery(AlertDestination $destination): AlertDelivery
{
return AlertDelivery::create([
'workspace_id' => (int) $destination->workspace_id,
'tenant_id' => null,
'alert_rule_id' => null,
'alert_destination_id' => (int) $destination->getKey(),
'event_type' => AlertDelivery::EVENT_TYPE_TEST,
'status' => AlertDelivery::STATUS_QUEUED,
'severity' => null,
'fingerprint_hash' => 'test:'.(int) $destination->getKey(),
'attempt_count' => 0,
'payload' => [
'title' => 'Test alert',
'body' => 'This is a test delivery for destination verification.',
],
]);
}
private function auditLog(AlertDestination $destination, User $actor): void
{
$workspace = $destination->workspace;
if ($workspace === null) {
return;
}
$this->auditLogger->log(
workspace: $workspace,
action: AuditActionId::AlertDestinationTestRequested->value,
context: [
'metadata' => [
'alert_destination_id' => (int) $destination->getKey(),
'name' => (string) $destination->name,
'type' => (string) $destination->type,
],
],
actor: $actor,
resourceType: 'alert_destination',
resourceId: (string) $destination->getKey(),
);
}
}