TenantAtlas/apps/platform/app/Services/TenantConfiguration/ExchangeTransportRuleEvidenceNormalizer.php
ahmido a09cc6ca8d Spec 435: Exchange structured output readiness (#502)
## Summary

- Add Spec 435 backend-only Exchange structured output envelope and target normalizer readiness helpers for transportRule, remoteDomain, and inboundConnector.
- Add deterministic hash input/readiness handling and no-promotion guard coverage.
- Add Spec 435 spec, plan, tasks, checklist, and implementation report artifacts.

## Validation

- git diff --cached --check
- cd apps/platform && php artisan test --filter=Spec435 --compact

## Product Surface / Deployment

- Livewire v4 compliance unchanged.
- Filament provider registration unchanged under apps/platform/bootstrap/providers.php.
- Global search unchanged; no resources added or modified.
- Destructive/high-impact actions: none.
- Asset strategy: none; no filament:assets requirement for this slice.
- Browser proof: N/A - no rendered UI surface changed.
- Deployment impact: no env vars, migrations, queues, scheduler, storage, assets, or Dokploy runtime behavior changes.

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #502
2026-07-08 14:33:23 +00:00

98 lines
2.7 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Services\TenantConfiguration;
final class ExchangeTransportRuleEvidenceNormalizer extends ExchangePowerShellTargetEvidenceNormalizer
{
public function resourceType(): string
{
return 'transportRule';
}
public function normalizerVersion(): string
{
return 'exchange-transport-rule-readiness-v1';
}
/**
* @return list<string>
*/
protected function materialFields(): array
{
return [
'Guid',
'RuleId',
'Identity',
'DisplayName',
'Name',
'Priority',
'Mode',
'State',
'Enabled',
'From',
'RecipientDomainIs',
'SenderDomainIs',
'SentTo',
'SubjectContainsWords',
'SubjectOrBodyContainsWords',
'BodyContainsWords',
'ApplyHtmlDisclaimerText',
'ApplyHtmlDisclaimerFallbackAction',
'ApplyHtmlDisclaimerLocation',
'DeleteMessage',
'RedirectMessageTo',
'ExceptIfFrom',
'ExceptIfRecipientDomainIs',
'HeaderContainsWords',
'SetHeaderValue',
];
}
/**
* @return list<list<string>>
*/
protected function identityAliasGroups(): array
{
return [
['id', 'sourceId'],
['Guid', 'RuleId'],
];
}
protected function normalizePayload(array $payload, array $identity): array
{
$normalized = parent::normalizePayload($payload, $identity);
$normalized['rule_order'] = [
'priority' => $this->normalizeSettingValue($payload['Priority'] ?? $payload['priority'] ?? null),
'order_is_identity' => false,
];
$normalized['conditions'] = $this->settingGroup($payload, ['Conditions', 'conditions'], [
'From',
'RecipientDomainIs',
'SenderDomainIs',
'SentTo',
'SubjectContainsWords',
'SubjectOrBodyContainsWords',
'BodyContainsWords',
]);
$normalized['actions'] = $this->settingGroup($payload, ['Actions', 'actions'], [
'ApplyHtmlDisclaimerText',
'ApplyHtmlDisclaimerFallbackAction',
'ApplyHtmlDisclaimerLocation',
'DeleteMessage',
'RedirectMessageTo',
'HeaderContainsWords',
'SetHeaderValue',
]);
$normalized['exceptions'] = $this->settingGroup($payload, ['Exceptions', 'exceptions'], [
'ExceptIfFrom',
'ExceptIfRecipientDomainIs',
]);
return $this->sortAssociative($normalized);
}
}