60 lines
2.8 KiB
PHP
60 lines
2.8 KiB
PHP
<?php
|
|
|
|
namespace App\Services;
|
|
|
|
use Opis\JsonSchema\Validator;
|
|
|
|
/** Transparent field completion, separate from validation, approval and external decisions. */
|
|
class OpportunityCompleteness
|
|
{
|
|
/** @param array<string,mixed> $payload
|
|
* @return array<string,mixed> */
|
|
public function calculate(array $payload): array
|
|
{
|
|
$schema = app(OpportunityCanonicalContract::class)->schema(false, $payload);
|
|
$validator = new Validator;
|
|
$paths = [];
|
|
$walk = function (mixed $value, array $node, string $path) use (&$walk, &$paths, $schema, $validator): void {
|
|
if (isset($node['$ref'])) {
|
|
$node = $schema['$defs'][basename($node['$ref'])] ?? $node;
|
|
}
|
|
$requirements = $node['required'] ?? [];
|
|
foreach ($node['allOf'] ?? [] as $condition) {
|
|
if (! isset($condition['if'])) {
|
|
$walk($value, $condition, $path);
|
|
|
|
continue;
|
|
}
|
|
$test = $condition['if'];
|
|
if (str_contains(json_encode($test, JSON_THROW_ON_ERROR), '#/$defs/')) {
|
|
$test['$defs'] = $schema['$defs'];
|
|
}
|
|
$actual = json_decode(json_encode(is_array($value) && ! array_is_list($value) ? (object) $value : $value, JSON_THROW_ON_ERROR), false, 512, JSON_THROW_ON_ERROR);
|
|
$yes = $validator->validate($actual, json_decode(json_encode($test, JSON_THROW_ON_ERROR), false, 512, JSON_THROW_ON_ERROR))->isValid();
|
|
$walk($value, $condition[$yes ? 'then' : 'else'] ?? [], $path);
|
|
}
|
|
foreach ($requirements as $key) {
|
|
$v = is_array($value) ? ($value[$key] ?? null) : null;
|
|
$paths[ltrim($path.'.'.$key, '.')] = $v !== null && $v !== '' && $v !== [];
|
|
}
|
|
foreach ($node['properties'] ?? [] as $key => $child) {
|
|
if (is_array($value) && array_key_exists($key, $value)) {
|
|
$walk($value[$key], $child, ltrim($path.'.'.$key, '.'));
|
|
}
|
|
}
|
|
if (isset($node['items']) && is_array($value) && array_is_list($value)) {
|
|
foreach ($value as $i => $child) {
|
|
$walk($child, $node['items'], $path.'.'.$i);
|
|
}
|
|
}
|
|
};
|
|
$walk($payload, ['$ref' => '#/$defs/opportunityCreate'], '');
|
|
$filled = count(array_filter($paths));
|
|
$total = count($paths);
|
|
|
|
return ['percentage' => $total ? round(100 * $filled / $total, 2) : 0, 'filled' => $filled, 'applicable' => $total,
|
|
'missing_paths' => array_keys(array_filter($paths, fn ($filled) => ! $filled)),
|
|
'rule' => 'required-applicable-fields-v1', 'scope' => 'Required fields and activated schema conditions; optional/hidden modules excluded. Presence is not validation or approval.'];
|
|
}
|
|
}
|