TenantAtlas/apps/platform/app/Services/TenantConfiguration/ExchangeTeamsRenderableSummaryBuilder.php
ahmido 13d363c8b8 feat: complete spec 422 exchange teams comparable renderable pack (#489)
## Summary

This PR completes spec 422 exchange teams comparable renderable pack with comparable diffing, renderable summary builders, and comprehensive test updates.

## Commit
- 4c1e14c6 feat: complete spec 422 exchange teams comparable renderable pack

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #489
2026-06-30 04:20:13 +00:00

306 lines
10 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Services\TenantConfiguration;
final class ExchangeTeamsRenderableSummaryBuilder
{
public function __construct(
private readonly ExchangeTeamsComparablePayloadNormalizer $normalizer,
) {}
public function supports(string $canonicalType): bool
{
return $this->normalizer->supports($canonicalType);
}
/**
* @param array<string, mixed> $payload
*/
public function canBuild(string $canonicalType, array $payload): bool
{
if (! $this->supports($canonicalType)) {
return false;
}
$normalized = $this->normalizer->normalize($canonicalType, $payload);
return ($normalized['supported'] ?? false) === true;
}
/**
* @param array<string, mixed> $payload
* @param array<string, mixed> $context
* @return array<string, mixed>|null
*/
public function build(string $canonicalType, array $payload, array $context = []): ?array
{
if (! $this->supports($canonicalType)) {
return null;
}
$normalized = $this->normalizer->normalize($canonicalType, $payload);
if (($normalized['supported'] ?? false) !== true) {
return null;
}
return match ($canonicalType) {
'transportRule' => $this->transportRuleSummary($normalized, $context),
'acceptedDomain' => $this->acceptedDomainSummary($normalized, $context),
'appPermissionPolicy' => $this->appPermissionPolicySummary($normalized, $context),
'meetingPolicy' => $this->meetingPolicySummary($normalized, $context),
default => null,
};
}
/**
* @param array<string, mixed> $normalized
* @param array<string, mixed> $context
* @return array<string, mixed>
*/
private function transportRuleSummary(array $normalized, array $context): array
{
return $this->baseSummary('Transport rule', $normalized, $context, [
['label' => 'Display name', 'value' => $normalized['display_name'] ?? 'Unnamed transport rule'],
['label' => 'Enabled/state', 'value' => $normalized['enabled_state'] ?? null],
['label' => 'Priority / order', 'value' => $normalized['priority_order'] ?? null],
['label' => 'Mode / enforcement', 'value' => $normalized['mode'] ?? null],
['label' => 'Conditions', 'value' => $this->settingSummary($normalized['conditions'] ?? [])],
['label' => 'Actions', 'value' => $this->settingSummary($normalized['actions'] ?? [])],
['label' => 'Exceptions', 'value' => $this->settingSummary($normalized['exceptions'] ?? [])],
]);
}
/**
* @param array<string, mixed> $normalized
* @param array<string, mixed> $context
* @return array<string, mixed>
*/
private function acceptedDomainSummary(array $normalized, array $context): array
{
return $this->baseSummary('Accepted domain', $normalized, $context, [
['label' => 'Domain name', 'value' => $normalized['domain_name'] ?? 'Unnamed accepted domain'],
['label' => 'Domain type', 'value' => $normalized['domain_type'] ?? null],
['label' => 'Default domain', 'value' => $normalized['is_default'] ?? null],
['label' => 'State', 'value' => $normalized['state'] ?? null],
], displayName: $normalized['domain_name'] ?? null, state: $normalized['state'] ?? null);
}
/**
* @param array<string, mixed> $normalized
* @param array<string, mixed> $context
* @return array<string, mixed>
*/
private function appPermissionPolicySummary(array $normalized, array $context): array
{
return $this->baseSummary('Teams app permission policy', $normalized, $context, [
['label' => 'Display name', 'value' => $normalized['display_name'] ?? 'Unnamed app permission policy'],
['label' => 'Policy mode', 'value' => $normalized['policy_mode'] ?? null],
['label' => 'Allowed apps', 'value' => $this->appSummary($normalized['allowed_apps'] ?? [])],
['label' => 'Blocked apps', 'value' => $this->appSummary($normalized['blocked_apps'] ?? [])],
['label' => 'Assignments / targets', 'value' => $this->settingSummary($normalized['targets'] ?? [])],
], state: $normalized['policy_mode'] ?? null);
}
/**
* @param array<string, mixed> $normalized
* @param array<string, mixed> $context
* @return array<string, mixed>
*/
private function meetingPolicySummary(array $normalized, array $context): array
{
return $this->baseSummary('Teams meeting policy', $normalized, $context, [
['label' => 'Display name', 'value' => $normalized['display_name'] ?? 'Unnamed meeting policy'],
['label' => 'State', 'value' => $normalized['state'] ?? null],
['label' => 'External / anonymous access', 'value' => $this->settingSummary($normalized['external_access'] ?? [])],
['label' => 'Recording / transcription', 'value' => $this->settingSummary($normalized['recording_transcription'] ?? [])],
['label' => 'Lobby / admission', 'value' => $this->settingSummary($normalized['lobby_admission'] ?? [])],
['label' => 'Content sharing', 'value' => $this->settingSummary($normalized['content_sharing'] ?? [])],
], state: $normalized['state'] ?? null);
}
/**
* @param array<string, mixed> $normalized
* @param array<string, mixed> $context
* @param list<array{label: string, value: mixed}> $fields
* @return array<string, mixed>
*/
private function baseSummary(
string $resourceType,
array $normalized,
array $context,
array $fields,
?string $displayName = null,
?string $state = null,
): array {
return [
'resource_type' => $resourceType,
'display_name' => $displayName ?? $this->displayName($normalized, $resourceType),
'state' => $state,
'summary_fields' => array_values(array_filter(
array_map(fn (array $field): array => [
'label' => $field['label'],
'value' => $this->summaryValue($field['value']),
], $fields),
static fn (array $field): bool => filled($field['value'] ?? null),
)),
'targets' => [],
'conditions' => [],
'claim_state' => $this->stringContext($context, 'claim_state'),
'identity_state' => $this->stringContext($context, 'identity_state'),
'last_captured' => $this->stringContext($context, 'last_captured'),
'unsupported_fields' => data_get($normalized, 'diagnostics.unsupported_fields', []),
'redacted_fields' => data_get($normalized, 'diagnostics.redacted_fields', []),
];
}
/**
* @param array<string, mixed> $normalized
*/
private function displayName(array $normalized, string $fallback): string
{
foreach (['display_name', 'domain_name'] as $key) {
$value = $normalized[$key] ?? null;
if (is_scalar($value) && trim((string) $value) !== '') {
return trim((string) $value);
}
}
return 'Unnamed '.$fallback;
}
private function summaryValue(mixed $value): ?string
{
if ($value === null || $value === '' || $value === []) {
return null;
}
if (is_bool($value)) {
return $value ? 'yes' : 'no';
}
if (is_scalar($value)) {
$value = trim((string) $value);
return $value !== '' ? $value : null;
}
if (is_array($value)) {
return $this->stringify($value);
}
return null;
}
/**
* @param array<string, mixed> $settings
*/
private function settingSummary(array $settings): ?string
{
if ($settings === []) {
return null;
}
$parts = [];
foreach ($settings as $key => $value) {
if ($this->containsRedacted($value)) {
continue;
}
$value = $this->summaryValue($value);
if ($value === null) {
continue;
}
$parts[] = str((string) $key)->replace('_', ' ')->headline()->toString().': '.$value;
}
return $parts === [] ? null : implode('; ', $parts);
}
private function containsRedacted(mixed $value): bool
{
if ($value === '[redacted]') {
return true;
}
if (! is_array($value)) {
return false;
}
foreach ($value as $nestedValue) {
if ($this->containsRedacted($nestedValue)) {
return true;
}
}
return false;
}
/**
* @param list<array<string, mixed>> $apps
*/
private function appSummary(array $apps): ?string
{
if ($apps === []) {
return null;
}
$parts = [];
foreach ($apps as $app) {
if (! is_array($app)) {
continue;
}
$display = $this->summaryValue($app['display_name'] ?? null);
if ($display !== null) {
$parts[] = $display;
}
}
$parts = array_values(array_filter($parts));
return $parts === [] ? null : implode(', ', $parts);
}
private function stringify(mixed $value): string
{
if (is_bool($value)) {
return $value ? 'yes' : 'no';
}
if (is_scalar($value)) {
return (string) $value;
}
return json_encode($value, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR);
}
/**
* @param array<string, mixed> $context
*/
private function stringContext(array $context, string $key): ?string
{
$value = $context[$key] ?? null;
if ($value instanceof \BackedEnum) {
return (string) $value->value;
}
if (! is_scalar($value)) {
return null;
}
$value = trim((string) $value);
return $value !== '' ? $value : null;
}
}