Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/agent-task-governance.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,10 @@ jobs:

steps:
- name: Check out repository
uses: actions/checkout@v4
uses: actions/checkout@v5

- name: Set up Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@v5
with:
node-version: 26.7.0
cache: npm
Expand Down
8 changes: 4 additions & 4 deletions .github/workflows/desktop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ jobs:

steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v5

- name: Install Linux desktop dependencies
if: matrix.platform == 'ubuntu-22.04'
Expand All @@ -88,7 +88,7 @@ jobs:
sudo timeout --foreground 8m apt-get -o Acquire::Retries=3 -o Acquire::http::Timeout=60 -o Acquire::https::Timeout=60 install -y --no-install-recommends libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf xdg-utils

- name: Set up Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@v5
with:
node-version: 26.7.0
cache: npm
Expand Down Expand Up @@ -543,10 +543,10 @@ jobs:

steps:
- name: Checkout release metadata generator
uses: actions/checkout@v4
uses: actions/checkout@v5

- name: Set up Node.js for manifest generation
uses: actions/setup-node@v4
uses: actions/setup-node@v5
with:
node-version: 26.7.0

Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout Code
uses: actions/checkout@v4
uses: actions/checkout@v5

# - name: Setup PHP
# uses: shivammathur/setup-php@v2
Expand All @@ -30,7 +30,7 @@ jobs:
# run: composer install -q --no-ansi --no-interaction --no-scripts --no-progress --prefer-dist

- name: Set up Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@v5
with:
node-version: 26.7.0
cache: npm
Expand Down
8 changes: 4 additions & 4 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ jobs:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5

- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
Expand Down Expand Up @@ -47,7 +47,7 @@ jobs:
options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3

steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v5

- name: Setup PHP
uses: shivammathur/setup-php@v2
Expand Down Expand Up @@ -89,13 +89,13 @@ jobs:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5

- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable

