TenantAtlas/apps/platform/app/Services/TenantConfiguration/ExchangePowerShellHashInputBuilder.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

74 lines
2.0 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Services\TenantConfiguration;
final class ExchangePowerShellHashInputBuilder
{
/**
* @param array<string, mixed> $normalizedPayload
* @return array<string, mixed>
*/
public function build(
string $resourceType,
string $sourceSurface,
string $commandContractName,
string $commandContractVersion,
string $payloadShapeVersion,
string $normalizerVersion,
array $normalizedPayload,
): array {
return $this->canonicalize([
'resource_type' => $resourceType,
'source_surface' => $sourceSurface,
'command_contract_name' => $commandContractName,
'command_contract_version' => $commandContractVersion,
'payload_shape_version' => $payloadShapeVersion,
'normalizer_version' => $normalizerVersion,
'normalized_payload' => $normalizedPayload,
]);
}
/**
* @param array<string, mixed> $hashInput
*/
public function hash(array $hashInput): string
{
return hash('sha256', $this->canonicalJson($hashInput));
}
/**
* @param array<string, mixed> $hashInput
*/
public function canonicalJson(array $hashInput): string
{
return json_encode($this->canonicalize($hashInput), JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR);
}
private function canonicalize(mixed $value): mixed
{
if (! is_array($value)) {
return $value;
}
if (array_is_list($value)) {
return array_map(fn (mixed $item): mixed => $this->canonicalize($item), $value);
}
$normalized = [];
foreach ($value as $key => $nestedValue) {
if ($nestedValue === null) {
continue;
}
$normalized[(string) $key] = $this->canonicalize($nestedValue);
}
ksort($normalized, SORT_NATURAL | SORT_FLAG_CASE);
return $normalized;
}
}