forked from fa/breadcrumb-the-shire
refactor(actions): deduplicate audit metadata and grid user count enrichment
This commit is contained in:
125
agent-system/runs/CODE-DEDUP-ACTIONS-001/execution-report.json
Normal file
125
agent-system/runs/CODE-DEDUP-ACTIONS-001/execution-report.json
Normal file
@@ -0,0 +1,125 @@
|
||||
{
|
||||
"task_id": "CODE-DEDUP-ACTIONS-001",
|
||||
"plan_ref": "agent-system/runs/CODE-DEDUP-ACTIONS-001/plan.json",
|
||||
"status": "done",
|
||||
"changed_files": [
|
||||
{
|
||||
"path": "lib/Service/Audit/AuditMetadataEnricher.php",
|
||||
"summary": "CREATED — S1: New service resolving audit user IDs (created_by, modified_by, etc.) into display labels and UUIDs. Accepts configurable field list. Depends on UserReadRepositoryInterface."
|
||||
},
|
||||
{
|
||||
"path": "lib/Service/Data/GridUserCountEnricher.php",
|
||||
"summary": "CREATED — S2: New stateless service that collects entity IDs from grid rows, batch-queries total and active user counts via callables, and returns a lookup map."
|
||||
},
|
||||
{
|
||||
"path": "tests/Service/Audit/AuditMetadataEnricherTest.php",
|
||||
"summary": "CREATED — S3: 6 tests covering label resolution, email fallback, zero/missing IDs, user not found, and custom fields."
|
||||
},
|
||||
{
|
||||
"path": "tests/Service/Data/GridUserCountEnricherTest.php",
|
||||
"summary": "CREATED — S3: 6 tests covering typical rows, empty rows, ID deduplication, zero IDs, zero counts, and negative inactive clamping."
|
||||
},
|
||||
{
|
||||
"path": "lib/App/Container/Registrars/AppServicesRegistrar.php",
|
||||
"summary": "S4: Registered AuditMetadataEnricher (with UserReadRepository dep) and GridUserCountEnricher (stateless) in DI container."
|
||||
},
|
||||
{
|
||||
"path": "pages/admin/tenants/edit($id).php",
|
||||
"summary": "S5: Replaced 27 lines of inline audit metadata resolution (created_by, modified_by, status_changed_by) with single AuditMetadataEnricher call. Removed unused $userAccountService variable."
|
||||
},
|
||||
{
|
||||
"path": "pages/admin/roles/edit($id).php",
|
||||
"summary": "S5: Replaced 18 lines of inline audit metadata resolution (created_by, modified_by) with single AuditMetadataEnricher call. Removed unused $userAccountService variable."
|
||||
},
|
||||
{
|
||||
"path": "pages/admin/departments/edit($id).php",
|
||||
"summary": "S5: Replaced 18 lines of inline audit metadata resolution (created_by, modified_by) with single AuditMetadataEnricher call. Removed unused $userAccountService variable."
|
||||
},
|
||||
{
|
||||
"path": "pages/admin/users/edit($id).php",
|
||||
"summary": "S5: Replaced 28 lines of inline audit metadata resolution (created_by, modified_by, active_changed_by) with single AuditMetadataEnricher call inside permission guard."
|
||||
},
|
||||
{
|
||||
"path": "pages/admin/roles/data().php",
|
||||
"summary": "S6: Replaced 12 lines of ID collection + user count aggregation with GridUserCountEnricher.computeCounts() call. Row loop uses count map lookup."
|
||||
},
|
||||
{
|
||||
"path": "pages/admin/departments/data().php",
|
||||
"summary": "S6: Replaced 11 lines of ID collection + user count aggregation with GridUserCountEnricher.computeCounts() call. Row loop uses count map lookup."
|
||||
},
|
||||
{
|
||||
"path": "pages/admin/tenants/data().php",
|
||||
"summary": "S6: Replaced 8 lines of ID collection + user count aggregation inside $fetchRows closure with GridUserCountEnricher.computeCounts() call. Also uses enricher IDs for SSO map query."
|
||||
}
|
||||
],
|
||||
"guard_evidence": [
|
||||
{
|
||||
"guard_id": "GR-CORE-001",
|
||||
"status": "pass",
|
||||
"evidence": "AuditMetadataEnricher is a Service class with single repository dependency (UserReadRepositoryInterface). GridUserCountEnricher is a stateless Service with no dependencies. Both follow the Service contract — business logic, no HTML, no orchestration flow."
|
||||
},
|
||||
{
|
||||
"guard_id": "GR-CORE-002",
|
||||
"status": "pass",
|
||||
"evidence": "Strict layering preserved. New services registered in AppServicesRegistrar. Action files call services via app() helper. No cross-layer violations. No circular dependencies."
|
||||
},
|
||||
{
|
||||
"guard_id": "GR-TEST-001",
|
||||
"status": "pass",
|
||||
"evidence": "12 new tests added (6 per service). 603 total tests run, 0 errors introduced by this task. 1 pre-existing failure (TranslationKeysTest) unrelated."
|
||||
},
|
||||
{
|
||||
"guard_id": "GR-TEST-002",
|
||||
"status": "pass",
|
||||
"evidence": "New tests cover edge cases: zero IDs, missing fields, user not found, email fallback, ID deduplication, negative inactive clamping. No existing tests modified."
|
||||
},
|
||||
{
|
||||
"guard_id": "GR-LANG-001",
|
||||
"status": "pass",
|
||||
"evidence": "All new PHP files follow PSR-4 namespace conventions. Import ordering auto-fixed by php-cs-fixer."
|
||||
},
|
||||
{
|
||||
"guard_id": "GR-LANG-002",
|
||||
"status": "pass",
|
||||
"evidence": "PHPStan level 5: 0 new errors from this task. 4 pre-existing errors in AuthzUiContractTest.php are unrelated."
|
||||
}
|
||||
],
|
||||
"commands": [
|
||||
{ "cmd": "docker compose exec php vendor/bin/phpunit --no-coverage", "result": "pass" },
|
||||
{ "cmd": "docker compose exec php vendor/bin/phpstan analyse -c phpstan.neon --no-progress", "result": "pass" },
|
||||
{ "cmd": "docker compose exec php vendor/bin/php-cs-fixer fix --dry-run --config=.php-cs-fixer.dist.php", "result": "pass" },
|
||||
{ "cmd": "docker compose exec php vendor/bin/phpunit --filter CoreStarterkitContractTest|ContainerFactoryContractTest", "result": "pass" },
|
||||
{ "cmd": "rg 'AuditMetadataEnricher' pages/admin/ (SC-001: usage in 4 edit action files)", "result": "pass" },
|
||||
{ "cmd": "rg 'GridUserCountEnricher' pages/admin/ (SC-002: usage in 3 data endpoint files)", "result": "pass" },
|
||||
{ "cmd": "rg 'created_by_label.*=.*trim' pages/ (SC-003: zero inline audit resolution left)", "result": "pass" }
|
||||
],
|
||||
"quality_gate_results": [
|
||||
{
|
||||
"gate_id": "QG-001",
|
||||
"result": "pass",
|
||||
"notes": "603 tests, 0 errors introduced by this task. 12 new tests added. 1 pre-existing failure (TranslationKeysTest: missing i18n key) — verified pre-existing: same failure reproduces on clean branch (git stash → phpunit → same failure). This task introduced zero test failures."
|
||||
},
|
||||
{
|
||||
"gate_id": "QG-002",
|
||||
"result": "pass",
|
||||
"notes": "0 new PHPStan errors introduced by this task. 4 pre-existing errors in tests/Architecture/AuthzUiContractTest.php (nullable offset warnings) — verified pre-existing: present on clean branch before any task changes. This task introduced zero PHPStan errors."
|
||||
},
|
||||
{
|
||||
"gate_id": "QG-003",
|
||||
"result": "pass",
|
||||
"notes": "Architecture contract tests (CoreStarterkitContractTest + ContainerFactoryContractTest): 24 tests, 6454 assertions, all green."
|
||||
},
|
||||
{
|
||||
"gate_id": "QG-006",
|
||||
"result": "pass",
|
||||
"notes": "php-cs-fixer fix --dry-run: 0 of 526 files need fixing."
|
||||
}
|
||||
],
|
||||
"test_results": [
|
||||
{ "name": "tests/Service/Audit/AuditMetadataEnricherTest.php", "result": "pass" },
|
||||
{ "name": "tests/Service/Data/GridUserCountEnricherTest.php", "result": "pass" },
|
||||
{ "name": "tests/Architecture/CoreStarterkitContractTest.php", "result": "pass" },
|
||||
{ "name": "tests/Architecture/ContainerFactoryContractTest.php", "result": "pass" }
|
||||
],
|
||||
"open_items": []
|
||||
}
|
||||
10
agent-system/runs/CODE-DEDUP-ACTIONS-001/finalize.json
Normal file
10
agent-system/runs/CODE-DEDUP-ACTIONS-001/finalize.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"task_id": "CODE-DEDUP-ACTIONS-001",
|
||||
"ready_to_finalize": true,
|
||||
"guard_review": "pass",
|
||||
"acceptance_review": "pass",
|
||||
"ci_status": "pass",
|
||||
"final_action": "commit",
|
||||
"commit_message": "refactor(actions): deduplicate audit metadata and grid user count enrichment",
|
||||
"notes": "review-guards and review-acceptance are pass; execution-report documents QG-001/QG-002/QG-003/QG-006 as pass with no task-introduced failures."
|
||||
}
|
||||
128
agent-system/runs/CODE-DEDUP-ACTIONS-001/plan.json
Normal file
128
agent-system/runs/CODE-DEDUP-ACTIONS-001/plan.json
Normal file
@@ -0,0 +1,128 @@
|
||||
{
|
||||
"task_id": "CODE-DEDUP-ACTIONS-001",
|
||||
"summary": "Extract repeated action-layer patterns (audit metadata enrichment, grid user count calculation) into shared service classes to eliminate ~260 lines of duplication across 7+ action/data files.",
|
||||
"assumptions": [
|
||||
"Audit metadata enrichment (created_by, modified_by labels) repeats identically across 4+ edit actions.",
|
||||
"Grid user count calculation repeats identically across 3 data endpoints (roles, departments, tenants).",
|
||||
"Both patterns are pure data transformation — suitable for service extraction without changing action flow."
|
||||
],
|
||||
"scope": {
|
||||
"in": [
|
||||
"New: lib/Service/Audit/AuditMetadataEnricher.php — extract audit label resolution",
|
||||
"New: lib/Service/Data/GridUserCountEnricher.php — extract user count aggregation for grid data endpoints",
|
||||
"pages/admin/tenants/edit($id).php — replace inline audit metadata with service call",
|
||||
"pages/admin/roles/edit($id).php — replace inline audit metadata with service call",
|
||||
"pages/admin/users/edit($tenantslug,$id).php — replace inline audit metadata with service call",
|
||||
"pages/admin/departments/edit($id).php — replace inline audit metadata with service call",
|
||||
"pages/admin/roles/data().php — replace inline user count logic with service call",
|
||||
"pages/admin/departments/data().php — replace inline user count logic with service call",
|
||||
"pages/admin/tenants/data().php — replace inline user count logic with service call",
|
||||
"Factory registrars for new services"
|
||||
],
|
||||
"out": [
|
||||
"View files (.phtml) — not touched",
|
||||
"Repository SQL queries — not touched",
|
||||
"Frontend JS/CSS — not touched",
|
||||
"Permission/authorization logic — not touched"
|
||||
]
|
||||
},
|
||||
"guardrails": {
|
||||
"required_guard_ids": [
|
||||
"GR-CORE-001",
|
||||
"GR-CORE-002",
|
||||
"GR-TEST-001",
|
||||
"GR-TEST-002",
|
||||
"GR-LANG-001",
|
||||
"GR-LANG-002"
|
||||
],
|
||||
"required_quality_gate_ids": [
|
||||
"QG-001",
|
||||
"QG-002",
|
||||
"QG-003",
|
||||
"QG-006"
|
||||
]
|
||||
},
|
||||
"success_criteria": [
|
||||
{
|
||||
"id": "SC-001",
|
||||
"criterion": "AuditMetadataEnricher exists in lib/Service/Audit/ and is used by all 4 edit actions instead of inline created_by/modified_by resolution."
|
||||
},
|
||||
{
|
||||
"id": "SC-002",
|
||||
"criterion": "GridUserCountEnricher exists in lib/Service/Data/ and is used by all 3 data endpoints instead of inline count aggregation."
|
||||
},
|
||||
{
|
||||
"id": "SC-003",
|
||||
"criterion": "No inline audit-label or user-count-aggregation logic remains in pages/ action files."
|
||||
},
|
||||
{
|
||||
"id": "SC-004",
|
||||
"criterion": "PHPUnit, PHPStan, Architecture Contract, and CS checks pass green; zero new failures introduced by this task (pre-existing failures are out of scope)."
|
||||
}
|
||||
],
|
||||
"implementation_steps": [
|
||||
{
|
||||
"id": "S1",
|
||||
"title": "Create AuditMetadataEnricher service",
|
||||
"description": "Extract the repeated pattern of resolving created_by, modified_by, status_changed_by, active_changed_by into display labels (name or email fallback) and UUIDs. Accept entity array by reference, list of audit fields to resolve, and UserAccountService dependency.",
|
||||
"guard_refs": ["GR-CORE-001", "GR-TEST-002"]
|
||||
},
|
||||
{
|
||||
"id": "S2",
|
||||
"title": "Create GridUserCountEnricher service",
|
||||
"description": "Extract repeated pattern of collecting entity IDs from grid rows, querying user counts (total + active), and enriching rows with count fields. Generic enough to work with roles, departments, and tenants.",
|
||||
"guard_refs": ["GR-CORE-001", "GR-TEST-002"]
|
||||
},
|
||||
{
|
||||
"id": "S3",
|
||||
"title": "Write unit tests for both new services",
|
||||
"description": "AuditMetadataEnricher: test with present/missing/zero audit IDs, user not found fallback. GridUserCountEnricher: test with empty rows, mixed IDs, zero counts.",
|
||||
"guard_refs": ["GR-TEST-001"]
|
||||
},
|
||||
{
|
||||
"id": "S4",
|
||||
"title": "Register new services in DI container",
|
||||
"description": "Add factory registrations in appropriate Container/Registrars/ files.",
|
||||
"guard_refs": ["GR-CORE-002"]
|
||||
},
|
||||
{
|
||||
"id": "S5",
|
||||
"title": "Refactor edit actions to use AuditMetadataEnricher",
|
||||
"description": "Replace inline audit metadata resolution in all 4 edit actions with enricher service call.",
|
||||
"guard_refs": ["GR-CORE-001", "GR-CORE-002"]
|
||||
},
|
||||
{
|
||||
"id": "S6",
|
||||
"title": "Refactor data endpoints to use GridUserCountEnricher",
|
||||
"description": "Replace inline user count aggregation in all 3 data endpoints with enricher service call.",
|
||||
"guard_refs": ["GR-CORE-001", "GR-CORE-002"]
|
||||
},
|
||||
{
|
||||
"id": "S7",
|
||||
"title": "Run all quality gates",
|
||||
"description": "QG-001, QG-002, QG-003, QG-006.",
|
||||
"guard_refs": ["GR-LANG-002"]
|
||||
}
|
||||
],
|
||||
"tests": [
|
||||
"tests/Service/Audit/AuditMetadataEnricherTest.php — new, covers label resolution edge cases",
|
||||
"tests/Service/Data/GridUserCountEnricherTest.php — new, covers count aggregation edge cases",
|
||||
"tests/Architecture/CoreStarterkitContractTest.php — must stay green"
|
||||
],
|
||||
"acceptance_checks": [
|
||||
"SC-001: rg 'AuditMetadataEnricher' pages/admin/ shows usage in 4 edit action files",
|
||||
"SC-002: rg 'GridUserCountEnricher' pages/admin/ shows usage in 3 data endpoint files",
|
||||
"SC-003: rg 'created_by_label.*=.*trim' pages/ returns 0 matches (no inline resolution left)",
|
||||
"SC-004: QG-001 + QG-002 + QG-003 + QG-006 introduce zero new failures (pre-existing issues are out of scope)"
|
||||
],
|
||||
"risks": [
|
||||
{
|
||||
"risk": "Edit actions have slightly different audit fields (some have status_changed_by, others don't)",
|
||||
"mitigation": "Design AuditMetadataEnricher to accept a configurable list of field names to resolve. Each caller specifies which fields apply."
|
||||
},
|
||||
{
|
||||
"risk": "Data endpoints have slightly different count repository interfaces",
|
||||
"mitigation": "Design GridUserCountEnricher to accept a callable or repository interface for count queries, keeping it generic."
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"task_id": "CODE-DEDUP-ACTIONS-001",
|
||||
"verdict": "pass",
|
||||
"checked_criterion_ids": ["SC-001", "SC-002", "SC-003", "SC-004"],
|
||||
"checks": [
|
||||
{
|
||||
"criterion_id": "SC-001",
|
||||
"criterion": "AuditMetadataEnricher exists in lib/Service/Audit/ and is used by all 4 edit actions instead of inline created_by/modified_by resolution.",
|
||||
"result": "pass",
|
||||
"evidence": "execution-report.changed_files lists lib/Service/Audit/AuditMetadataEnricher.php plus all 4 edit actions as refactored; execution-report.commands includes the SC-001 rg check with result pass."
|
||||
},
|
||||
{
|
||||
"criterion_id": "SC-002",
|
||||
"criterion": "GridUserCountEnricher exists in lib/Service/Data/ and is used by all 3 data endpoints instead of inline count aggregation.",
|
||||
"result": "pass",
|
||||
"evidence": "execution-report.changed_files lists lib/Service/Data/GridUserCountEnricher.php plus all 3 data endpoints as refactored; execution-report.commands includes the SC-002 rg check with result pass."
|
||||
},
|
||||
{
|
||||
"criterion_id": "SC-003",
|
||||
"criterion": "No inline audit-label or user-count-aggregation logic remains in pages/ action files.",
|
||||
"result": "pass",
|
||||
"evidence": "execution-report.commands includes the SC-003 inline-pattern grep check with result pass ('rg created_by_label.*=.*trim pages/')."
|
||||
},
|
||||
{
|
||||
"criterion_id": "SC-004",
|
||||
"criterion": "PHPUnit, PHPStan, Architecture Contract, and CS checks pass green; zero new failures introduced by this task (pre-existing failures are out of scope).",
|
||||
"result": "pass",
|
||||
"evidence": "execution-report.quality_gate_results marks QG-001, QG-002, QG-003, and QG-006 as pass, and execution-report.commands marks all corresponding commands as pass; notes explicitly classify remaining issues as pre-existing and not introduced by this task."
|
||||
}
|
||||
]
|
||||
}
|
||||
19
agent-system/runs/CODE-DEDUP-ACTIONS-001/review-guards.json
Normal file
19
agent-system/runs/CODE-DEDUP-ACTIONS-001/review-guards.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"task_id": "CODE-DEDUP-ACTIONS-001",
|
||||
"verdict": "pass",
|
||||
"checked_guard_ids": [
|
||||
"GR-CORE-001",
|
||||
"GR-CORE-002",
|
||||
"GR-TEST-001",
|
||||
"GR-TEST-002",
|
||||
"GR-LANG-001",
|
||||
"GR-LANG-002"
|
||||
],
|
||||
"checked_quality_gate_ids": [
|
||||
"QG-001",
|
||||
"QG-002",
|
||||
"QG-003",
|
||||
"QG-006"
|
||||
],
|
||||
"findings": []
|
||||
}
|
||||
@@ -15,10 +15,12 @@ use MintyPHP\Http\SessionStoreInterface;
|
||||
use MintyPHP\Repository\Search\SearchQueryRepository;
|
||||
use MintyPHP\Repository\Stats\AdminStatsRepository;
|
||||
use MintyPHP\Repository\Tenant\UserTenantRepository;
|
||||
use MintyPHP\Repository\User\UserReadRepository;
|
||||
use MintyPHP\Service\Access\PermissionService;
|
||||
use MintyPHP\Service\AddressBook\AddressBookService;
|
||||
use MintyPHP\Service\AddressBook\AddressBookServicesFactory;
|
||||
use MintyPHP\Service\Audit\ApiAuditService;
|
||||
use MintyPHP\Service\Audit\AuditMetadataEnricher;
|
||||
use MintyPHP\Service\Audit\AuditServicesFactory;
|
||||
use MintyPHP\Service\Audit\FrontendTelemetryIngestService;
|
||||
use MintyPHP\Service\Audit\ImportAuditService;
|
||||
@@ -27,6 +29,7 @@ use MintyPHP\Service\Audit\UserLifecycleAuditService;
|
||||
use MintyPHP\Service\CustomField\CustomFieldServicesFactory;
|
||||
use MintyPHP\Service\CustomField\TenantCustomFieldService;
|
||||
use MintyPHP\Service\CustomField\UserCustomFieldValueService;
|
||||
use MintyPHP\Service\Data\GridUserCountEnricher;
|
||||
use MintyPHP\Service\Import\ImportService;
|
||||
use MintyPHP\Service\Import\ImportServicesFactory;
|
||||
use MintyPHP\Service\Mail\MailLogService;
|
||||
@@ -75,6 +78,10 @@ final class AppServicesRegistrar implements ContainerRegistrar
|
||||
$container->set(AddressBookService::class, static fn (AppContainer $c): AddressBookService => $c->get(AddressBookServicesFactory::class)->createAddressBookService());
|
||||
$container->set(TenantCustomFieldService::class, static fn (AppContainer $c): TenantCustomFieldService => $c->get(CustomFieldServicesFactory::class)->createTenantCustomFieldService());
|
||||
$container->set(UserCustomFieldValueService::class, static fn (AppContainer $c): UserCustomFieldValueService => $c->get(CustomFieldServicesFactory::class)->createUserCustomFieldValueService());
|
||||
$container->set(AuditMetadataEnricher::class, static fn (AppContainer $c): AuditMetadataEnricher => new AuditMetadataEnricher(
|
||||
$c->get(UserReadRepository::class)
|
||||
));
|
||||
$container->set(GridUserCountEnricher::class, static fn (): GridUserCountEnricher => new GridUserCountEnricher());
|
||||
$container->set(RequestInputFactory::class, static fn (): RequestInputFactory => new RequestInputFactory());
|
||||
$container->set(SessionStore::class, static fn (): SessionStore => new SessionStore());
|
||||
$container->set(SessionStoreInterface::class, static fn (AppContainer $c): SessionStoreInterface => $c->get(SessionStore::class));
|
||||
|
||||
42
lib/Service/Audit/AuditMetadataEnricher.php
Normal file
42
lib/Service/Audit/AuditMetadataEnricher.php
Normal file
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace MintyPHP\Service\Audit;
|
||||
|
||||
use MintyPHP\Repository\User\UserReadRepositoryInterface;
|
||||
|
||||
class AuditMetadataEnricher
|
||||
{
|
||||
public function __construct(
|
||||
private readonly UserReadRepositoryInterface $userReadRepository
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve audit user IDs on an entity into display labels and UUIDs.
|
||||
*
|
||||
* For each field name in $fields the method reads `$entity[$field]` as a
|
||||
* user ID, looks up the user record, and sets `{$field}_label` (full name
|
||||
* with email fallback) and `{$field}_uuid` on the entity.
|
||||
*
|
||||
* @param array<string,mixed> $entity The entity array (modified in place).
|
||||
* @param list<string> $fields Audit field names to resolve (e.g. 'created_by', 'modified_by').
|
||||
*/
|
||||
public function enrich(array &$entity, array $fields = ['created_by', 'modified_by']): void
|
||||
{
|
||||
foreach ($fields as $field) {
|
||||
$userId = (int) ($entity[$field] ?? 0);
|
||||
if ($userId <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$user = $this->userReadRepository->find($userId);
|
||||
if (!$user) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$name = trim(($user['first_name'] ?? '') . ' ' . ($user['last_name'] ?? ''));
|
||||
$entity[$field . '_label'] = $name !== '' ? $name : ($user['email'] ?? '');
|
||||
$entity[$field . '_uuid'] = $user['uuid'] ?? null;
|
||||
}
|
||||
}
|
||||
}
|
||||
46
lib/Service/Data/GridUserCountEnricher.php
Normal file
46
lib/Service/Data/GridUserCountEnricher.php
Normal file
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
namespace MintyPHP\Service\Data;
|
||||
|
||||
class GridUserCountEnricher
|
||||
{
|
||||
/**
|
||||
* Collect entity IDs from grid rows, batch-query total and active user
|
||||
* counts, and return a lookup map keyed by entity ID.
|
||||
*
|
||||
* @param array[] $rows Grid result rows (each must have an 'id' key).
|
||||
* @param callable(list<int>): array<int,int> $countTotal fn(ids) => [entityId => totalCount]
|
||||
* @param callable(list<int>): array<int,int> $countActive fn(ids) => [entityId => activeCount]
|
||||
* @return array<int, array{active_users: int, inactive_users: int}>
|
||||
*/
|
||||
public function computeCounts(array $rows, callable $countTotal, callable $countActive): array
|
||||
{
|
||||
$ids = [];
|
||||
foreach ($rows as $row) {
|
||||
$id = (int) ($row['id'] ?? 0);
|
||||
if ($id > 0) {
|
||||
$ids[] = $id;
|
||||
}
|
||||
}
|
||||
$ids = array_values(array_unique($ids));
|
||||
|
||||
if (!$ids) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$totalCounts = $countTotal($ids);
|
||||
$activeCounts = $countActive($ids);
|
||||
|
||||
$result = [];
|
||||
foreach ($ids as $id) {
|
||||
$total = (int) ($totalCounts[$id] ?? 0);
|
||||
$active = (int) ($activeCounts[$id] ?? 0);
|
||||
$result[$id] = [
|
||||
'active_users' => $active,
|
||||
'inactive_users' => max(0, $total - $active),
|
||||
];
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
@@ -29,24 +29,17 @@ $result = app(\MintyPHP\Service\Org\DepartmentService::class)->listPaged([
|
||||
|
||||
$settingsDefaultsGateway = app(\MintyPHP\Service\Settings\SettingsDefaultsGateway::class);
|
||||
$defaultDepartmentId = $settingsDefaultsGateway->getDefaultDepartmentId();
|
||||
$departmentIds = [];
|
||||
foreach ($result['rows'] as $departmentRow) {
|
||||
$departmentId = (int) ($departmentRow['id'] ?? 0);
|
||||
if ($departmentId > 0) {
|
||||
$departmentIds[] = $departmentId;
|
||||
}
|
||||
}
|
||||
$departmentIds = array_values(array_unique($departmentIds));
|
||||
$userDepartmentRepository = app(\MintyPHP\Repository\Org\UserDepartmentRepository::class);
|
||||
$departmentUserCounts = $userDepartmentRepository->countUsersByDepartmentIds($departmentIds);
|
||||
$departmentActiveUserCounts = $userDepartmentRepository->countActiveUsersByDepartmentIds($departmentIds);
|
||||
$userCounts = app(\MintyPHP\Service\Data\GridUserCountEnricher::class)->computeCounts(
|
||||
$result['rows'],
|
||||
$userDepartmentRepository->countUsersByDepartmentIds(...),
|
||||
$userDepartmentRepository->countActiveUsersByDepartmentIds(...)
|
||||
);
|
||||
|
||||
$rows = [];
|
||||
foreach ($result['rows'] as $row) {
|
||||
$departmentId = (int) ($row['id'] ?? 0);
|
||||
$usersTotal = (int) ($departmentUserCounts[$departmentId] ?? 0);
|
||||
$usersActive = (int) ($departmentActiveUserCounts[$departmentId] ?? 0);
|
||||
$usersInactive = max(0, $usersTotal - $usersActive);
|
||||
$counts = $userCounts[$departmentId] ?? ['active_users' => 0, 'inactive_users' => 0];
|
||||
$rows[] = [
|
||||
'id' => $row['id'] ?? null,
|
||||
'uuid' => $row['uuid'] ?? '',
|
||||
@@ -55,8 +48,8 @@ foreach ($result['rows'] as $row) {
|
||||
'code' => $row['code'] ?? '',
|
||||
'cost_center' => $row['cost_center'] ?? '',
|
||||
'active' => $row['active'] ?? 1,
|
||||
'active_users' => $usersActive,
|
||||
'inactive_users' => $usersInactive,
|
||||
'active_users' => $counts['active_users'],
|
||||
'inactive_users' => $counts['inactive_users'],
|
||||
'tenants' => $row['tenant_labels'] ?? [],
|
||||
'created' => dt($row['created'] ?? ''),
|
||||
'modified' => dt($row['modified'] ?? ''),
|
||||
|
||||
@@ -19,7 +19,6 @@ $currentUserId = (int) ($session['user']['id'] ?? 0);
|
||||
if ($currentUserId > 0) {
|
||||
app(\MintyPHP\Service\Access\PermissionService::class)->getUserPermissions($currentUserId);
|
||||
}
|
||||
$userAccountService = app(\MintyPHP\Service\User\UserAccountService::class);
|
||||
$departmentService = app(\MintyPHP\Service\Org\DepartmentService::class);
|
||||
$tenantService = app(\MintyPHP\Service\Tenant\TenantService::class);
|
||||
$directoryScopeGateway = app(\MintyPHP\Service\Tenant\TenantScopeService::class);
|
||||
@@ -62,24 +61,7 @@ if (!$canViewPage) {
|
||||
return;
|
||||
}
|
||||
|
||||
$creatorId = (int) ($department['created_by'] ?? 0);
|
||||
if ($creatorId > 0) {
|
||||
$creator = $userAccountService->findById($creatorId);
|
||||
if ($creator) {
|
||||
$creatorName = trim(($creator['first_name'] ?? '') . ' ' . ($creator['last_name'] ?? ''));
|
||||
$department['created_by_label'] = $creatorName !== '' ? $creatorName : ($creator['email'] ?? '');
|
||||
$department['created_by_uuid'] = $creator['uuid'] ?? null;
|
||||
}
|
||||
}
|
||||
$modifierId = (int) ($department['modified_by'] ?? 0);
|
||||
if ($modifierId > 0) {
|
||||
$modifier = $userAccountService->findById($modifierId);
|
||||
if ($modifier) {
|
||||
$modifierName = trim(($modifier['first_name'] ?? '') . ' ' . ($modifier['last_name'] ?? ''));
|
||||
$department['modified_by_label'] = $modifierName !== '' ? $modifierName : ($modifier['email'] ?? '');
|
||||
$department['modified_by_uuid'] = $modifier['uuid'] ?? null;
|
||||
}
|
||||
}
|
||||
app(\MintyPHP\Service\Audit\AuditMetadataEnricher::class)->enrich($department);
|
||||
|
||||
$errorBag = formErrors();
|
||||
$errors = [];
|
||||
|
||||
@@ -22,25 +22,18 @@ $result = app(\MintyPHP\Service\Access\RoleService::class)->listPaged([
|
||||
$settingsDefaultsGateway = app(\MintyPHP\Service\Settings\SettingsDefaultsGateway::class);
|
||||
$defaultRoleId = $settingsDefaultsGateway->getDefaultRoleId();
|
||||
$rolePermissionRepository = app(\MintyPHP\Repository\Access\RolePermissionRepository::class);
|
||||
$roleIds = [];
|
||||
foreach ($result['rows'] as $roleRow) {
|
||||
$roleId = (int) ($roleRow['id'] ?? 0);
|
||||
if ($roleId > 0) {
|
||||
$roleIds[] = $roleId;
|
||||
}
|
||||
}
|
||||
$roleIds = array_values(array_unique($roleIds));
|
||||
$userRoleRepository = app(\MintyPHP\Repository\Access\UserRoleRepository::class);
|
||||
$roleUserCounts = $userRoleRepository->countUsersByRoleIds($roleIds);
|
||||
$roleActiveUserCounts = $userRoleRepository->countActiveUsersByRoleIds($roleIds);
|
||||
$rolePermissionCounts = $rolePermissionRepository->countPermissionsByRoleIds($roleIds);
|
||||
$userCounts = app(\MintyPHP\Service\Data\GridUserCountEnricher::class)->computeCounts(
|
||||
$result['rows'],
|
||||
$userRoleRepository->countUsersByRoleIds(...),
|
||||
$userRoleRepository->countActiveUsersByRoleIds(...)
|
||||
);
|
||||
$rolePermissionCounts = $rolePermissionRepository->countPermissionsByRoleIds(array_keys($userCounts));
|
||||
|
||||
$rows = [];
|
||||
foreach ($result['rows'] as $row) {
|
||||
$roleId = (int) ($row['id'] ?? 0);
|
||||
$usersTotal = (int) ($roleUserCounts[$roleId] ?? 0);
|
||||
$usersActive = (int) ($roleActiveUserCounts[$roleId] ?? 0);
|
||||
$usersInactive = max(0, $usersTotal - $usersActive);
|
||||
$counts = $userCounts[$roleId] ?? ['active_users' => 0, 'inactive_users' => 0];
|
||||
$rows[] = [
|
||||
'id' => $row['id'] ?? null,
|
||||
'uuid' => $row['uuid'] ?? '',
|
||||
@@ -48,8 +41,8 @@ foreach ($result['rows'] as $row) {
|
||||
'description' => $row['description'] ?? '',
|
||||
'code' => $row['code'] ?? '',
|
||||
'active' => (int) ($row['active'] ?? 1),
|
||||
'active_users' => $usersActive,
|
||||
'inactive_users' => $usersInactive,
|
||||
'active_users' => $counts['active_users'],
|
||||
'inactive_users' => $counts['inactive_users'],
|
||||
'permission_count' => (int) ($rolePermissionCounts[$roleId] ?? 0),
|
||||
'created' => dt($row['created'] ?? ''),
|
||||
'modified' => dt($row['modified'] ?? ''),
|
||||
|
||||
@@ -20,8 +20,6 @@ $permissionRepository = app(\MintyPHP\Repository\Access\PermissionRepository::cl
|
||||
$rolePermissionRepository = app(\MintyPHP\Repository\Access\RolePermissionRepository::class);
|
||||
$userRoleRepository = app(\MintyPHP\Repository\Access\UserRoleRepository::class);
|
||||
$userWriteRepository = app(\MintyPHP\Repository\User\UserWriteRepository::class);
|
||||
$userAccountService = app(\MintyPHP\Service\User\UserAccountService::class);
|
||||
|
||||
$uuid = trim((string) ($id ?? ''));
|
||||
$editTarget = requestPathWithReturnTarget("admin/roles/edit/{$uuid}", $returnTarget);
|
||||
$role = $uuid !== '' ? app(\MintyPHP\Service\Access\RoleService::class)->findByUuid($uuid) : null;
|
||||
@@ -42,24 +40,7 @@ $capabilities = $contextDecision->attribute('capabilities', []);
|
||||
$canUpdateRole = is_array($capabilities) ? (bool) ($capabilities['can_edit_role'] ?? false) : false;
|
||||
$canDeleteRole = is_array($capabilities) ? (bool) ($capabilities['can_delete_role'] ?? false) : false;
|
||||
|
||||
$creatorId = (int) ($role['created_by'] ?? 0);
|
||||
if ($creatorId > 0) {
|
||||
$creator = $userAccountService->findById($creatorId);
|
||||
if ($creator) {
|
||||
$creatorName = trim(($creator['first_name'] ?? '') . ' ' . ($creator['last_name'] ?? ''));
|
||||
$role['created_by_label'] = $creatorName !== '' ? $creatorName : ($creator['email'] ?? '');
|
||||
$role['created_by_uuid'] = $creator['uuid'] ?? null;
|
||||
}
|
||||
}
|
||||
$modifierId = (int) ($role['modified_by'] ?? 0);
|
||||
if ($modifierId > 0) {
|
||||
$modifier = $userAccountService->findById($modifierId);
|
||||
if ($modifier) {
|
||||
$modifierName = trim(($modifier['first_name'] ?? '') . ' ' . ($modifier['last_name'] ?? ''));
|
||||
$role['modified_by_label'] = $modifierName !== '' ? $modifierName : ($modifier['email'] ?? '');
|
||||
$role['modified_by_uuid'] = $modifier['uuid'] ?? null;
|
||||
}
|
||||
}
|
||||
app(\MintyPHP\Service\Audit\AuditMetadataEnricher::class)->enrich($role);
|
||||
|
||||
$errorBag = formErrors();
|
||||
$errors = [];
|
||||
|
||||
@@ -25,23 +25,19 @@ $tenantAvatarService = app(\MintyPHP\Service\Tenant\TenantAvatarService::class);
|
||||
$settingsDefaultsGateway = app(\MintyPHP\Service\Settings\SettingsDefaultsGateway::class);
|
||||
$settingsAppGateway = app(\MintyPHP\Service\Settings\SettingsAppGateway::class);
|
||||
|
||||
$gridUserCountEnricher = app(\MintyPHP\Service\Data\GridUserCountEnricher::class);
|
||||
$fetchRows = static function (
|
||||
array $tenantRows,
|
||||
array $themes,
|
||||
string $globalDefaultTheme
|
||||
) use ($userTenantRepository, $tenantMicrosoftAuthRepository, $tenantAvatarService, $settingsDefaultsGateway): array {
|
||||
$tenantIds = [];
|
||||
foreach ($tenantRows as $tenantRow) {
|
||||
$tenantId = (int) ($tenantRow['id'] ?? 0);
|
||||
if ($tenantId > 0) {
|
||||
$tenantIds[] = $tenantId;
|
||||
}
|
||||
}
|
||||
$tenantIds = array_values(array_unique($tenantIds));
|
||||
|
||||
) use ($userTenantRepository, $tenantMicrosoftAuthRepository, $tenantAvatarService, $settingsDefaultsGateway, $gridUserCountEnricher): array {
|
||||
$userCounts = $gridUserCountEnricher->computeCounts(
|
||||
$tenantRows,
|
||||
$userTenantRepository->countUsersByTenantIds(...),
|
||||
$userTenantRepository->countActiveUsersByTenantIds(...)
|
||||
);
|
||||
$tenantIds = array_keys($userCounts);
|
||||
$tenantSsoMap = $tenantMicrosoftAuthRepository->listByTenantIds($tenantIds);
|
||||
$tenantUserCounts = $userTenantRepository->countUsersByTenantIds($tenantIds);
|
||||
$tenantActiveUserCounts = $userTenantRepository->countActiveUsersByTenantIds($tenantIds);
|
||||
|
||||
$defaultTenantId = $settingsDefaultsGateway->getDefaultTenantId();
|
||||
$rows = [];
|
||||
@@ -67,9 +63,7 @@ $fetchRows = static function (
|
||||
$tenantSso = $tenantSsoMap[$tenantId] ?? null;
|
||||
$ssoEnabled = !empty($tenantSso['enabled']);
|
||||
$ssoMicrosoftOnly = $ssoEnabled && !empty($tenantSso['enforce_microsoft_login']);
|
||||
$usersTotal = (int) ($tenantUserCounts[$tenantId] ?? 0);
|
||||
$usersActive = (int) ($tenantActiveUserCounts[$tenantId] ?? 0);
|
||||
$usersInactive = max(0, $usersTotal - $usersActive);
|
||||
$counts = $userCounts[$tenantId] ?? ['active_users' => 0, 'inactive_users' => 0];
|
||||
$tenantStatus = TenantStatus::normalizeOr((string) ($row['status'] ?? ''), TenantStatus::Active);
|
||||
$tenantUuid = (string) ($row['uuid'] ?? '');
|
||||
|
||||
@@ -91,8 +85,8 @@ $fetchRows = static function (
|
||||
'enabled' => $ssoEnabled ? 1 : 0,
|
||||
'microsoft_only' => $ssoMicrosoftOnly ? 1 : 0,
|
||||
],
|
||||
'active_users' => $usersActive,
|
||||
'inactive_users' => $usersInactive,
|
||||
'active_users' => $counts['active_users'],
|
||||
'inactive_users' => $counts['inactive_users'],
|
||||
'created' => dt($row['created'] ?? ''),
|
||||
'modified' => dt($row['modified'] ?? ''),
|
||||
'has_avatar' => $tenantUuid !== '' && $tenantAvatarService->hasAvatar($tenantUuid),
|
||||
|
||||
@@ -19,8 +19,6 @@ $currentUserId = (int) ($session['user']['id'] ?? 0);
|
||||
if ($currentUserId > 0) {
|
||||
app(\MintyPHP\Service\Access\PermissionService::class)->getUserPermissions($currentUserId);
|
||||
}
|
||||
$userAccountService = app(\MintyPHP\Service\User\UserAccountService::class);
|
||||
|
||||
$uuid = trim((string) ($id ?? ''));
|
||||
$editTarget = requestPathWithReturnTarget("admin/tenants/edit/{$uuid}", $returnTarget);
|
||||
$tenant = $uuid !== '' ? app(\MintyPHP\Service\Tenant\TenantService::class)->findByUuid($uuid) : null;
|
||||
@@ -69,33 +67,7 @@ $customFieldDefinitions = $canManageCustomFields
|
||||
: [];
|
||||
$ssoConfig = $canManageSso ? $tenantSsoService->getTenantMicrosoftAuth($tenantId) : [];
|
||||
$ssoUiState = $canManageSso ? $tenantSsoService->buildMicrosoftUiState($tenantId, $ssoConfig) : [];
|
||||
$creatorId = (int) ($tenant['created_by'] ?? 0);
|
||||
if ($creatorId > 0) {
|
||||
$creator = $userAccountService->findById($creatorId);
|
||||
if ($creator) {
|
||||
$creatorName = trim(($creator['first_name'] ?? '') . ' ' . ($creator['last_name'] ?? ''));
|
||||
$tenant['created_by_label'] = $creatorName !== '' ? $creatorName : ($creator['email'] ?? '');
|
||||
$tenant['created_by_uuid'] = $creator['uuid'] ?? null;
|
||||
}
|
||||
}
|
||||
$modifierId = (int) ($tenant['modified_by'] ?? 0);
|
||||
if ($modifierId > 0) {
|
||||
$modifier = $userAccountService->findById($modifierId);
|
||||
if ($modifier) {
|
||||
$modifierName = trim(($modifier['first_name'] ?? '') . ' ' . ($modifier['last_name'] ?? ''));
|
||||
$tenant['modified_by_label'] = $modifierName !== '' ? $modifierName : ($modifier['email'] ?? '');
|
||||
$tenant['modified_by_uuid'] = $modifier['uuid'] ?? null;
|
||||
}
|
||||
}
|
||||
$statusChangedById = (int) ($tenant['status_changed_by'] ?? 0);
|
||||
if ($statusChangedById > 0) {
|
||||
$statusChanger = $userAccountService->findById($statusChangedById);
|
||||
if ($statusChanger) {
|
||||
$statusChangerName = trim(($statusChanger['first_name'] ?? '') . ' ' . ($statusChanger['last_name'] ?? ''));
|
||||
$tenant['status_changed_by_label'] = $statusChangerName !== '' ? $statusChangerName : ($statusChanger['email'] ?? '');
|
||||
$tenant['status_changed_by_uuid'] = $statusChanger['uuid'] ?? null;
|
||||
}
|
||||
}
|
||||
app(\MintyPHP\Service\Audit\AuditMetadataEnricher::class)->enrich($tenant, ['created_by', 'modified_by', 'status_changed_by']);
|
||||
|
||||
$errorBag = formErrors();
|
||||
$errors = [];
|
||||
|
||||
@@ -87,33 +87,7 @@ if (!$canViewPage) {
|
||||
}
|
||||
|
||||
if ($canViewUserMeta || $canViewUserAudit) {
|
||||
$creatorId = (int) ($user['created_by'] ?? 0);
|
||||
if ($creatorId > 0) {
|
||||
$creator = $userAccountService->findById($creatorId);
|
||||
if ($creator) {
|
||||
$creatorName = trim(($creator['first_name'] ?? '') . ' ' . ($creator['last_name'] ?? ''));
|
||||
$user['created_by_label'] = $creatorName !== '' ? $creatorName : ($creator['email'] ?? '');
|
||||
$user['created_by_uuid'] = $creator['uuid'] ?? null;
|
||||
}
|
||||
}
|
||||
$modifierId = (int) ($user['modified_by'] ?? 0);
|
||||
if ($modifierId > 0) {
|
||||
$modifier = $userAccountService->findById($modifierId);
|
||||
if ($modifier) {
|
||||
$modifierName = trim(($modifier['first_name'] ?? '') . ' ' . ($modifier['last_name'] ?? ''));
|
||||
$user['modified_by_label'] = $modifierName !== '' ? $modifierName : ($modifier['email'] ?? '');
|
||||
$user['modified_by_uuid'] = $modifier['uuid'] ?? null;
|
||||
}
|
||||
}
|
||||
$activeChangedById = (int) ($user['active_changed_by'] ?? 0);
|
||||
if ($activeChangedById > 0) {
|
||||
$activeChanger = $userAccountService->findById($activeChangedById);
|
||||
if ($activeChanger) {
|
||||
$activeChangerName = trim(($activeChanger['first_name'] ?? '') . ' ' . ($activeChanger['last_name'] ?? ''));
|
||||
$user['active_changed_by_label'] = $activeChangerName !== '' ? $activeChangerName : ($activeChanger['email'] ?? '');
|
||||
$user['active_changed_by_uuid'] = $activeChanger['uuid'] ?? null;
|
||||
}
|
||||
}
|
||||
app(\MintyPHP\Service\Audit\AuditMetadataEnricher::class)->enrich($user, ['created_by', 'modified_by', 'active_changed_by']);
|
||||
}
|
||||
|
||||
$errorBag = formErrors();
|
||||
|
||||
92
tests/Service/Audit/AuditMetadataEnricherTest.php
Normal file
92
tests/Service/Audit/AuditMetadataEnricherTest.php
Normal file
@@ -0,0 +1,92 @@
|
||||
<?php
|
||||
|
||||
namespace MintyPHP\Tests\Service\Audit;
|
||||
|
||||
use MintyPHP\Repository\User\UserReadRepositoryInterface;
|
||||
use MintyPHP\Service\Audit\AuditMetadataEnricher;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class AuditMetadataEnricherTest extends TestCase
|
||||
{
|
||||
private function newEnricher(UserReadRepositoryInterface $userReadRepository): AuditMetadataEnricher
|
||||
{
|
||||
return new AuditMetadataEnricher($userReadRepository);
|
||||
}
|
||||
|
||||
public function testEnrichResolvesCreatedByAndModifiedBy(): void
|
||||
{
|
||||
$repo = $this->createMock(UserReadRepositoryInterface::class);
|
||||
$repo->method('find')->willReturnMap([
|
||||
[10, ['first_name' => 'Alice', 'last_name' => 'Smith', 'email' => 'alice@example.com', 'uuid' => 'uuid-10']],
|
||||
[20, ['first_name' => 'Bob', 'last_name' => 'Jones', 'email' => 'bob@example.com', 'uuid' => 'uuid-20']],
|
||||
]);
|
||||
|
||||
$entity = ['created_by' => 10, 'modified_by' => 20];
|
||||
$this->newEnricher($repo)->enrich($entity);
|
||||
|
||||
$this->assertSame('Alice Smith', $entity['created_by_label']);
|
||||
$this->assertSame('uuid-10', $entity['created_by_uuid']);
|
||||
$this->assertSame('Bob Jones', $entity['modified_by_label']);
|
||||
$this->assertSame('uuid-20', $entity['modified_by_uuid']);
|
||||
}
|
||||
|
||||
public function testEnrichFallsBackToEmailWhenNameEmpty(): void
|
||||
{
|
||||
$repo = $this->createMock(UserReadRepositoryInterface::class);
|
||||
$repo->method('find')->willReturn(['first_name' => '', 'last_name' => '', 'email' => 'no-name@example.com', 'uuid' => 'uuid-x']);
|
||||
|
||||
$entity = ['created_by' => 5];
|
||||
$this->newEnricher($repo)->enrich($entity, ['created_by']);
|
||||
|
||||
$this->assertSame('no-name@example.com', $entity['created_by_label']);
|
||||
$this->assertSame('uuid-x', $entity['created_by_uuid']);
|
||||
}
|
||||
|
||||
public function testEnrichSkipsZeroAndNegativeIds(): void
|
||||
{
|
||||
$repo = $this->createMock(UserReadRepositoryInterface::class);
|
||||
$repo->expects($this->never())->method('find');
|
||||
|
||||
$entity = ['created_by' => 0, 'modified_by' => -1];
|
||||
$this->newEnricher($repo)->enrich($entity);
|
||||
|
||||
$this->assertArrayNotHasKey('created_by_label', $entity);
|
||||
$this->assertArrayNotHasKey('modified_by_label', $entity);
|
||||
}
|
||||
|
||||
public function testEnrichSkipsMissingFields(): void
|
||||
{
|
||||
$repo = $this->createMock(UserReadRepositoryInterface::class);
|
||||
$repo->expects($this->never())->method('find');
|
||||
|
||||
$entity = [];
|
||||
$this->newEnricher($repo)->enrich($entity, ['created_by', 'modified_by']);
|
||||
|
||||
$this->assertArrayNotHasKey('created_by_label', $entity);
|
||||
}
|
||||
|
||||
public function testEnrichHandlesUserNotFound(): void
|
||||
{
|
||||
$repo = $this->createMock(UserReadRepositoryInterface::class);
|
||||
$repo->method('find')->willReturn(null);
|
||||
|
||||
$entity = ['created_by' => 999];
|
||||
$this->newEnricher($repo)->enrich($entity, ['created_by']);
|
||||
|
||||
$this->assertArrayNotHasKey('created_by_label', $entity);
|
||||
}
|
||||
|
||||
public function testEnrichWithCustomFields(): void
|
||||
{
|
||||
$repo = $this->createMock(UserReadRepositoryInterface::class);
|
||||
$repo->method('find')->willReturn(['first_name' => 'Admin', 'last_name' => '', 'email' => 'admin@co.com', 'uuid' => 'uuid-a']);
|
||||
|
||||
$entity = ['status_changed_by' => 7, 'active_changed_by' => 7];
|
||||
$this->newEnricher($repo)->enrich($entity, ['status_changed_by', 'active_changed_by']);
|
||||
|
||||
$this->assertSame('Admin', $entity['status_changed_by_label']);
|
||||
$this->assertSame('uuid-a', $entity['status_changed_by_uuid']);
|
||||
$this->assertSame('Admin', $entity['active_changed_by_label']);
|
||||
$this->assertSame('uuid-a', $entity['active_changed_by_uuid']);
|
||||
}
|
||||
}
|
||||
106
tests/Service/Data/GridUserCountEnricherTest.php
Normal file
106
tests/Service/Data/GridUserCountEnricherTest.php
Normal file
@@ -0,0 +1,106 @@
|
||||
<?php
|
||||
|
||||
namespace MintyPHP\Tests\Service\Data;
|
||||
|
||||
use MintyPHP\Service\Data\GridUserCountEnricher;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class GridUserCountEnricherTest extends TestCase
|
||||
{
|
||||
private function enricher(): GridUserCountEnricher
|
||||
{
|
||||
return new GridUserCountEnricher();
|
||||
}
|
||||
|
||||
public function testComputeCountsWithTypicalRows(): void
|
||||
{
|
||||
$rows = [
|
||||
['id' => 1, 'description' => 'A'],
|
||||
['id' => 2, 'description' => 'B'],
|
||||
];
|
||||
$countTotal = static fn (array $ids): array => [1 => 5, 2 => 3];
|
||||
$countActive = static fn (array $ids): array => [1 => 3, 2 => 1];
|
||||
|
||||
$result = $this->enricher()->computeCounts($rows, $countTotal, $countActive);
|
||||
|
||||
$this->assertSame(['active_users' => 3, 'inactive_users' => 2], $result[1]);
|
||||
$this->assertSame(['active_users' => 1, 'inactive_users' => 2], $result[2]);
|
||||
}
|
||||
|
||||
public function testComputeCountsWithEmptyRows(): void
|
||||
{
|
||||
$called = false;
|
||||
$countTotal = static function () use (&$called): array {
|
||||
$called = true;
|
||||
return [];
|
||||
};
|
||||
$countActive = static fn (): array => [];
|
||||
|
||||
$result = $this->enricher()->computeCounts([], $countTotal, $countActive);
|
||||
|
||||
$this->assertSame([], $result);
|
||||
$this->assertFalse($called, 'Count callables should not be invoked for empty rows');
|
||||
}
|
||||
|
||||
public function testComputeCountsDeduplicatesIds(): void
|
||||
{
|
||||
$rows = [
|
||||
['id' => 1],
|
||||
['id' => 1],
|
||||
['id' => 2],
|
||||
];
|
||||
$receivedIds = null;
|
||||
$countTotal = static function (array $ids) use (&$receivedIds): array {
|
||||
$receivedIds = $ids;
|
||||
return [1 => 10, 2 => 5];
|
||||
};
|
||||
$countActive = static fn (array $ids): array => [1 => 8, 2 => 3];
|
||||
|
||||
$this->enricher()->computeCounts($rows, $countTotal, $countActive);
|
||||
|
||||
$this->assertSame([1, 2], $receivedIds);
|
||||
}
|
||||
|
||||
public function testComputeCountsSkipsZeroIds(): void
|
||||
{
|
||||
$rows = [
|
||||
['id' => 0],
|
||||
['id' => -1],
|
||||
['id' => 3],
|
||||
];
|
||||
$receivedIds = null;
|
||||
$countTotal = static function (array $ids) use (&$receivedIds): array {
|
||||
$receivedIds = $ids;
|
||||
return [3 => 2];
|
||||
};
|
||||
$countActive = static fn (array $ids): array => [3 => 1];
|
||||
|
||||
$result = $this->enricher()->computeCounts($rows, $countTotal, $countActive);
|
||||
|
||||
$this->assertSame([3], $receivedIds);
|
||||
$this->assertCount(1, $result);
|
||||
$this->assertSame(['active_users' => 1, 'inactive_users' => 1], $result[3]);
|
||||
}
|
||||
|
||||
public function testComputeCountsWithZeroCounts(): void
|
||||
{
|
||||
$rows = [['id' => 5]];
|
||||
$countTotal = static fn (array $ids): array => [];
|
||||
$countActive = static fn (array $ids): array => [];
|
||||
|
||||
$result = $this->enricher()->computeCounts($rows, $countTotal, $countActive);
|
||||
|
||||
$this->assertSame(['active_users' => 0, 'inactive_users' => 0], $result[5]);
|
||||
}
|
||||
|
||||
public function testInactiveNeverNegative(): void
|
||||
{
|
||||
$rows = [['id' => 1]];
|
||||
$countTotal = static fn (array $ids): array => [1 => 2];
|
||||
$countActive = static fn (array $ids): array => [1 => 5];
|
||||
|
||||
$result = $this->enricher()->computeCounts($rows, $countTotal, $countActive);
|
||||
|
||||
$this->assertSame(0, $result[1]['inactive_users']);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user