Merge improvements/fork-harvest into main
Some checks failed
Architecture Governance / architecture (push) Has been cancelled
Deploy Frontend / deploy (push) Has been cancelled

Fork-harvest brings: the ip-api.com geo fix, credential bundle scan,
mock gateways out of production, JIT compiler dropped (1.55->1.04 MB),
host hardening, provider-agnostic identity + VK/Yandex + account linking,
and the backend contracts consolidated into one BACKEND-INTEGRATION.md.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

# Conflicts:
#	docs/backend/BACKEND-HANDOFF.md
#	docs/backend/TRACK-S-SECURITY-RBAC-CONTRACT.md
This commit is contained in:
sdarbinyan
2026-08-22 16:19:55 +04:00
77 changed files with 1659 additions and 3023 deletions

View File

@@ -16,6 +16,7 @@ import { MockMediaRepository } from './core/media/mock-media-repository.service'
import { ApiConfigService } from './core/config/api-config.service';
import { TenantResolverService } from './core/config/tenant-resolver.service';
import { environment } from '../environments/environment';
import { MOCK_GATEWAY_PROVIDERS } from './mock-gateway.providers';
export const appConfig: ApplicationConfig = {
providers: [
@@ -72,6 +73,9 @@ export const appConfig: ApplicationConfig = {
provideServiceWorker('ngsw-worker.js', {
enabled: !isDevMode(),
registrationStrategy: 'registerWhenStable:30000'
})
}),
// Empty in production - the file is swapped at build time so no mock
// gateway is even importable there. See mock-gateway.providers.ts.
...MOCK_GATEWAY_PROVIDERS
]
};

View File

@@ -0,0 +1,10 @@
<button
type="button"
class="social-login-button"
[class]="'social-login-button--' + provider()"
[disabled]="loading()"
(click)="startLogin()"
>
<app-icon name="user" [size]="18" />
<span>{{ label() }}</span>
</button>

View File

