import { Injectable, computed, inject, signal } from '@angular/core'; import { take } from 'rxjs/operators'; import { PARTNER_HIERARCHY_GATEWAY } from '../../../../core/partner-hierarchy/services/partner-hierarchy-gateway.token'; import { PartnerCredential } from '../../../../core/partner-hierarchy/models/partner-credential.model'; import { Environment, NodeLevel, ProvisioningNode, childLevelOf, childrenOf, } from '../../../../core/partner-hierarchy/models/provisioning-node.model'; /** One node plus its depth, flattened for rendering a tree as a list. */ export interface HierarchyRow { node: ProvisioningNode; depth: number; /** The level a child of this node would be, or null at the leaf. */ childLevel: NodeLevel | null; } /** * Read/act surface for the partner-provisioned hierarchy. * Contract: docs/backend/PARTNER-PROVISIONING-API-CONTRACT.md §4. * * The company id is currently a constant: Company creation is out of band * (§4.3 - partners provision INSIDE a company we create commercially), and * there is no company-selector UI yet. When one exists this becomes an input. */ const DEFAULT_COMPANY_ID = 'cmp_1'; @Injectable({ providedIn: 'root' }) export class AdminPartnerHierarchyFacade { private readonly gateway = inject(PARTNER_HIERARCHY_GATEWAY); readonly companyId = signal(DEFAULT_COMPANY_ID); readonly environment = signal('TEST'); readonly nodes = signal([]); readonly credentials = signal([]); readonly loading = signal(false); readonly error = signal(null); /** * Depth-first flattening, so the template renders a tree without recursion. * Roots are nodes whose parent is absent from the loaded set - not * necessarily level 'company', so a partially-loaded subtree still renders * instead of coming back empty. */ readonly rows = computed(() => { const all = this.nodes(); const ids = new Set(all.map(n => n.id)); const roots = all .filter(n => n.parentId === null || !ids.has(n.parentId)) .sort((a, b) => a.displayName.localeCompare(b.displayName)); const rows: HierarchyRow[] = []; const walk = (node: ProvisioningNode, depth: number): void => { rows.push({ node, depth, childLevel: childLevelOf(node.level) }); for (const child of childrenOf(node.id, all)) { walk(child, depth + 1); } }; roots.forEach(root => walk(root, 0)); return rows; }); readonly paymentPointCount = computed( () => this.nodes().filter(n => n.level === 'payment_point').length, ); readonly suspendedCount = computed( () => this.nodes().filter(n => n.status === 'suspended').length, ); load(): void { this.loading.set(true); this.error.set(null); this.gateway .loadHierarchy(this.companyId(), this.environment()) .pipe(take(1)) .subscribe({ next: nodes => { this.nodes.set(nodes); this.loading.set(false); }, error: () => { // Empty the tree rather than leaving the previous environment's // nodes on screen labelled as the newly selected one. this.nodes.set([]); this.loading.set(false); this.error.set('Could not load the hierarchy.'); }, }); this.gateway .loadCredentials(this.companyId()) .pipe(take(1)) .subscribe({ next: credentials => this.credentials.set(credentials), error: () => this.credentials.set([]), }); } setEnvironment(environment: Environment): void { if (environment === this.environment()) { return; } this.environment.set(environment); this.load(); } suspend(nodeId: string): void { this.act(this.gateway.setStatus(nodeId, 'suspended')); } activate(nodeId: string): void { this.act(this.gateway.setStatus(nodeId, 'active')); } /** Terminal and cascading - callers must confirm with the operator first. */ disable(nodeId: string): void { this.act(this.gateway.disable(nodeId)); } private act(action: ReturnType): void { this.error.set(null); action.pipe(take(1)).subscribe({ // Reload rather than patching one node locally: disable cascades to // descendants server-side, so a local patch would leave children // showing a status they no longer have. next: () => this.load(), error: (err: Error) => this.error.set(err.message || 'Action failed.'), }); } }