- name: Set up Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@v5
with:
node-version: 26.7.0
cache: npm
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,8 @@
*/
class ModuleReadinessService
{
public const ONBOARDING_POLICY_VERSION = '2026-08-technology-commerce-v1';

/**
* The default profile supports a small technology retailer or technology
* organisation with a physical device store, warehouse, POS, finance, and
* purchasing flows. Optional delivery/project/manufacturing capabilities
* remain governed by their own module requirements.
*/
public const DEFAULT_ONBOARDING_PROFILE = 'technology_commerce';
public const ONBOARDING_POLICY_VERSION = '2026-08-profile-driven-setup-v1';
public const DEFAULT_ONBOARDING_PROFILE = 'guided_setup';

/** @var list<string> */
private const FOUNDATION_NODE_TYPES = ['COMP_CODE', 'CONTROLLING_AREA', 'COST_CENTER', 'PROFIT_CENTER'];
Expand All @@ -36,20 +29,27 @@ class ModuleReadinessService
/** @var list<string> */
private const CORE_STARTER_MODULES = ['sales', 'products', 'purchases', 'general_ledger'];

public function __construct(private readonly OrgStructureService $orgStructureService)
{
public function __construct(
private readonly OrgStructureService $orgStructureService,
private readonly OrganizationTemplateService $templates,
) {
}

public function evaluate(?int $userId = null): array
{
$templateProfile = $this->templates->profile();
$profileKey = (string) ($templateProfile['template_key'] ?? self::DEFAULT_ONBOARDING_PROFILE);
$coreOperatingNodeTypes = $this->coreOperatingNodeTypes($profileKey);
$requiresOperatingContext = $this->requiresOperatingContext($profileKey);
$starterModules = $this->starterModules($profileKey);
$integrityIssues = $this->orgStructureService->runIntegrityCheck();
$activeNodeTypes = $this->activeNodeTypes();
$context = $this->defaultContext($userId);
$workingUnit = $this->validateWorkingUnit($context);
$operatingStructure = $this->validateOperatingStructure($context, $integrityIssues);
$accountingReadiness = $this->accountingReadiness();
$foundation = $this->foundationReadiness($activeNodeTypes, $integrityIssues, $accountingReadiness);
$coreOperations = $this->coreOperationsReadiness($context, $activeNodeTypes, $integrityIssues, $operatingStructure);
$coreOperations = $this->coreOperationsReadiness($context, $activeNodeTypes, $integrityIssues, $operatingStructure, $coreOperatingNodeTypes, $requiresOperatingContext);
$hasActiveStructure = $activeNodeTypes !== [];
$modules = Module::query()->orderBy('category')->orderBy('sort_order')->get();

Expand Down Expand Up @@ -132,7 +132,7 @@ public function evaluate(?int $userId = null): array
$baselineReady = $foundation['ready'] && $coreOperations['ready'];
$onboarding = [
'policy_version' => self::ONBOARDING_POLICY_VERSION,
'profile' => self::DEFAULT_ONBOARDING_PROFILE,
'profile' => $profileKey,
'baseline_ready' => $baselineReady,
'phases' => [
'foundation' => $foundation,
Expand All @@ -141,7 +141,7 @@ public function evaluate(?int $userId = null): array
'next_phase' => !$foundation['ready']
? 'foundation'
: (!$coreOperations['ready'] ? 'core_operations' : 'module_activation'),
'starter_module_keys' => self::CORE_STARTER_MODULES,
'starter_module_keys' => $starterModules,
];

return [
Expand Down Expand Up @@ -371,15 +371,17 @@ private function foundationReadiness(array $activeNodeTypes, array $integrityIss
* hierarchy, warehouse, and POS terminal. The organisation can later add
* project, manufacturing, and HR structures without reopening this gate.
*/
private function coreOperationsReadiness(?OperatingContext $context, array $activeNodeTypes, array $integrityIssues, array $operatingStructure): array
private function coreOperationsReadiness(?OperatingContext $context, array $activeNodeTypes, array $integrityIssues, array $operatingStructure, array $coreOperatingNodeTypes, bool $requiresOperatingContext): array
{
$missingNodeTypes = array_values(array_diff(self::CORE_OPERATING_NODE_TYPES, $activeNodeTypes));
$relevantIssues = $this->relevantIntegrityIssues($integrityIssues, self::CORE_OPERATING_NODE_TYPES);
$validNodeTypes = $this->validCoreOperatingNodeTypes($context);
$unlinkedNodeTypes = array_values(array_diff(self::CORE_OPERATING_NODE_TYPES, $validNodeTypes));
$missingNodeTypes = array_values(array_diff($coreOperatingNodeTypes, $activeNodeTypes));
$relevantIssues = $this->relevantIntegrityIssues($integrityIssues, $coreOperatingNodeTypes);
$validNodeTypes = $requiresOperatingContext
? $this->validCoreOperatingNodeTypes($context, $coreOperatingNodeTypes)
: $coreOperatingNodeTypes;
$unlinkedNodeTypes = array_values(array_diff($coreOperatingNodeTypes, $validNodeTypes));
$reasonCodes = [];

if (!$operatingStructure['ready']) {
if ($requiresOperatingContext && !$operatingStructure['ready']) {
$reasonCodes = [...$reasonCodes, ...$operatingStructure['reason_codes']];
}
if ($missingNodeTypes !== []) {
Expand All @@ -397,7 +399,7 @@ private function coreOperationsReadiness(?OperatingContext $context, array $acti
return [
'id' => 'core_operations',
'ready' => $reasonCodes === [],
'required_node_types' => self::CORE_OPERATING_NODE_TYPES,
'required_node_types' => $coreOperatingNodeTypes,
'missing_node_types' => $missingNodeTypes,
'unlinked_node_types' => $unlinkedNodeTypes,
'integrity_issue_count' => count($relevantIssues),
Expand All @@ -406,7 +408,7 @@ private function coreOperationsReadiness(?OperatingContext $context, array $acti
}

/** @return list<string> */
private function validCoreOperatingNodeTypes(?OperatingContext $context): array
private function validCoreOperatingNodeTypes(?OperatingContext $context, array $coreOperatingNodeTypes): array
{
if (!$context?->org_node_uuid) {
return [];
Expand All @@ -419,7 +421,7 @@ private function validCoreOperatingNodeTypes(?OperatingContext $context): array
}

$valid = [];
foreach (self::CORE_OPERATING_NODE_TYPES as $nodeType) {
foreach ($coreOperatingNodeTypes as $nodeType) {
$hasCompanyLinkedNode = StructureNode::query()
->where('node_type_id', $nodeType)
->where('status', 'active')
Expand All @@ -438,6 +440,36 @@ private function validCoreOperatingNodeTypes(?OperatingContext $context): array
return $valid;
}

/** @return list<string> */
private function coreOperatingNodeTypes(string $profileKey): array
{
return match ($profileKey) {
'single_store_retail', 'multi_site_retail', 'manufacturing' => ['PLANT', 'STORAGE_LOC', 'SALES_ORG', 'PURCH_ORG'],
'single_store_service', 'professional_services' => ['SALES_ORG'],
// Enterprise blueprints deliberately defer operational scope until
// each legal entity/site is governed through the advanced designer.
'enterprise_blueprint' => [],
default => [],
};
}

private function requiresOperatingContext(string $profileKey): bool
{
return in_array($profileKey, ['single_store_retail', 'multi_site_retail', 'manufacturing'], true);
}

/** @return list<string> */
private function starterModules(string $profileKey): array
{
return match ($profileKey) {
'single_store_retail', 'multi_site_retail' => ['sales', 'products', 'purchases', 'general_ledger'],
'manufacturing' => ['sales', 'products', 'purchases', 'general_ledger'],
'single_store_service', 'professional_services' => ['sales', 'general_ledger'],
'enterprise_blueprint' => ['general_ledger'],
default => self::CORE_STARTER_MODULES,
};
}

private function activeNodeTypes(): array
{
return StructureNode::query()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,10 @@ final class ModuleSelectionService
*/
public const CORE_STARTER_MODULES = ['sales', 'products', 'purchases', 'general_ledger'];

public function __construct(private readonly ModuleReadinessService $readiness)
{
public function __construct(
private readonly ModuleReadinessService $readiness,
private readonly OrganizationTemplateService $templates,
) {
}

/** @return list<string> */
Expand Down Expand Up @@ -204,6 +206,7 @@ public function state(?int $userId = null): array
'active_module_keys' => $activeBusiness->pluck('module_key')->all(),
'pending_module_keys' => $pendingBusiness->pluck('module_key')->all(),
'modules' => $modules->all(),
'organization_template' => $this->templates->state(),
];
}

Expand Down
Loading
Loading