@@ -1,4 +1,4 @@
.vk-id-login {
.social-login-button {
display: flex;
align-items: center;
justify-content: center;

View File

@@ -0,0 +1,56 @@
import { ChangeDetectionStrategy, Component, computed, inject, input, signal } from '@angular/core';
import { take } from 'rxjs/operators';
import { SOCIAL_IDENTITY_GATEWAY } from '../../core/identity/services/social-identity-gateway.token';
import { SocialProvider } from '../../core/identity/services/social-identity-gateway.interface';
import { IconComponent } from '../../shared/ui/icon/icon.component';
const PROVIDER_LABEL: Record<SocialProvider, string> = {
vk: 'Continue with VK ID',
yandex: 'Continue with Yandex ID',
};
/**
* One button per social provider, per v3.1 §14 (VK ID is the primary
* storefront social login; Yandex ID is the second instance of the same
* flow, not a separate integration).
*
* Deliberately not spliced into TelegramLoginComponent's dialog yet. That
* component is the live customer login surface, and adding providers to it
* belongs in the pass that also demotes Telegram to one ExternalIdentity
* among several (FH-4.6) - not before a real OAuth application exists to
* test against.
*/
@Component({
selector: 'app-social-login-button',
standalone: true,
imports: [IconComponent],
templateUrl: './social-login-button.component.html',
styleUrls: ['./social-login-button.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class SocialLoginButtonComponent {
private readonly gateway = inject(SOCIAL_IDENTITY_GATEWAY);
readonly provider = input.required<SocialProvider>();
/** Where to land after the callback. Validated backend-side. */
readonly returnTo = input<string | undefined>(undefined);
readonly loading = signal(false);
readonly label = computed(() => PROVIDER_LABEL[this.provider()]);
startLogin(): void {
this.loading.set(true);
this.gateway
.getAuthorizeUrl(this.provider(), this.returnTo())
.pipe(take(1))
.subscribe({
next: url => {
this.loading.set(false);
if (typeof window !== 'undefined') {
window.location.href = url;
}
},
error: () => this.loading.set(false),
});
}
}

View File

@@ -1,4 +0,0 @@
<button type="button" class="vk-id-login" [disabled]="loading()" (click)="startLogin()">
<app-icon name="user" [size]="18" />
<span>Continue with VK ID</span>
</button>

View File

@@ -1,37 +0,0 @@
import { ChangeDetectionStrategy, Component, inject, signal } from '@angular/core';
import { CommonModule } from '@angular/common';
import { take } from 'rxjs/operators';
import { VK_ID_GATEWAY } from '../../core/identity/services/vk-id-gateway.token';
import { IconComponent } from '../../shared/ui/icon/icon.component';
/**
* Standalone VK ID login button, per Sprint 0.1 ("do all after vk" - VK ID
* is the primary storefront social login going forward, per v3.1 §14).
* Deliberately not wired into TelegramLoginComponent's dialog yet - that
* component is the live, working customer/admin login surface, and
* splicing a second provider into it needs its own careful pass once a
* real VK OAuth app exists to test against, not a mock-backed bolt-on.
*/
@Component({
selector: 'app-vk-id-login',
standalone: true,
imports: [CommonModule, IconComponent],
templateUrl: './vk-id-login.component.html',
styleUrls: ['./vk-id-login.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class VkIdLoginComponent {
private readonly gateway = inject(VK_ID_GATEWAY);
readonly loading = signal(false);
startLogin(): void {
this.loading.set(true);
this.gateway.getAuthorizeUrl().pipe(take(1)).subscribe(url => {
this.loading.set(false);
if (typeof window !== 'undefined') {
window.location.href = url;
}
});
}
}

View File

@@ -1,11 +1,9 @@
import { InjectionToken, inject } from '@angular/core';
import { environment } from '../../../../environments/environment';
import { AnalyticsGateway } from './analytics-gateway.interface';
import { AnalyticsLocalGateway } from './analytics-local.gateway';
import { AnalyticsApiGateway } from './analytics-api.gateway';
/** Swap point for docs/backend/TRACK-A-ANALYTICS-CONTRACT.md §1. */
export const ANALYTICS_GATEWAY = new InjectionToken<AnalyticsGateway>('ANALYTICS_GATEWAY', {
providedIn: 'root',
factory: () => (environment.useMockData ? inject(AnalyticsLocalGateway) : inject(AnalyticsApiGateway)),
factory: () => inject(AnalyticsApiGateway),
});

View File

@@ -1,11 +1,9 @@
import { InjectionToken, inject } from '@angular/core';
import { environment } from '../../../../environments/environment';
import { ServerCartGateway } from './server-cart-gateway.interface';
import { ServerCartLocalGateway } from './server-cart-local.gateway';
import { ServerCartApiGateway } from './server-cart-api.gateway';
/** Swap point for docs/backend/PHASE-6-CART-CHECKOUT-CONTRACT.md §3, §5. */
export const SERVER_CART_GATEWAY = new InjectionToken<ServerCartGateway>('SERVER_CART_GATEWAY', {
providedIn: 'root',
factory: () => (environment.useMockData ? inject(ServerCartLocalGateway) : inject(ServerCartApiGateway)),
factory: () => inject(ServerCartApiGateway),
});

View File

@@ -1,11 +1,9 @@
import { InjectionToken, inject } from '@angular/core';
import { environment } from '../../../../environments/environment';
import { MallContentGateway } from './mall-content-gateway.interface';
import { MallContentLocalGateway } from './mall-content-local.gateway';
import { MallContentApiGateway } from './mall-content-api.gateway';
/** Swap point for docs/backend/PHASE-10-CONTENT-MODULES-CONTRACT.md. */
export const MALL_CONTENT_GATEWAY = new InjectionToken<MallContentGateway>('MALL_CONTENT_GATEWAY', {
providedIn: 'root',
factory: () => (environment.useMockData ? inject(MallContentLocalGateway) : inject(MallContentApiGateway)),
factory: () => inject(MallContentApiGateway),
});

View File

@@ -1,11 +1,9 @@
import { InjectionToken, inject } from '@angular/core';
import { environment } from '../../../../environments/environment';
import { FinanceGateway } from './finance-gateway.interface';
import { FinanceLocalGateway } from './finance-local.gateway';
import { FinanceApiGateway } from './finance-api.gateway';
/** Swap point for docs/backend/PHASE-7-PAYMENTS-RECONCILIATION-CONTRACT.md. */
export const FINANCE_GATEWAY = new InjectionToken<FinanceGateway>('FINANCE_GATEWAY', {
providedIn: 'root',
factory: () => (environment.useMockData ? inject(FinanceLocalGateway) : inject(FinanceApiGateway)),
factory: () => inject(FinanceApiGateway),
});

View File

@@ -9,12 +9,16 @@ export interface Customer {
createdAt: string;
}
export type ExternalIdentityProvider = 'vk_id' | 'telegram' | 'max';
export type ExternalIdentityProvider = 'vk_id' | 'yandex_id' | 'telegram' | 'max';
export interface ExternalIdentity {
customerId: string;
provider: ExternalIdentityProvider;
providerUserId: string;
/** Not every provider returns one - VK frequently does not. */
email?: string;
phone?: string;
displayName?: string;
verifiedAt: string;
lastUsedAt: string;
}

View File

@@ -0,0 +1,34 @@
import { HttpClient, HttpParams } from '@angular/common/http';
import { Injectable, inject } from '@angular/core';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { ExternalIdentity, ExternalIdentityProvider } from '../models/customer-identity.model';
import { SocialIdentityGateway, SocialProvider } from './social-identity-gateway.interface';
/**
* Contract: docs/backend/PHASE-8-IDENTITY-MESSAGING-CONTRACT.md §2.
*
* Provider-agnostic by construction - VK ID and Yandex ID differ only in the
* path segment, because the differences that matter (PKCE, VK's device_id,
* Yandex's Basic-auth token exchange) live entirely on the backend.
*/
@Injectable({ providedIn: 'root' })
export class SocialIdentityApiGateway implements SocialIdentityGateway {
private readonly http = inject(HttpClient);
private readonly base = '/api/identity/v1';
getAuthorizeUrl(provider: SocialProvider, returnTo?: string): Observable<string> {
const params = returnTo ? new HttpParams().set('returnTo', returnTo) : undefined;
return this.http
.get<{ url: string }>(`${this.base}/${provider}/authorize`, { params })
.pipe(map(response => response.url));
}
listIdentities(): Observable<ExternalIdentity[]> {
return this.http.get<ExternalIdentity[]>(`${this.base}/me/identities`);
}
unlink(provider: ExternalIdentityProvider): Observable<void> {
return this.http.post<void>(`${this.base}/${provider}/unlink`, {});
}
}

View File

@@ -0,0 +1,50 @@
import { Observable } from 'rxjs';
import { ExternalIdentity, ExternalIdentityProvider } from '../models/customer-identity.model';
/**
* Providers this surface can start an OAuth authorize redirect for.
*
* Narrower than ExternalIdentityProvider on purpose: Telegram and MAX link
* through a bot / QR flow owned by @marketplaces/auth, not an authorize
* redirect, so they can be listed and unlinked here (FH-4.6) but never
* passed to getAuthorizeUrl().
*/
export type SocialProvider = 'vk' | 'yandex';
/**
* Per docs/backend/PHASE-8-IDENTITY-MESSAGING-CONTRACT.md §2.
*
* One surface, one strategy per provider behind it. The client's entire
* involvement is "send me somewhere" and "tell me what is linked" - the
* OAuth exchange happens backend-side and the browser never holds a client
* secret, an access token, or a PKCE code verifier.
*
* Note what is absent: there is no completeCallback(). An earlier VK-only
* version of this interface took (code, codeVerifier) from the client, which
* forced the browser to generate and store the verifier. We are a
* confidential client; the backend owns state and verifier, handles the
* provider's callback itself, and redirects back with a session already set.
*/
export interface SocialIdentityGateway {
/**
* URL to navigate the browser to in order to start the flow. The backend
* has already minted and stored the single-use state and code verifier by
* the time this resolves.
*
* @param returnTo where to land after the callback completes, validated
* backend-side against the tenant's own origin - never used as an open
* redirect.
*/
getAuthorizeUrl(provider: SocialProvider, returnTo?: string): Observable<string>;
/** Providers currently linked to the authenticated customer. */
listIdentities(): Observable<ExternalIdentity[]>;
/**
* Unlinks a provider from the authenticated customer. Accepts any linked
* provider, not just the OAuth ones - a customer can detach Telegram or MAX
* the same way they detach VK, provided at least one identity remains
* (the backend enforces "you cannot unlink your last login").
*/
unlink(provider: ExternalIdentityProvider): Observable<void>;
}

View File

@@ -0,0 +1,76 @@
import { provideHttpClient } from '@angular/common/http';
import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing';
import { TestBed } from '@angular/core/testing';
import { SocialIdentityApiGateway } from './social-identity-api.gateway';
import { SocialProvider } from './social-identity-gateway.interface';
/**
* FH-4.1/FH-4.2. The whole point of this surface is what it does NOT carry:
* no client secret, no access token, no PKCE code verifier. These tests pin
* that shape, because a future "just add completeCallback back" would look
* harmless in review and would move verifier custody into the browser.
*/
describe('SocialIdentityApiGateway', () => {
let gateway: SocialIdentityApiGateway;
let httpTesting: HttpTestingController;
beforeEach(() => {
TestBed.configureTestingModule({
providers: [SocialIdentityApiGateway, provideHttpClient(), provideHttpClientTesting()],
});
gateway = TestBed.inject(SocialIdentityApiGateway);
httpTesting = TestBed.inject(HttpTestingController);
});
afterEach(() => httpTesting.verify());
const providers: SocialProvider[] = ['vk', 'yandex'];
for (const provider of providers) {
it(`asks the backend for the ${provider} authorize URL and sends no secret material`, () => {
let resolved: string | undefined;
gateway.getAuthorizeUrl(provider).subscribe(url => (resolved = url));
const request = httpTesting.expectOne(`/api/identity/v1/${provider}/authorize`);
expect(request.request.method).toBe('GET');
expect(request.request.body).toBeNull();
expect(request.request.urlWithParams).not.toContain('code_verifier');
expect(request.request.urlWithParams).not.toContain('client_secret');
request.flush({ url: `https://id.example.test/${provider}/authorize?state=abc` });
expect(resolved).toContain(provider);
});
}
it('passes returnTo through as a query parameter', () => {
gateway.getAuthorizeUrl('vk', '/cart').subscribe();
const request = httpTesting.expectOne(r => r.url === '/api/identity/v1/vk/authorize');
expect(request.request.params.get('returnTo')).toBe('/cart');
request.flush({ url: 'https://id.example.test/vk/authorize' });
});
it('reads linked identities from one endpoint for every provider', () => {
gateway.listIdentities().subscribe();
const request = httpTesting.expectOne('/api/identity/v1/me/identities');
expect(request.request.method).toBe('GET');
request.flush([]);
});
it('unlinks an OAuth provider by its identity name', () => {
gateway.unlink('yandex_id').subscribe();
const request = httpTesting.expectOne('/api/identity/v1/yandex_id/unlink');
expect(request.request.method).toBe('POST');
request.flush(null);
});
it('unlinks a non-OAuth provider too (FH-4.6: Telegram is one identity among several)', () => {
gateway.unlink('telegram').subscribe();
const request = httpTesting.expectOne('/api/identity/v1/telegram/unlink');
expect(request.request.method).toBe('POST');
request.flush(null);
});
});

View File

@@ -0,0 +1,9 @@
import { InjectionToken, inject } from '@angular/core';
import { SocialIdentityGateway } from './social-identity-gateway.interface';
import { SocialIdentityApiGateway } from './social-identity-api.gateway';
/** Swap point for docs/backend/PHASE-8-IDENTITY-MESSAGING-CONTRACT.md §2. */
export const SOCIAL_IDENTITY_GATEWAY = new InjectionToken<SocialIdentityGateway>('SOCIAL_IDENTITY_GATEWAY', {
providedIn: 'root',
factory: () => inject(SocialIdentityApiGateway),
});

View File

@@ -0,0 +1,46 @@
import { Injectable, signal } from '@angular/core';
import { Observable, of } from 'rxjs';
import { ExternalIdentity, ExternalIdentityProvider } from '../models/customer-identity.model';
import { SocialIdentityGateway, SocialProvider } from './social-identity-gateway.interface';
/**
* Development stand-in. No VK or Yandex OAuth application is registered yet,
* and registering one is blocked on a decision that has to be made before any
* of this can work for real: both providers validate redirect_uri against an
* exact registered list, so a multi-tenant platform needs one central
* identity host as the sole registered callback, with the tenant carried in
* the signed state. See FORK-HARVEST-TODO.md FH-0.1.
*
* Returning a data: URL rather than a fake provider URL is deliberate - it
* cannot be mistaken for a working flow if this ever runs outside dev.
*/
@Injectable({ providedIn: 'root' })
export class SocialIdentityLocalGateway implements SocialIdentityGateway {
// Seeded so the account-identities screen has something to render in dev.
// Telegram is present because in the current app it is the only real login;
// FH-4.6 makes it one ExternalIdentity among several, which is exactly what
// this list is meant to show.
private readonly linked = signal<ExternalIdentity[]>([
{
customerId: 'customer_local',
provider: 'telegram',
providerUserId: '100200300',
displayName: 'Local Telegram User',
verifiedAt: '2026-08-01T10:00:00.000Z',
lastUsedAt: '2026-08-21T09:00:00.000Z',
},
]);
getAuthorizeUrl(provider: SocialProvider): Observable<string> {
return of(`about:blank#${provider}-oauth-not-configured`);
}
listIdentities(): Observable<ExternalIdentity[]> {
return of(this.linked());
}
unlink(provider: ExternalIdentityProvider): Observable<void> {
this.linked.update(list => list.filter(identity => identity.provider !== provider));
return of(void 0);
}
}

View File

@@ -1,27 +0,0 @@
import { HttpClient } from '@angular/common/http';
import { Injectable, inject } from '@angular/core';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { Customer } from '../models/customer-identity.model';
import { VkIdGateway } from './vk-id-gateway.interface';
/**
* Contract: docs/backend/PHASE-8-IDENTITY-MESSAGING-CONTRACT.md §2.
* OAuth completion happens backend-side; this is the client-facing surface
* only - getAuthorizeUrl navigates the browser there, completeCallback hands
* back the code/verifier pair for the backend to exchange.
*/
@Injectable({ providedIn: 'root' })
export class VkIdApiGateway implements VkIdGateway {
private readonly http = inject(HttpClient);
getAuthorizeUrl(): Observable<string> {
return this.http
.get<{ url: string }>('/api/identity/v1/vk/authorize')
.pipe(map(response => response.url));
}
completeCallback(code: string, codeVerifier: string): Observable<Customer> {
return this.http.post<Customer>('/api/identity/v1/vk/callback', { code, codeVerifier });
}
}

View File

@@ -1,8 +0,0 @@
import { Observable } from 'rxjs';
import { Customer } from '../models/customer-identity.model';
/** Per docs/backend/PHASE-8-IDENTITY-MESSAGING-CONTRACT.md §2. OAuth completion is backend-side; this is the client-facing surface only. */
export interface VkIdGateway {
getAuthorizeUrl(): Observable<string>;
completeCallback(code: string, codeVerifier: string): Observable<Customer>;
}

View File

@@ -1,11 +0,0 @@
import { InjectionToken, inject } from '@angular/core';
import { environment } from '../../../../environments/environment';
import { VkIdGateway } from './vk-id-gateway.interface';
import { VkIdLocalGateway } from './vk-id-local.gateway';
import { VkIdApiGateway } from './vk-id-api.gateway';
/** Swap point for docs/backend/PHASE-8-IDENTITY-MESSAGING-CONTRACT.md §2. */
export const VK_ID_GATEWAY = new InjectionToken<VkIdGateway>('VK_ID_GATEWAY', {
providedIn: 'root',
factory: () => (environment.useMockData ? inject(VkIdLocalGateway) : inject(VkIdApiGateway)),
});

View File

@@ -1,28 +0,0 @@
import { Injectable } from '@angular/core';
import { Observable, of } from 'rxjs';
import { Customer } from '../models/customer-identity.model';
import { VkIdGateway } from './vk-id-gateway.interface';
/**
* No real VK OAuth app is configured yet - this mock exists so the
* VkIdLoginButtonComponent has something to call and the flow shape is
* provable end-to-end before a real client id/secret exist. Swap
* VK_ID_GATEWAY once docs/backend/PHASE-8-IDENTITY-MESSAGING-CONTRACT.md §2
* ships; the real backend completes OAuth server-side, this interface never
* exposes a client secret regardless of implementation.
*/
@Injectable({ providedIn: 'root' })
export class VkIdLocalGateway implements VkIdGateway {
getAuthorizeUrl(): Observable<string> {
return of('about:blank#vk-id-not-configured');
}
completeCallback(_code: string, _codeVerifier: string): Observable<Customer> {
return of({
id: 'customer_vk_mock',
marketplaceId: 'default',
status: 'active',
createdAt: new Date().toISOString(),
});
}
}

View File

@@ -1,11 +1,9 @@
import { InjectionToken, inject } from '@angular/core';
import { environment } from '../../../../environments/environment';
import { ConnectorGateway } from './connector-gateway.interface';
import { ConnectorLocalGateway } from './connector-local.gateway';
import { ConnectorApiGateway } from './connector-api.gateway';
/** Swap point for docs/backend/PHASE-4-CONNECTOR-FRAMEWORK-CONTRACT.md §7. */
export const CONNECTOR_GATEWAY = new InjectionToken<ConnectorGateway>('CONNECTOR_GATEWAY', {
providedIn: 'root',
factory: () => (environment.useMockData ? inject(ConnectorLocalGateway) : inject(ConnectorApiGateway)),
factory: () => inject(ConnectorApiGateway),
});

View File

@@ -1,11 +1,9 @@
import { InjectionToken, inject } from '@angular/core';
import { environment } from '../../../../environments/environment';
import { MarketplaceGateway } from './marketplace-gateway.interface';
import { MarketplaceLocalGateway } from './marketplace-local.gateway';
import { MarketplaceApiGateway } from './marketplace-api.gateway';
/** Swap point for docs/backend/PHASE-9-TENANT-REGISTRY-DOMAINS-CONTRACT.md. */
export const MARKETPLACE_GATEWAY = new InjectionToken<MarketplaceGateway>('MARKETPLACE_GATEWAY', {
providedIn: 'root',
factory: () => (environment.useMockData ? inject(MarketplaceLocalGateway) : inject(MarketplaceApiGateway)),
factory: () => inject(MarketplaceApiGateway),
});

View File

@@ -1,11 +1,9 @@
import { InjectionToken, inject } from '@angular/core';
import { environment } from '../../../../environments/environment';
import { MarketplaceRevisionGateway } from './marketplace-revision-gateway.interface';
import { MarketplaceRevisionLocalGateway } from './marketplace-revision-local.gateway';
import { MarketplaceRevisionApiGateway } from './marketplace-revision-api.gateway';
/** Swap point for docs/backend/PHASE-9-TENANT-REGISTRY-DOMAINS-CONTRACT.md §5. */
export const MARKETPLACE_REVISION_GATEWAY = new InjectionToken<MarketplaceRevisionGateway>('MARKETPLACE_REVISION_GATEWAY', {
providedIn: 'root',
factory: () => (environment.useMockData ? inject(MarketplaceRevisionLocalGateway) : inject(MarketplaceRevisionApiGateway)),
factory: () => inject(MarketplaceRevisionApiGateway),
});

View File

@@ -1,11 +1,9 @@
import { InjectionToken, inject } from '@angular/core';
import { environment } from '../../../../environments/environment';
import { OfferGateway } from './offer-gateway.interface';
import { OfferLocalGateway } from './offer-local.gateway';
import { OfferApiGateway } from './offer-api.gateway';
/** Swap point for docs/backend/PHASE-3-CATALOG-OFFER-FULFILLMENT-CONTRACT.md §7. */
export const OFFER_GATEWAY = new InjectionToken<OfferGateway>('OFFER_GATEWAY', {
providedIn: 'root',
factory: () => (environment.useMockData ? inject(OfferLocalGateway) : inject(OfferApiGateway)),
factory: () => inject(OfferApiGateway),
});

View File

@@ -1,11 +1,9 @@
import { InjectionToken, inject } from '@angular/core';
import { environment } from '../../../../environments/environment';
import { PartnerHierarchyGateway } from './partner-hierarchy-gateway.interface';
import { PartnerHierarchyLocalGateway } from './partner-hierarchy-local.gateway';
import { PartnerHierarchyApiGateway } from './partner-hierarchy-api.gateway';
/** Swap point for docs/backend/PARTNER-PROVISIONING-API-CONTRACT.md §4, §6. */
export const PARTNER_HIERARCHY_GATEWAY = new InjectionToken<PartnerHierarchyGateway>('PARTNER_HIERARCHY_GATEWAY', {
providedIn: 'root',
factory: () => (environment.useMockData ? inject(PartnerHierarchyLocalGateway) : inject(PartnerHierarchyApiGateway)),
factory: () => inject(PartnerHierarchyApiGateway),
});

View File

@@ -1,11 +1,9 @@
import { InjectionToken, inject } from '@angular/core';
import { environment } from '../../../../environments/environment';
import { PermissionGateway } from './permission-gateway.interface';
import { PermissionLocalGateway } from './permission-local.gateway';
import { PermissionApiGateway } from './permission-api.gateway';
/** Swap point for docs/backend/TRACK-S-SECURITY-RBAC-CONTRACT.md §2-3. */
export const PERMISSION_GATEWAY = new InjectionToken<PermissionGateway>('PERMISSION_GATEWAY', {
providedIn: 'root',
factory: () => (environment.useMockData ? inject(PermissionLocalGateway) : inject(PermissionApiGateway)),
factory: () => inject(PermissionApiGateway),
});

View File

@@ -1,11 +1,9 @@
import { InjectionToken, inject } from '@angular/core';
import { environment } from '../../../../environments/environment';
import { FxQuoteGateway } from './fx-quote-gateway.interface';
import { FxQuoteLocalGateway } from './fx-quote-local.gateway';
import { FxQuoteApiGateway } from './fx-quote-api.gateway';
/** Swap point for docs/backend/PHASE-1-MONEY-FX-PAYMENTS-CONTRACT.md §3.1. */
export const FX_QUOTE_GATEWAY = new InjectionToken<FxQuoteGateway>('FX_QUOTE_GATEWAY', {
providedIn: 'root',
factory: () => (environment.useMockData ? inject(FxQuoteLocalGateway) : inject(FxQuoteApiGateway)),
factory: () => inject(FxQuoteApiGateway),
});

View File

@@ -1,11 +1,9 @@
import { InjectionToken, inject } from '@angular/core';
import { environment } from '../../../../environments/environment';
import { SellerGateway } from './seller-gateway.interface';
import { SellerLocalGateway } from './seller-local.gateway';
import { SellerApiGateway } from './seller-api.gateway';
/** Swap point for docs/backend/PHASE-5-SELLER-PORTAL-CONTRACT.md §3. */
export const SELLER_GATEWAY = new InjectionToken<SellerGateway>('SELLER_GATEWAY', {
providedIn: 'root',
factory: () => (environment.useMockData ? inject(SellerLocalGateway) : inject(SellerApiGateway)),
factory: () => inject(SellerApiGateway),
});

View File

@@ -1,11 +1,9 @@
import { InjectionToken, inject } from '@angular/core';
import { environment } from '../../../../../environments/environment';
import { AdminDashboardMetricsGateway } from './admin-dashboard-metrics.gateway.interface';
import { AdminDashboardMetricsLocalGateway } from './admin-dashboard-metrics.local.gateway';
import { AdminDashboardMetricsApiGateway } from './admin-dashboard-metrics-api.gateway';
/** Swap point. */
export const ADMIN_DASHBOARD_METRICS_GATEWAY = new InjectionToken<AdminDashboardMetricsGateway>('ADMIN_DASHBOARD_METRICS_GATEWAY', {
providedIn: 'root',
factory: () => (environment.useMockData ? inject(AdminDashboardMetricsLocalGateway) : inject(AdminDashboardMetricsApiGateway)),
factory: () => inject(AdminDashboardMetricsApiGateway),
});

View File

@@ -1,11 +1,9 @@
import { InjectionToken, inject } from '@angular/core';
import { environment } from '../../../../../environments/environment';
import { AdminModerationGateway } from './admin-moderation-gateway.interface';
import { AdminModerationLocalGateway } from './admin-moderation-local.gateway';
import { AdminModerationApiGateway } from './admin-moderation-api.gateway';
/** Swap point. */
export const ADMIN_MODERATION_GATEWAY = new InjectionToken<AdminModerationGateway>('ADMIN_MODERATION_GATEWAY', {
providedIn: 'root',
factory: () => (environment.useMockData ? inject(AdminModerationLocalGateway) : inject(AdminModerationApiGateway)),
factory: () => inject(AdminModerationApiGateway),
});

View File

@@ -1,11 +1,9 @@
import { InjectionToken, inject } from '@angular/core';
import { environment } from '../../../../../environments/environment';
import { AdminMonitoringGateway } from './admin-monitoring-gateway.interface';
import { AdminMonitoringLocalGateway } from './admin-monitoring-local.gateway';
import { AdminMonitoringApiGateway } from './admin-monitoring-api.gateway';
/** Swap point. */
export const ADMIN_MONITORING_GATEWAY = new InjectionToken<AdminMonitoringGateway>('ADMIN_MONITORING_GATEWAY', {
providedIn: 'root',
factory: () => (environment.useMockData ? inject(AdminMonitoringLocalGateway) : inject(AdminMonitoringApiGateway)),
factory: () => inject(AdminMonitoringApiGateway),
});

View File

@@ -1,11 +1,9 @@
import { InjectionToken, inject } from '@angular/core';
import { environment } from '../../../../../environments/environment';
import { AdminNotificationsGateway } from './admin-notifications-gateway.interface';
import { AdminNotificationsLocalGateway } from './admin-notifications-local.gateway';
import { AdminNotificationsApiGateway } from './admin-notifications-api.gateway';
/** Swap point for docs/backend/PHASE-2-ORDERS-NOTIFICATIONS-CONTRACT.md §7. */
export const ADMIN_NOTIFICATIONS_GATEWAY = new InjectionToken<AdminNotificationsGateway>('ADMIN_NOTIFICATIONS_GATEWAY', {
providedIn: 'root',
factory: () => (environment.useMockData ? inject(AdminNotificationsLocalGateway) : inject(AdminNotificationsApiGateway)),
factory: () => inject(AdminNotificationsApiGateway),
});

View File

@@ -1,11 +1,9 @@
import { InjectionToken, inject } from '@angular/core';
import { environment } from '../../../../../environments/environment';
import { AdminOrdersGateway } from './admin-orders-gateway.interface';
import { AdminOrdersLocalGateway } from './admin-orders-local.gateway';
import { AdminOrdersApiGateway } from './admin-orders-api.gateway';
/** Swap point for docs/backend/PHASE-2-ORDERS-NOTIFICATIONS-CONTRACT.md. */
export const ADMIN_ORDERS_GATEWAY = new InjectionToken<AdminOrdersGateway>('ADMIN_ORDERS_GATEWAY', {
providedIn: 'root',
factory: () => (environment.useMockData ? inject(AdminOrdersLocalGateway) : inject(AdminOrdersApiGateway)),
factory: () => inject(AdminOrdersApiGateway),
});

View File

@@ -1,11 +1,9 @@
import { InjectionToken, inject } from '@angular/core';
import { environment } from '../../../../../environments/environment';
import { AdminProductsGateway } from './admin-products-gateway.interface';
import { AdminProductsLocalGateway } from './admin-products-local.gateway';
import { AdminProductsApiGateway } from './admin-products-api.gateway';
/** Swap point for docs/backend/PHASE-3-CATALOG-OFFER-FULFILLMENT-CONTRACT.md §7. */
export const ADMIN_PRODUCTS_GATEWAY = new InjectionToken<AdminProductsGateway>('ADMIN_PRODUCTS_GATEWAY', {
providedIn: 'root',
factory: () => (environment.useMockData ? inject(AdminProductsLocalGateway) : inject(AdminProductsApiGateway)),
factory: () => inject(AdminProductsApiGateway),
});

View File

@@ -1,11 +1,9 @@
import { InjectionToken, inject } from '@angular/core';
import { environment } from '../../../../../environments/environment';
import { AdminTransactionsGateway } from './admin-transactions-gateway.interface';
import { AdminTransactionsLocalGateway } from './admin-transactions-local.gateway';
import { AdminTransactionsApiGateway } from './admin-transactions-api.gateway';
/** Swap point for docs/backend/PHASE-1-MONEY-FX-PAYMENTS-CONTRACT.md §6. */
export const ADMIN_TRANSACTIONS_GATEWAY = new InjectionToken<AdminTransactionsGateway>('ADMIN_TRANSACTIONS_GATEWAY', {
providedIn: 'root',
factory: () => (environment.useMockData ? inject(AdminTransactionsLocalGateway) : inject(AdminTransactionsApiGateway)),
factory: () => inject(AdminTransactionsApiGateway),
});

View File

@@ -1,11 +1,9 @@
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 docs/backend/TRACK-S-SECURITY-RBAC-CONTRACT.md §8. */
export const ADMIN_USERS_GATEWAY = new InjectionToken<AdminUsersGateway>('ADMIN_USERS_GATEWAY', {
providedIn: 'root',
factory: () => (environment.useMockData ? inject(AdminUsersLocalGateway) : inject(AdminUsersApiGateway)),
factory: () => inject(AdminUsersApiGateway),
});

View File

@@ -508,6 +508,15 @@ export class ProjectEditorFacade {
if (!current || this.hasBlockingIssues()) {
return false;
}
// FH-E.5 / PHASE-9 §5. This is a LOCAL preview publish: it applies the
// config to the in-memory runtime and saves the draft to localStorage.
// localStorage is a recovery cache here (see draftRestored), never the
// published source of truth. Real publish must round-trip through the
// revision API - POST .../revisions/{id}/publish - which creates an
// immutable server-side revision and flips the published pointer in one
// transaction. Until that endpoint exists this stays local-only and must
// not be treated as authoritative; when it ships, this method awaits the
// server response and only then marks status 'published'.
this.runtime.reloadFromBootstrap(current);
const savedAt = this.draftStorage.save(current);
this.state.update(state => ({

View File

@@ -0,0 +1,64 @@
<section class="identities">
<header class="identities__header">
<h1>Connected accounts</h1>
<p>Sign in with any of these. You can add or remove them at any time.</p>
</header>
@switch (state()) {
@case ('loading') {
<p class="identities__status" role="status">Loading…</p>
}
@case ('error') {
<div class="identities__status identities__status--error" role="alert">
<app-icon name="warning" [size]="18" />
<span>Could not load your connected accounts.</span>
<button type="button" class="identities__retry" (click)="reload()">Retry</button>
</div>
}
@case ('ready') {
@if (conflict(); as message) {
<div class="identities__conflict" role="alert">
<app-icon name="warning" [size]="18" />
<span>{{ message }}</span>
</div>
}
<ul class="identities__list">
@for (identity of identities(); track identity.provider) {
<li class="identity">
<span class="identity__icon"><app-icon name="user" [size]="20" /></span>
<span class="identity__body">
<span class="identity__name">{{ label(identity.provider) }}</span>
@if (identity.displayName) {
<span class="identity__detail">{{ identity.displayName }}</span>
}
</span>
<button
type="button"
class="identity__unlink"
[disabled]="isLastIdentity() || unlinking() === identity.provider"
[attr.title]="isLastIdentity() ? 'This is your only way to sign in - add another before removing it.' : null"
(click)="unlink(identity.provider)"
>
@if (unlinking() === identity.provider) {
<app-icon name="refresh" [size]="16" />
} @else {
<app-icon name="trash" [size]="16" />
}
<span>Remove</span>
</button>
</li>
}
</ul>
@if (linkable().length) {
<div class="identities__add">
<h2>Add another</h2>
@for (provider of linkable(); track provider) {
<app-social-login-button [provider]="provider" returnTo="/account/identities" />
}
</div>
}
}
}
</section>

View File

@@ -0,0 +1,126 @@
.identities {
max-width: 560px;
margin: 0 auto;
padding: 24px 16px;
display: flex;
flex-direction: column;
gap: 24px;
}
.identities__header {
h1 {
margin: 0 0 4px;
font-size: 1.5rem;
color: var(--text-primary);
}
p {
margin: 0;
color: var(--text-secondary, var(--text-muted));
}
}
.identities__status {
display: flex;
align-items: center;
gap: 8px;
color: var(--text-secondary, var(--text-muted));
&--error {
color: var(--color-error, #c0392b);
}
}
.identities__retry {
margin-left: auto;
padding: 4px 12px;
border: 1px solid var(--border-color);
border-radius: var(--radius-sm, 6px);
background: var(--bg-primary);
color: var(--text-primary);
cursor: pointer;
}
.identities__conflict {
display: flex;
align-items: center;
gap: 8px;
padding: 12px 14px;
border: 1px solid var(--color-warning, #d19a00);
border-radius: var(--radius-md, 8px);
background: var(--color-warning-bg, rgba(209, 154, 0, 0.08));
color: var(--text-primary);
}
.identities__list {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
gap: 8px;
}
.identity {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 14px;
border: 1px solid var(--border-color);
border-radius: var(--radius-md, 8px);
background: var(--bg-primary);
}
.identity__icon {
display: inline-flex;
color: var(--text-secondary, var(--text-muted));
}
.identity__body {
display: flex;
flex-direction: column;
min-width: 0;
}
.identity__name {
font-weight: var(--font-weight-bold, 700);
color: var(--text-primary);
}
.identity__detail {
font-size: 0.875rem;
color: var(--text-secondary, var(--text-muted));
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.identity__unlink {
margin-left: auto;
display: inline-flex;
align-items: center;
gap: 6px;
padding: 6px 12px;
border: 1px solid var(--border-color);
border-radius: var(--radius-sm, 6px);
background: transparent;
color: var(--text-primary);
cursor: pointer;
&:disabled {
opacity: 0.5;
cursor: not-allowed;
}
}
.identities__add {
display: flex;
flex-direction: column;
gap: 12px;
h2 {
margin: 0;
font-size: 1rem;
color: var(--text-primary);
}
}

View File

@@ -0,0 +1,93 @@
import { TestBed } from '@angular/core/testing';
import { Observable, of, throwError } from 'rxjs';
import { AccountIdentitiesComponent } from './account-identities.component';
import { SOCIAL_IDENTITY_GATEWAY } from '../../../../core/identity/services/social-identity-gateway.token';
import { SocialIdentityGateway } from '../../../../core/identity/services/social-identity-gateway.interface';
import {
ExternalIdentity,
ExternalIdentityProvider,
} from '../../../../core/identity/models/customer-identity.model';
function identity(provider: ExternalIdentityProvider): ExternalIdentity {
return {
customerId: 'c1',
provider,
providerUserId: 'u_' + provider,
verifiedAt: '2026-08-01T00:00:00.000Z',
lastUsedAt: '2026-08-21T00:00:00.000Z',
};
}
class FakeGateway implements SocialIdentityGateway {
linked: ExternalIdentity[] = [];
unlinkCalls: ExternalIdentityProvider[] = [];
failList = false;
getAuthorizeUrl(): Observable<string> {
return of('about:blank');
}
listIdentities(): Observable<ExternalIdentity[]> {
return this.failList ? throwError(() => new Error('boom')) : of(this.linked);
}
unlink(provider: ExternalIdentityProvider): Observable<void> {
this.unlinkCalls.push(provider);
return of(void 0);
}
}
function make(gateway: FakeGateway): AccountIdentitiesComponent {
TestBed.configureTestingModule({
providers: [{ provide: SOCIAL_IDENTITY_GATEWAY, useValue: gateway }],
});
return TestBed.createComponent(AccountIdentitiesComponent).componentInstance;
}
describe('AccountIdentitiesComponent', () => {
it('lists the linked identities and reports ready', () => {
const g = new FakeGateway();
g.linked = [identity('telegram'), identity('vk_id')];
const c = make(g);
expect(c.state()).toBe('ready');
expect(c.identities().map(i => i.provider)).toEqual(['telegram', 'vk_id']);
});
it('offers only the OAuth providers that are not already linked', () => {
const g = new FakeGateway();
g.linked = [identity('vk_id')]; // vk linked, yandex not
const c = make(g);
expect(c.linkable()).toEqual(['yandex']);
});
it('refuses to unlink the last remaining identity', () => {
const g = new FakeGateway();
g.linked = [identity('telegram')];
const c = make(g);
expect(c.isLastIdentity()).toBe(true);
c.unlink('telegram');
expect(g.unlinkCalls).toEqual([]);
expect(c.identities().length).toBe(1);
});
it('unlinks a provider when more than one is linked', () => {
const g = new FakeGateway();
g.linked = [identity('telegram'), identity('vk_id')];
const c = make(g);
c.unlink('vk_id');
expect(g.unlinkCalls).toEqual(['vk_id']);
expect(c.identities().map(i => i.provider)).toEqual(['telegram']);
});
it('surfaces a load failure instead of showing an empty account', () => {
const g = new FakeGateway();
g.failList = true;
const c = make(g);
expect(c.state()).toBe('error');
});
});

View File

@@ -0,0 +1,131 @@
import { ChangeDetectionStrategy, Component, computed, inject, signal } from '@angular/core';
import { catchError, of } from 'rxjs';
import { take } from 'rxjs/operators';
import { SOCIAL_IDENTITY_GATEWAY } from '../../../../core/identity/services/social-identity-gateway.token';
import { SocialProvider } from '../../../../core/identity/services/social-identity-gateway.interface';
import {
ExternalIdentity,
ExternalIdentityProvider,
} from '../../../../core/identity/models/customer-identity.model';
import { SocialLoginButtonComponent } from '../../../../components/social-login-button/social-login-button.component';
import { IconComponent } from '../../../../shared/ui/icon/icon.component';
/** Providers a customer can attach through an OAuth authorize redirect. */
const LINKABLE_SOCIAL: readonly SocialProvider[] = ['vk', 'yandex'];
const PROVIDER_LABEL: Record<ExternalIdentityProvider, string> = {
vk_id: 'VK ID',
yandex_id: 'Yandex ID',
telegram: 'Telegram',
max: 'MAX',
};
/** Which ExternalIdentity.provider a given OAuth SocialProvider produces. */
const SOCIAL_TO_IDENTITY: Record<SocialProvider, ExternalIdentityProvider> = {
vk: 'vk_id',
yandex: 'yandex_id',
};
type LoadState = 'loading' | 'ready' | 'error';
/**
* FH-4.7. Account screen for managing linked external identities: shows what
* is linked, lets the customer attach the OAuth providers they have not linked
* yet, and detach any of them.
*
* Contract: docs/backend/PHASE-8-IDENTITY-MESSAGING-CONTRACT.md §2.1
* (GET /me/identities, POST /{provider}/unlink, GET /{provider}/authorize).
*
* Two rules this UI has to honour, both enforced server-side but surfaced
* here so the customer is never surprised:
* - the last remaining identity cannot be unlinked (it is the only way back
* in), so the detach control is disabled when exactly one is linked;
* - attempting to link a provider account already bound to a different
* customer is an identity conflict, not a silent rebind - the backend
* returns it as such and this screen shows the conflict rather than
* pretending the link succeeded.
*
* Not yet wired into a route: there is no customer account area in the
* storefront yet, and no real OAuth application to authorize against
* (FH-0.1). This is the surface those depend on, buildable and testable now.
*/
@Component({
selector: 'app-account-identities',
standalone: true,
imports: [SocialLoginButtonComponent, IconComponent],
templateUrl: './account-identities.component.html',
styleUrls: ['./account-identities.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AccountIdentitiesComponent {
private readonly gateway = inject(SOCIAL_IDENTITY_GATEWAY);
readonly state = signal<LoadState>('loading');
readonly identities = signal<ExternalIdentity[]>([]);
/** Set when a link attempt hit an identity conflict (§2.3). */
readonly conflict = signal<string | null>(null);
/** Provider currently being detached, so only its button shows a spinner. */
readonly unlinking = signal<ExternalIdentityProvider | null>(null);
/** True while exactly one identity remains - detaching it is refused. */
readonly isLastIdentity = computed(() => this.identities().length <= 1);
/** OAuth providers not yet linked, offered as attach buttons. */
readonly linkable = computed<SocialProvider[]>(() => {
const linked = new Set(this.identities().map(identity => identity.provider));
return LINKABLE_SOCIAL.filter(social => !linked.has(SOCIAL_TO_IDENTITY[social]));
});
constructor() {
this.reload();
}
label(provider: ExternalIdentityProvider): string {
return PROVIDER_LABEL[provider];
}
reload(): void {
this.state.set('loading');
this.gateway
.listIdentities()
.pipe(
take(1),
catchError(() => {
this.state.set('error');
return of<ExternalIdentity[] | null>(null);
}),
)
.subscribe(identities => {
if (identities === null) {
return;
}
this.identities.set(identities);
this.state.set('ready');
});
}
unlink(provider: ExternalIdentityProvider): void {
if (this.isLastIdentity() || this.unlinking()) {
return;
}
this.conflict.set(null);
this.unlinking.set(provider);
this.gateway
.unlink(provider)
.pipe(
take(1),
catchError(() => {
this.unlinking.set(null);
this.state.set('error');
return of<'failed'>('failed');
}),
)
.subscribe(result => {
if (result === 'failed') {
return;
}
this.identities.update(list => list.filter(identity => identity.provider !== provider));
this.unlinking.set(null);
});
}
}

View File

@@ -0,0 +1,9 @@
import { Provider } from '@angular/core';
/**
* Production replacement for mock-gateway.providers.ts (angular.json
* fileReplacements). Imports nothing on purpose: this file existing is what
* guarantees no *LocalGateway or its fixtures can be reached from a
* production build.
*/
export const MOCK_GATEWAY_PROVIDERS: Provider[] = [];

View File

@@ -0,0 +1,91 @@
import { Provider } from '@angular/core';
import { environment } from '../environments/environment';
import { ADMIN_DASHBOARD_METRICS_GATEWAY } from './features/admin/dashboard/services/admin-dashboard-metrics-gateway.token';
import { AdminDashboardMetricsLocalGateway } from './features/admin/dashboard/services/admin-dashboard-metrics.local.gateway';
import { ADMIN_MODERATION_GATEWAY } from './features/admin/moderation/services/admin-moderation-gateway.token';
import { AdminModerationLocalGateway } from './features/admin/moderation/services/admin-moderation-local.gateway';
import { ADMIN_MONITORING_GATEWAY } from './features/admin/monitoring/services/admin-monitoring-gateway.token';
import { AdminMonitoringLocalGateway } from './features/admin/monitoring/services/admin-monitoring-local.gateway';
import { ADMIN_NOTIFICATIONS_GATEWAY } from './features/admin/notifications/services/admin-notifications-gateway.token';
import { AdminNotificationsLocalGateway } from './features/admin/notifications/services/admin-notifications-local.gateway';
import { ADMIN_ORDERS_GATEWAY } from './features/admin/orders/services/admin-orders-gateway.token';
import { AdminOrdersLocalGateway } from './features/admin/orders/services/admin-orders-local.gateway';
import { ADMIN_PRODUCTS_GATEWAY } from './features/admin/products/services/admin-products-gateway.token';
import { AdminProductsLocalGateway } from './features/admin/products/services/admin-products-local.gateway';
import { ADMIN_TRANSACTIONS_GATEWAY } from './features/admin/transactions/services/admin-transactions-gateway.token';
import { AdminTransactionsLocalGateway } from './features/admin/transactions/services/admin-transactions-local.gateway';
import { ADMIN_USERS_GATEWAY } from './features/admin/users/services/admin-users-gateway.token';
import { AdminUsersLocalGateway } from './features/admin/users/services/admin-users-local.gateway';
import { ANALYTICS_GATEWAY } from './core/analytics/services/analytics-gateway.token';
import { AnalyticsLocalGateway } from './core/analytics/services/analytics-local.gateway';
import { CONNECTOR_GATEWAY } from './core/integrations/services/connector-gateway.token';
import { ConnectorLocalGateway } from './core/integrations/services/connector-local.gateway';
import { FINANCE_GATEWAY } from './core/finance/services/finance-gateway.token';
import { FinanceLocalGateway } from './core/finance/services/finance-local.gateway';
import { FX_QUOTE_GATEWAY } from './core/pricing/services/fx-quote-gateway.token';
import { FxQuoteLocalGateway } from './core/pricing/services/fx-quote-local.gateway';
import { MALL_CONTENT_GATEWAY } from './core/content-modules/services/mall-content-gateway.token';
import { MallContentLocalGateway } from './core/content-modules/services/mall-content-local.gateway';
import { MARKETPLACE_GATEWAY } from './core/marketplace-registry/services/marketplace-gateway.token';
import { MarketplaceLocalGateway } from './core/marketplace-registry/services/marketplace-local.gateway';
import { MARKETPLACE_REVISION_GATEWAY } from './core/marketplace-registry/services/marketplace-revision-gateway.token';
import { MarketplaceRevisionLocalGateway } from './core/marketplace-registry/services/marketplace-revision-local.gateway';
import { OFFER_GATEWAY } from './core/offers/services/offer-gateway.token';
import { OfferLocalGateway } from './core/offers/services/offer-local.gateway';
import { PARTNER_HIERARCHY_GATEWAY } from './core/partner-hierarchy/services/partner-hierarchy-gateway.token';
import { PartnerHierarchyLocalGateway } from './core/partner-hierarchy/services/partner-hierarchy-local.gateway';
import { PERMISSION_GATEWAY } from './core/permissions/services/permission-gateway.token';
import { PermissionLocalGateway } from './core/permissions/services/permission-local.gateway';
import { SELLER_GATEWAY } from './core/sellers/services/seller-gateway.token';
import { SellerLocalGateway } from './core/sellers/services/seller-local.gateway';
import { SERVER_CART_GATEWAY } from './core/cart/services/server-cart-gateway.token';
import { ServerCartLocalGateway } from './core/cart/services/server-cart-local.gateway';
import { SOCIAL_IDENTITY_GATEWAY } from './core/identity/services/social-identity-gateway.token';
import { SocialIdentityLocalGateway } from './core/identity/services/social-identity-local.gateway';
/**
* Development-only gateway overrides.
*
* This file is swapped for mock-gateway.providers.production.ts at build time
* (see angular.json fileReplacements), the same mechanism
* mock-data.interceptor.production.ts already uses. The production copy
* imports nothing, so no *LocalGateway class - and none of its seeded
* fixtures - can reach a production bundle.
*
* It used to live inside each token factory as
* factory: () => (environment.useMockData ? inject(XLocal) : inject(XApi))
* which reads as a toggle but is not one: naming both classes in the factory
* keeps both reachable, so every mock shipped regardless of the flag. A
* fixture string from partner-hierarchy-local.gateway.ts was verifiably
* present in a production build on 2026-08-21.
*
* useExisting rather than useClass: the local gateways are already
* providedIn: 'root' singletons, and an app-level provider for the token
* wins over its tree-shakable default.
*/
export const MOCK_GATEWAY_PROVIDERS: Provider[] = environment.useMockData
? [
{ provide: ADMIN_DASHBOARD_METRICS_GATEWAY, useExisting: AdminDashboardMetricsLocalGateway },
{ provide: ADMIN_MODERATION_GATEWAY, useExisting: AdminModerationLocalGateway },
{ provide: ADMIN_MONITORING_GATEWAY, useExisting: AdminMonitoringLocalGateway },
{ provide: ADMIN_NOTIFICATIONS_GATEWAY, useExisting: AdminNotificationsLocalGateway },
{ provide: ADMIN_ORDERS_GATEWAY, useExisting: AdminOrdersLocalGateway },
{ provide: ADMIN_PRODUCTS_GATEWAY, useExisting: AdminProductsLocalGateway },
{ provide: ADMIN_TRANSACTIONS_GATEWAY, useExisting: AdminTransactionsLocalGateway },
{ provide: ADMIN_USERS_GATEWAY, useExisting: AdminUsersLocalGateway },
{ provide: ANALYTICS_GATEWAY, useExisting: AnalyticsLocalGateway },
{ provide: CONNECTOR_GATEWAY, useExisting: ConnectorLocalGateway },
{ provide: FINANCE_GATEWAY, useExisting: FinanceLocalGateway },
{ provide: FX_QUOTE_GATEWAY, useExisting: FxQuoteLocalGateway },
{ provide: MALL_CONTENT_GATEWAY, useExisting: MallContentLocalGateway },
{ provide: MARKETPLACE_GATEWAY, useExisting: MarketplaceLocalGateway },
{ provide: MARKETPLACE_REVISION_GATEWAY, useExisting: MarketplaceRevisionLocalGateway },
{ provide: OFFER_GATEWAY, useExisting: OfferLocalGateway },
{ provide: PARTNER_HIERARCHY_GATEWAY, useExisting: PartnerHierarchyLocalGateway },
{ provide: PERMISSION_GATEWAY, useExisting: PermissionLocalGateway },
{ provide: SELLER_GATEWAY, useExisting: SellerLocalGateway },
{ provide: SERVER_CART_GATEWAY, useExisting: ServerCartLocalGateway },
{ provide: SOCIAL_IDENTITY_GATEWAY, useExisting: SocialIdentityLocalGateway },
]
: [];