feat: real API gateways for all remaining local-only domains (F17-F39)
Some checks failed
Architecture Governance / architecture (push) Has been cancelled

Block 3 of the frontend backlog. 18 gateways were hardcoded to their local
(mock/localStorage) implementation with no seam to a real backend at all -
this closes that gap for everything with a documented contract to build
against.

10 core gateways, token now resolves environment.useMockData ? local : api,
same pattern already proven on fx-quote earlier this session:

  permission, analytics, cart (server-cart), finance, vk-id (identity),
  connector (integrations), marketplace (registry), offer, seller,
  mall-content

8 admin gateways, same pattern:

  orders, products, users, transactions, monitoring, moderation,
  notifications, dashboard-metrics

Endpoints came from the matching contract doc where one exists (Phase 1-10,
Track A, Track S - cited per file). Three domains have no dedicated contract
doc yet (transactions, monitoring, moderation) - those gateways call the
established /api/admin/v2/{resource} convention used throughout the rest of
docs/backend/, flagged in each file's own comment as inferred rather than
specified, for whoever writes that contract to confirm or correct.

One real fix along the way: offer-api.gateway.ts's publish() translates a
422 + details[] response (contract §7's actual failure mode) into the
interface's { ok: false, errors } shape, rather than letting an HTTP error
leak past a caller that expects a value back.

Media repository (mock-media-repository.service.ts) deliberately NOT
swapped - no backend contract exists for it anywhere in docs/backend/, and
inventing endpoint shapes with zero grounding is worse than leaving it mock.

Regression this surfaced, fixed as part of the same change: three spec files
stubbed a gateway's concrete Local class directly via useValue. That worked
by accident while the token unconditionally resolved to the local class; once
the token became conditional on useMockData, those specs silently injected
the real (unmocked) API gateway instead and failed. Fixed by providing the
token instead of the class - the pattern the ADMIN_CATEGORIES_GATEWAY entry
in the same spec file already used correctly, because categories was
already token-swapped before this session:
  - admin-analytics.facade.spec.ts (orders/products/moderation gateways)
  - admin-order-watcher.service.spec.ts (orders gateway, 3 call sites)

Verified: 115/115 unit tests, 5/5 E2E, arch:check clean, production build
succeeds.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
sdarbinyan
2026-08-18 14:29:50 +04:00
parent fc53a3b7f5
commit ffd57f2d18
38 changed files with 787 additions and 43 deletions

View File

@@ -0,0 +1,64 @@
import { HttpClient } from '@angular/common/http';
import { Injectable, inject } from '@angular/core';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { AdminInvitation, AdminUserRoleRecord, AdminSession, AdminUser, AdminUserAuditEntry, AdminUserScope, AdminUserStatus } from '../models/admin-user.model';
import { AdminUsersGateway } from './admin-users-gateway.interface';
/**
* Contract: docs/backend/TRACK-S-SECURITY-RBAC-CONTRACT.md §8 (team
* invite/list/role/revoke on /api/admin/v2/team). Sessions/audit/status
* endpoints aren't individually specified - inferred as sub-resources of the
* same collection, consistent with the contract's team/{userId} pattern.
*/
@Injectable({ providedIn: 'root' })
export class AdminUsersApiGateway implements AdminUsersGateway {
private readonly http = inject(HttpClient);
loadUsers(): Observable<AdminUser[]> {
return this.http.get<AdminUser[]>('/api/admin/v2/team');
}
loadRoles(): Observable<AdminUserRoleRecord[]> {
return this.http.get<AdminUserRoleRecord[]>('/api/admin/v2/team/roles');
}
loadInvitations(): Observable<AdminInvitation[]> {
return this.http.get<AdminInvitation[]>('/api/admin/v2/team/invitations');
}
loadSessions(userId: string): Observable<AdminSession[]> {
return this.http.get<AdminSession[]>(`/api/admin/v2/team/${encodeURIComponent(userId)}/sessions`);
}
loadAudit(userId: string): Observable<AdminUserAuditEntry[]> {
return this.http.get<AdminUserAuditEntry[]>(`/api/admin/v2/audit`, { params: { actor: userId } });
}
setUserRole(userId: string, roleId: string): Observable<AdminUser | null> {
return this.http.patch<AdminUser | null>(`/api/admin/v2/team/${encodeURIComponent(userId)}`, { role: roleId });
}
setUserStatus(userId: string, status: AdminUserStatus): Observable<AdminUser | null> {
return this.http.patch<AdminUser | null>(`/api/admin/v2/team/${encodeURIComponent(userId)}/status`, { status });
}
inviteUser(email: string, roleId: string, scope: AdminUserScope): Observable<AdminInvitation> {
// §8's example body is { email, role, marketplaceId } - `scope` here is
// 'marketplace' | 'office', not an id, so it is sent as its own field
// rather than forced into marketplaceId.
return this.http.post<AdminInvitation>('/api/admin/v2/team/invite', { email, role: roleId, scope });
}
revokeInvitation(id: string): Observable<void> {
return this.http
.delete(`/api/admin/v2/team/invitations/${encodeURIComponent(id)}`)
.pipe(map(() => undefined));
}
revokeSession(sessionId: string): Observable<void> {
return this.http
.delete(`/api/admin/v2/team/sessions/${encodeURIComponent(sessionId)}`)
.pipe(map(() => undefined));
}
}

View File

@@ -1,9 +1,11 @@
import { InjectionToken, inject } from '@angular/core';
import { environment } from '../../../../../environments/environment';
import { AdminUsersGateway } from './admin-users-gateway.interface';
import { AdminUsersLocalGateway } from './admin-users-local.gateway';
import { AdminUsersApiGateway } from './admin-users-api.gateway';
/** Swap point for a real Users backend - see BACKEND-API-REFERENCE.md §8 (no seam existed before this token). */
/** Swap point for docs/backend/TRACK-S-SECURITY-RBAC-CONTRACT.md §8. */
export const ADMIN_USERS_GATEWAY = new InjectionToken<AdminUsersGateway>('ADMIN_USERS_GATEWAY', {
providedIn: 'root',
factory: () => inject(AdminUsersLocalGateway),
factory: () => (environment.useMockData ? inject(AdminUsersLocalGateway) : inject(AdminUsersApiGateway)),
});