Compare commits
2 Commits
7fe5ac7cd4
...
0df8d3d592
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0df8d3d592 | ||
|
|
ffd57f2d18 |
18
src/app/core/analytics/services/analytics-api.gateway.ts
Normal file
18
src/app/core/analytics/services/analytics-api.gateway.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import { HttpClient } from '@angular/common/http';
|
||||||
|
import { Injectable, inject } from '@angular/core';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { map } from 'rxjs/operators';
|
||||||
|
import { AnalyticsEvent } from '../models/analytics-event.model';
|
||||||
|
import { AnalyticsGateway } from './analytics-gateway.interface';
|
||||||
|
|
||||||
|
/** Contract: docs/backend/TRACK-A-ANALYTICS-CONTRACT.md §1 - server-side batched ingest. */
|
||||||
|
@Injectable({ providedIn: 'root' })
|
||||||
|
export class AnalyticsApiGateway implements AnalyticsGateway {
|
||||||
|
private readonly http = inject(HttpClient);
|
||||||
|
|
||||||
|
track(event: AnalyticsEvent): Observable<void> {
|
||||||
|
return this.http
|
||||||
|
.post('/api/v2/storefront/analytics/events', event)
|
||||||
|
.pipe(map(() => undefined));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,11 @@
|
|||||||
import { InjectionToken, inject } from '@angular/core';
|
import { InjectionToken, inject } from '@angular/core';
|
||||||
|
import { environment } from '../../../../environments/environment';
|
||||||
import { AnalyticsGateway } from './analytics-gateway.interface';
|
import { AnalyticsGateway } from './analytics-gateway.interface';
|
||||||
import { AnalyticsLocalGateway } from './analytics-local.gateway';
|
import { AnalyticsLocalGateway } from './analytics-local.gateway';
|
||||||
|
import { AnalyticsApiGateway } from './analytics-api.gateway';
|
||||||
|
|
||||||
/** Swap point for docs/backend/TRACK-A-ANALYTICS-CONTRACT.md §1. */
|
/** Swap point for docs/backend/TRACK-A-ANALYTICS-CONTRACT.md §1. */
|
||||||
export const ANALYTICS_GATEWAY = new InjectionToken<AnalyticsGateway>('ANALYTICS_GATEWAY', {
|
export const ANALYTICS_GATEWAY = new InjectionToken<AnalyticsGateway>('ANALYTICS_GATEWAY', {
|
||||||
providedIn: 'root',
|
providedIn: 'root',
|
||||||
factory: () => inject(AnalyticsLocalGateway),
|
factory: () => (environment.useMockData ? inject(AnalyticsLocalGateway) : inject(AnalyticsApiGateway)),
|
||||||
});
|
});
|
||||||
|
|||||||
36
src/app/core/cart/services/server-cart-api.gateway.ts
Normal file
36
src/app/core/cart/services/server-cart-api.gateway.ts
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import { HttpClient } from '@angular/common/http';
|
||||||
|
import { Injectable, inject } from '@angular/core';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { map } from 'rxjs/operators';
|
||||||
|
import { CheckoutSession, ServerCart, ServerCartLine } from '../models/server-cart.model';
|
||||||
|
import { ServerCartGateway } from './server-cart-gateway.interface';
|
||||||
|
|
||||||
|
/** Contract: docs/backend/PHASE-6-CART-CHECKOUT-CONTRACT.md §3, §5. */
|
||||||
|
@Injectable({ providedIn: 'root' })
|
||||||
|
export class ServerCartApiGateway implements ServerCartGateway {
|
||||||
|
private readonly http = inject(HttpClient);
|
||||||
|
|
||||||
|
getCart(): Observable<{ cart: ServerCart; lines: ServerCartLine[] }> {
|
||||||
|
return this.http.get<{ cart: ServerCart; lines: ServerCartLine[] }>('/api/v2/storefront/cart');
|
||||||
|
}
|
||||||
|
|
||||||
|
addLine(offerId: string, qty: number): Observable<ServerCartLine> {
|
||||||
|
return this.http.post<ServerCartLine>('/api/v2/storefront/cart/lines', { offerId, qty });
|
||||||
|
}
|
||||||
|
|
||||||
|
updateLine(lineId: string, qty: number): Observable<ServerCartLine> {
|
||||||
|
return this.http.patch<ServerCartLine>(`/api/v2/storefront/cart/lines/${encodeURIComponent(lineId)}`, { qty });
|
||||||
|
}
|
||||||
|
|
||||||
|
removeLine(lineId: string): Observable<void> {
|
||||||
|
return this.http
|
||||||
|
.delete(`/api/v2/storefront/cart/lines/${encodeURIComponent(lineId)}`)
|
||||||
|
.pipe(map(() => undefined));
|
||||||
|
}
|
||||||
|
|
||||||
|
startCheckout(deliveryOptionId: string, currency: string): Observable<CheckoutSession> {
|
||||||
|
// §5 example body is { cartId, currency, deliveryOptionId } - cartId is
|
||||||
|
// implicit server-side (the session's own cart), so it is not sent here.
|
||||||
|
return this.http.post<CheckoutSession>('/api/v2/storefront/checkout', { currency, deliveryOptionId });
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,11 @@
|
|||||||
import { InjectionToken, inject } from '@angular/core';
|
import { InjectionToken, inject } from '@angular/core';
|
||||||
|
import { environment } from '../../../../environments/environment';
|
||||||
import { ServerCartGateway } from './server-cart-gateway.interface';
|
import { ServerCartGateway } from './server-cart-gateway.interface';
|
||||||
import { ServerCartLocalGateway } from './server-cart-local.gateway';
|
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. */
|
/** Swap point for docs/backend/PHASE-6-CART-CHECKOUT-CONTRACT.md §3, §5. */
|
||||||
export const SERVER_CART_GATEWAY = new InjectionToken<ServerCartGateway>('SERVER_CART_GATEWAY', {
|
export const SERVER_CART_GATEWAY = new InjectionToken<ServerCartGateway>('SERVER_CART_GATEWAY', {
|
||||||
providedIn: 'root',
|
providedIn: 'root',
|
||||||
factory: () => inject(ServerCartLocalGateway),
|
factory: () => (environment.useMockData ? inject(ServerCartLocalGateway) : inject(ServerCartApiGateway)),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import { HttpClient } from '@angular/common/http';
|
||||||
|
import { Injectable, inject } from '@angular/core';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { Floor, Lead, RentListing, SchemePin, Shop, ShopCategory } from '../models/mall-content.model';
|
||||||
|
import { MallContentGateway } from './mall-content-gateway.interface';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Contract: docs/backend/PHASE-10-CONTENT-MODULES-CONTRACT.md §2.
|
||||||
|
* Only the lead-submission and settings endpoints are spelled out explicitly
|
||||||
|
* (§ "PATCH /api/admin/v2/content/mall-settings", "POST .../rent-listings/{id}/leads");
|
||||||
|
* the read endpoints below follow that same /api/admin/v2/content/ namespace.
|
||||||
|
*/
|
||||||
|
@Injectable({ providedIn: 'root' })
|
||||||
|
export class MallContentApiGateway implements MallContentGateway {
|
||||||
|
private readonly http = inject(HttpClient);
|
||||||
|
|
||||||
|
loadShops(): Observable<Shop[]> {
|
||||||
|
return this.http.get<Shop[]>('/api/admin/v2/content/shops');
|
||||||
|
}
|
||||||
|
|
||||||
|
loadShopCategories(): Observable<ShopCategory[]> {
|
||||||
|
return this.http.get<ShopCategory[]>('/api/admin/v2/content/shop-categories');
|
||||||
|
}
|
||||||
|
|
||||||
|
loadFloors(): Observable<Floor[]> {
|
||||||
|
return this.http.get<Floor[]>('/api/admin/v2/content/floors');
|
||||||
|
}
|
||||||
|
|
||||||
|
loadSchemePins(floorId: string): Observable<SchemePin[]> {
|
||||||
|
return this.http.get<SchemePin[]>(
|
||||||
|
`/api/admin/v2/content/floors/${encodeURIComponent(floorId)}/pins`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
loadRentListings(): Observable<RentListing[]> {
|
||||||
|
return this.http.get<RentListing[]>('/api/admin/v2/content/rent-listings');
|
||||||
|
}
|
||||||
|
|
||||||
|
submitLead(lead: Omit<Lead, 'id' | 'createdAt'>): Observable<Lead> {
|
||||||
|
const listingId = lead.rentListingId ?? '';
|
||||||
|
return this.http.post<Lead>(
|
||||||
|
`/api/admin/v2/content/rent-listings/${encodeURIComponent(listingId)}/leads`,
|
||||||
|
lead,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,11 @@
|
|||||||
import { InjectionToken, inject } from '@angular/core';
|
import { InjectionToken, inject } from '@angular/core';
|
||||||
|
import { environment } from '../../../../environments/environment';
|
||||||
import { MallContentGateway } from './mall-content-gateway.interface';
|
import { MallContentGateway } from './mall-content-gateway.interface';
|
||||||
import { MallContentLocalGateway } from './mall-content-local.gateway';
|
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. */
|
/** Swap point for docs/backend/PHASE-10-CONTENT-MODULES-CONTRACT.md. */
|
||||||
export const MALL_CONTENT_GATEWAY = new InjectionToken<MallContentGateway>('MALL_CONTENT_GATEWAY', {
|
export const MALL_CONTENT_GATEWAY = new InjectionToken<MallContentGateway>('MALL_CONTENT_GATEWAY', {
|
||||||
providedIn: 'root',
|
providedIn: 'root',
|
||||||
factory: () => inject(MallContentLocalGateway),
|
factory: () => (environment.useMockData ? inject(MallContentLocalGateway) : inject(MallContentApiGateway)),
|
||||||
});
|
});
|
||||||
|
|||||||
40
src/app/core/finance/services/finance-api.gateway.ts
Normal file
40
src/app/core/finance/services/finance-api.gateway.ts
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import { HttpClient, HttpParams } from '@angular/common/http';
|
||||||
|
import { Injectable, inject } from '@angular/core';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { map } from 'rxjs/operators';
|
||||||
|
import { ReconciliationRecord, Refund, Settlement } from '../models/reconciliation.model';
|
||||||
|
import { FinanceGateway } from './finance-gateway.interface';
|
||||||
|
|
||||||
|
/** Contract: docs/backend/PHASE-7-PAYMENTS-RECONCILIATION-CONTRACT.md §1-3. */
|
||||||
|
@Injectable({ providedIn: 'root' })
|
||||||
|
export class FinanceApiGateway implements FinanceGateway {
|
||||||
|
private readonly http = inject(HttpClient);
|
||||||
|
|
||||||
|
loadRefunds(orderId?: string): Observable<Refund[]> {
|
||||||
|
// §1: refunds are scoped to one order (POST/GET .../orders/{orderId}/refunds).
|
||||||
|
// Without an orderId there is no single-order endpoint to call; return
|
||||||
|
// empty rather than guess at a global refunds list the contract doesn't define.
|
||||||
|
if (!orderId) {
|
||||||
|
return new Observable<Refund[]>(subscriber => {
|
||||||
|
subscriber.next([]);
|
||||||
|
subscriber.complete();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return this.http.get<Refund[]>(`/api/admin/v2/orders/${encodeURIComponent(orderId)}/refunds`);
|
||||||
|
}
|
||||||
|
|
||||||
|
loadReconciliationQueue(): Observable<ReconciliationRecord[]> {
|
||||||
|
return this.http.get<ReconciliationRecord[]>('/api/admin/v2/reconciliation/queue');
|
||||||
|
}
|
||||||
|
|
||||||
|
resolveReconciliation(id: string, note: string): Observable<void> {
|
||||||
|
return this.http
|
||||||
|
.post(`/api/admin/v2/reconciliation/${encodeURIComponent(id)}/resolve`, { note })
|
||||||
|
.pipe(map(() => undefined));
|
||||||
|
}
|
||||||
|
|
||||||
|
loadSettlements(sellerId?: string): Observable<Settlement[]> {
|
||||||
|
const params = sellerId ? new HttpParams().set('sellerId', sellerId) : undefined;
|
||||||
|
return this.http.get<Settlement[]>('/api/admin/v2/finance/settlements', { params });
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,11 @@
|
|||||||
import { InjectionToken, inject } from '@angular/core';
|
import { InjectionToken, inject } from '@angular/core';
|
||||||
|
import { environment } from '../../../../environments/environment';
|
||||||
import { FinanceGateway } from './finance-gateway.interface';
|
import { FinanceGateway } from './finance-gateway.interface';
|
||||||
import { FinanceLocalGateway } from './finance-local.gateway';
|
import { FinanceLocalGateway } from './finance-local.gateway';
|
||||||
|
import { FinanceApiGateway } from './finance-api.gateway';
|
||||||
|
|
||||||
/** Swap point for docs/backend/PHASE-7-PAYMENTS-RECONCILIATION-CONTRACT.md. */
|
/** Swap point for docs/backend/PHASE-7-PAYMENTS-RECONCILIATION-CONTRACT.md. */
|
||||||
export const FINANCE_GATEWAY = new InjectionToken<FinanceGateway>('FINANCE_GATEWAY', {
|
export const FINANCE_GATEWAY = new InjectionToken<FinanceGateway>('FINANCE_GATEWAY', {
|
||||||
providedIn: 'root',
|
providedIn: 'root',
|
||||||
factory: () => inject(FinanceLocalGateway),
|
factory: () => (environment.useMockData ? inject(FinanceLocalGateway) : inject(FinanceApiGateway)),
|
||||||
});
|
});
|
||||||
|
|||||||
27
src/app/core/identity/services/vk-id-api.gateway.ts
Normal file
27
src/app/core/identity/services/vk-id-api.gateway.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
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 });
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,11 @@
|
|||||||
import { InjectionToken, inject } from '@angular/core';
|
import { InjectionToken, inject } from '@angular/core';
|
||||||
|
import { environment } from '../../../../environments/environment';
|
||||||
import { VkIdGateway } from './vk-id-gateway.interface';
|
import { VkIdGateway } from './vk-id-gateway.interface';
|
||||||
import { VkIdLocalGateway } from './vk-id-local.gateway';
|
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. */
|
/** Swap point for docs/backend/PHASE-8-IDENTITY-MESSAGING-CONTRACT.md §2. */
|
||||||
export const VK_ID_GATEWAY = new InjectionToken<VkIdGateway>('VK_ID_GATEWAY', {
|
export const VK_ID_GATEWAY = new InjectionToken<VkIdGateway>('VK_ID_GATEWAY', {
|
||||||
providedIn: 'root',
|
providedIn: 'root',
|
||||||
factory: () => inject(VkIdLocalGateway),
|
factory: () => (environment.useMockData ? inject(VkIdLocalGateway) : inject(VkIdApiGateway)),
|
||||||
});
|
});
|
||||||
|
|||||||
43
src/app/core/integrations/services/connector-api.gateway.ts
Normal file
43
src/app/core/integrations/services/connector-api.gateway.ts
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import { HttpClient } from '@angular/common/http';
|
||||||
|
import { Injectable, inject } from '@angular/core';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { map } from 'rxjs/operators';
|
||||||
|
import { Connector, DeadLetterEntry } from '../models/connector.model';
|
||||||
|
import { ConnectorGateway } from './connector-gateway.interface';
|
||||||
|
|
||||||
|
/** Contract: docs/backend/PHASE-4-CONNECTOR-FRAMEWORK-CONTRACT.md §7-8. */
|
||||||
|
@Injectable({ providedIn: 'root' })
|
||||||
|
export class ConnectorApiGateway implements ConnectorGateway {
|
||||||
|
private readonly http = inject(HttpClient);
|
||||||
|
|
||||||
|
loadConnectors(): Observable<Connector[]> {
|
||||||
|
return this.http.get<Connector[]>('/api/admin/v2/integrations');
|
||||||
|
}
|
||||||
|
|
||||||
|
loadDeadLetter(connectorId: string): Observable<DeadLetterEntry[]> {
|
||||||
|
// §7 defines dead-letter replay (POST .../dead-letter/{id}/replay) but not
|
||||||
|
// a GET list endpoint - inferred at the same collection path, consistent
|
||||||
|
// with the rest of this contract's REST conventions.
|
||||||
|
return this.http.get<DeadLetterEntry[]>(
|
||||||
|
`/api/admin/v2/integrations/${encodeURIComponent(connectorId)}/dead-letter`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
replay(deadLetterId: string): Observable<void> {
|
||||||
|
return this.http
|
||||||
|
.post(`/api/admin/v2/integrations/dead-letter/${encodeURIComponent(deadLetterId)}/replay`, {})
|
||||||
|
.pipe(map(() => undefined));
|
||||||
|
}
|
||||||
|
|
||||||
|
pause(connectorId: string): Observable<void> {
|
||||||
|
return this.http
|
||||||
|
.patch(`/api/admin/v2/integrations/${encodeURIComponent(connectorId)}`, { status: 'paused' })
|
||||||
|
.pipe(map(() => undefined));
|
||||||
|
}
|
||||||
|
|
||||||
|
resume(connectorId: string): Observable<void> {
|
||||||
|
return this.http
|
||||||
|
.patch(`/api/admin/v2/integrations/${encodeURIComponent(connectorId)}`, { status: 'active' })
|
||||||
|
.pipe(map(() => undefined));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,11 @@
|
|||||||
import { InjectionToken, inject } from '@angular/core';
|
import { InjectionToken, inject } from '@angular/core';
|
||||||
|
import { environment } from '../../../../environments/environment';
|
||||||
import { ConnectorGateway } from './connector-gateway.interface';
|
import { ConnectorGateway } from './connector-gateway.interface';
|
||||||
import { ConnectorLocalGateway } from './connector-local.gateway';
|
import { ConnectorLocalGateway } from './connector-local.gateway';
|
||||||
|
import { ConnectorApiGateway } from './connector-api.gateway';
|
||||||
|
|
||||||
/** Swap point for docs/backend/PHASE-4-CONNECTOR-FRAMEWORK-CONTRACT.md §7. */
|
/** Swap point for docs/backend/PHASE-4-CONNECTOR-FRAMEWORK-CONTRACT.md §7. */
|
||||||
export const CONNECTOR_GATEWAY = new InjectionToken<ConnectorGateway>('CONNECTOR_GATEWAY', {
|
export const CONNECTOR_GATEWAY = new InjectionToken<ConnectorGateway>('CONNECTOR_GATEWAY', {
|
||||||
providedIn: 'root',
|
providedIn: 'root',
|
||||||
factory: () => inject(ConnectorLocalGateway),
|
factory: () => (environment.useMockData ? inject(ConnectorLocalGateway) : inject(ConnectorApiGateway)),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import { HttpClient } from '@angular/common/http';
|
||||||
|
import { Injectable, inject } from '@angular/core';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { LifecycleAdvanceResult, Marketplace, MarketplaceDomain } from '../models/marketplace.model';
|
||||||
|
import { MarketplaceGateway } from './marketplace-gateway.interface';
|
||||||
|
|
||||||
|
/** Contract: docs/backend/PHASE-9-TENANT-REGISTRY-DOMAINS-CONTRACT.md §2-4. */
|
||||||
|
@Injectable({ providedIn: 'root' })
|
||||||
|
export class MarketplaceApiGateway implements MarketplaceGateway {
|
||||||
|
private readonly http = inject(HttpClient);
|
||||||
|
|
||||||
|
loadMarketplaces(): Observable<Marketplace[]> {
|
||||||
|
// The contract specifies POST/PATCH marketplace endpoints in detail but
|
||||||
|
// not the list GET explicitly - inferred at the collection root, the
|
||||||
|
// conventional counterpart to POST /api/admin/v2/marketplaces (§3 step 1).
|
||||||
|
return this.http.get<Marketplace[]>('/api/admin/v2/marketplaces');
|
||||||
|
}
|
||||||
|
|
||||||
|
loadDomains(marketplaceId: string): Observable<MarketplaceDomain[]> {
|
||||||
|
return this.http.get<MarketplaceDomain[]>(
|
||||||
|
`/api/admin/v2/marketplaces/${encodeURIComponent(marketplaceId)}/domains`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
loadLifecycle(marketplaceId: string): Observable<LifecycleAdvanceResult> {
|
||||||
|
return this.http.get<LifecycleAdvanceResult>(
|
||||||
|
`/api/admin/v2/marketplaces/${encodeURIComponent(marketplaceId)}/lifecycle`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,11 @@
|
|||||||
import { InjectionToken, inject } from '@angular/core';
|
import { InjectionToken, inject } from '@angular/core';
|
||||||
|
import { environment } from '../../../../environments/environment';
|
||||||
import { MarketplaceGateway } from './marketplace-gateway.interface';
|
import { MarketplaceGateway } from './marketplace-gateway.interface';
|
||||||
import { MarketplaceLocalGateway } from './marketplace-local.gateway';
|
import { MarketplaceLocalGateway } from './marketplace-local.gateway';
|
||||||
|
import { MarketplaceApiGateway } from './marketplace-api.gateway';
|
||||||
|
|
||||||
/** Swap point for docs/backend/PHASE-9-TENANT-REGISTRY-DOMAINS-CONTRACT.md. */
|
/** Swap point for docs/backend/PHASE-9-TENANT-REGISTRY-DOMAINS-CONTRACT.md. */
|
||||||
export const MARKETPLACE_GATEWAY = new InjectionToken<MarketplaceGateway>('MARKETPLACE_GATEWAY', {
|
export const MARKETPLACE_GATEWAY = new InjectionToken<MarketplaceGateway>('MARKETPLACE_GATEWAY', {
|
||||||
providedIn: 'root',
|
providedIn: 'root',
|
||||||
factory: () => inject(MarketplaceLocalGateway),
|
factory: () => (environment.useMockData ? inject(MarketplaceLocalGateway) : inject(MarketplaceApiGateway)),
|
||||||
});
|
});
|
||||||
|
|||||||
56
src/app/core/offers/services/offer-api.gateway.ts
Normal file
56
src/app/core/offers/services/offer-api.gateway.ts
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import { HttpClient, HttpErrorResponse, HttpParams } from '@angular/common/http';
|
||||||
|
import { Injectable, inject } from '@angular/core';
|
||||||
|
import { Observable, of } from 'rxjs';
|
||||||
|
import { catchError } from 'rxjs/operators';
|
||||||
|
import { InventoryRecord, Offer, OfferLookupQuery } from '../models/offer.model';
|
||||||
|
import { OfferGateway } from './offer-gateway.interface';
|
||||||
|
|
||||||
|
/** Contract: docs/backend/PHASE-3-CATALOG-OFFER-FULFILLMENT-CONTRACT.md §7. */
|
||||||
|
@Injectable({ providedIn: 'root' })
|
||||||
|
export class OfferApiGateway implements OfferGateway {
|
||||||
|
private readonly http = inject(HttpClient);
|
||||||
|
|
||||||
|
loadOffers(sellerId?: string): Observable<Offer[]> {
|
||||||
|
const params = sellerId ? new HttpParams().set('sellerId', sellerId) : undefined;
|
||||||
|
return this.http.get<Offer[]>('/api/admin/v2/offers', { params });
|
||||||
|
}
|
||||||
|
|
||||||
|
lookup(query: OfferLookupQuery): Observable<Offer[]> {
|
||||||
|
let params = new HttpParams();
|
||||||
|
for (const [key, value] of Object.entries(query)) {
|
||||||
|
if (value) {
|
||||||
|
params = params.set(key, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return this.http.get<Offer[]>('/api/admin/v2/offers/lookup', { params });
|
||||||
|
}
|
||||||
|
|
||||||
|
loadInventory(offerId: string): Observable<InventoryRecord | null> {
|
||||||
|
// §7 does not define a dedicated inventory-by-offer endpoint - inferred
|
||||||
|
// as a sub-resource of the offer, consistent with this contract's
|
||||||
|
// {id}/publish pattern.
|
||||||
|
return this.http.get<InventoryRecord | null>(
|
||||||
|
`/api/admin/v2/offers/${encodeURIComponent(offerId)}/inventory`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
publish(offerId: string): Observable<{ ok: true } | { ok: false; errors: string[] }> {
|
||||||
|
// §7: publish runs the §5 executability check and fails with 422 +
|
||||||
|
// details[], not a 200 body - translated here into the interface's
|
||||||
|
// ok:false shape so callers get a value, not an error to catch.
|
||||||
|
return this.http
|
||||||
|
.post<{ ok: true }>(`/api/admin/v2/offers/${encodeURIComponent(offerId)}/publish`, {})
|
||||||
|
.pipe(
|
||||||
|
catchError((err: HttpErrorResponse) => {
|
||||||
|
if (err.status === 422) {
|
||||||
|
const details = err.error?.error?.details ?? [];
|
||||||
|
const errors = Array.isArray(details) && details.length > 0
|
||||||
|
? details.map((d: { message?: string }) => d.message ?? 'Validation failed')
|
||||||
|
: ['Offer is not executable.'];
|
||||||
|
return of({ ok: false as const, errors });
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,11 @@
|
|||||||
import { InjectionToken, inject } from '@angular/core';
|
import { InjectionToken, inject } from '@angular/core';
|
||||||
|
import { environment } from '../../../../environments/environment';
|
||||||
import { OfferGateway } from './offer-gateway.interface';
|
import { OfferGateway } from './offer-gateway.interface';
|
||||||
import { OfferLocalGateway } from './offer-local.gateway';
|
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. */
|
/** Swap point for docs/backend/PHASE-3-CATALOG-OFFER-FULFILLMENT-CONTRACT.md §7. */
|
||||||
export const OFFER_GATEWAY = new InjectionToken<OfferGateway>('OFFER_GATEWAY', {
|
export const OFFER_GATEWAY = new InjectionToken<OfferGateway>('OFFER_GATEWAY', {
|
||||||
providedIn: 'root',
|
providedIn: 'root',
|
||||||
factory: () => inject(OfferLocalGateway),
|
factory: () => (environment.useMockData ? inject(OfferLocalGateway) : inject(OfferApiGateway)),
|
||||||
});
|
});
|
||||||
|
|||||||
19
src/app/core/permissions/services/permission-api.gateway.ts
Normal file
19
src/app/core/permissions/services/permission-api.gateway.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { HttpClient } from '@angular/common/http';
|
||||||
|
import { Injectable, inject } from '@angular/core';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { AuditEvent, SessionPermissions } from '../models/permission.model';
|
||||||
|
import { PermissionGateway } from './permission-gateway.interface';
|
||||||
|
|
||||||
|
/** Contract: docs/backend/TRACK-S-SECURITY-RBAC-CONTRACT.md §2-3. */
|
||||||
|
@Injectable({ providedIn: 'root' })
|
||||||
|
export class PermissionApiGateway implements PermissionGateway {
|
||||||
|
private readonly http = inject(HttpClient);
|
||||||
|
|
||||||
|
getSessionPermissions(): Observable<SessionPermissions> {
|
||||||
|
return this.http.get<SessionPermissions>('/api/identity/v1/session/permissions');
|
||||||
|
}
|
||||||
|
|
||||||
|
loadAuditLog(): Observable<AuditEvent[]> {
|
||||||
|
return this.http.get<AuditEvent[]>('/api/admin/v2/audit');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,11 @@
|
|||||||
import { InjectionToken, inject } from '@angular/core';
|
import { InjectionToken, inject } from '@angular/core';
|
||||||
|
import { environment } from '../../../../environments/environment';
|
||||||
import { PermissionGateway } from './permission-gateway.interface';
|
import { PermissionGateway } from './permission-gateway.interface';
|
||||||
import { PermissionLocalGateway } from './permission-local.gateway';
|
import { PermissionLocalGateway } from './permission-local.gateway';
|
||||||
|
import { PermissionApiGateway } from './permission-api.gateway';
|
||||||
|
|
||||||
/** Swap point for docs/backend/TRACK-S-SECURITY-RBAC-CONTRACT.md §2. */
|
/** Swap point for docs/backend/TRACK-S-SECURITY-RBAC-CONTRACT.md §2-3. */
|
||||||
export const PERMISSION_GATEWAY = new InjectionToken<PermissionGateway>('PERMISSION_GATEWAY', {
|
export const PERMISSION_GATEWAY = new InjectionToken<PermissionGateway>('PERMISSION_GATEWAY', {
|
||||||
providedIn: 'root',
|
providedIn: 'root',
|
||||||
factory: () => inject(PermissionLocalGateway),
|
factory: () => (environment.useMockData ? inject(PermissionLocalGateway) : inject(PermissionApiGateway)),
|
||||||
});
|
});
|
||||||
|
|||||||
27
src/app/core/sellers/services/seller-api.gateway.ts
Normal file
27
src/app/core/sellers/services/seller-api.gateway.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import { HttpClient } from '@angular/common/http';
|
||||||
|
import { Injectable, inject } from '@angular/core';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { Seller } from '../models/seller.model';
|
||||||
|
import { SellerUser } from '../models/seller-user.model';
|
||||||
|
import { SellerGateway } from './seller-gateway.interface';
|
||||||
|
|
||||||
|
/** Contract: docs/backend/PHASE-5-SELLER-PORTAL-CONTRACT.md §3. */
|
||||||
|
@Injectable({ providedIn: 'root' })
|
||||||
|
export class SellerApiGateway implements SellerGateway {
|
||||||
|
private readonly http = inject(HttpClient);
|
||||||
|
|
||||||
|
loadSellers(): Observable<Seller[]> {
|
||||||
|
// §3 defines GET /api/seller/v1/profile (the acting seller's own record).
|
||||||
|
// An admin-side list-all-sellers endpoint isn't in this contract; inferred
|
||||||
|
// at the conventional admin collection path pending that being specified.
|
||||||
|
return this.http.get<Seller[]>('/api/admin/v2/sellers');
|
||||||
|
}
|
||||||
|
|
||||||
|
loadTeam(sellerId: string): Observable<SellerUser[]> {
|
||||||
|
// §3's GET /api/seller/v1/team is scoped to the caller's own session, not
|
||||||
|
// parameterized by sellerId. Using the admin-side sub-resource path so
|
||||||
|
// an admin looking at a specific seller's team has a real endpoint to
|
||||||
|
// call; revisit if the backend exposes this differently.
|
||||||
|
return this.http.get<SellerUser[]>(`/api/admin/v2/sellers/${encodeURIComponent(sellerId)}/team`);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,11 @@
|
|||||||
import { InjectionToken, inject } from '@angular/core';
|
import { InjectionToken, inject } from '@angular/core';
|
||||||
|
import { environment } from '../../../../environments/environment';
|
||||||
import { SellerGateway } from './seller-gateway.interface';
|
import { SellerGateway } from './seller-gateway.interface';
|
||||||
import { SellerLocalGateway } from './seller-local.gateway';
|
import { SellerLocalGateway } from './seller-local.gateway';
|
||||||
|
import { SellerApiGateway } from './seller-api.gateway';
|
||||||
|
|
||||||
/** Swap point for docs/backend/PHASE-5-SELLER-PORTAL-CONTRACT.md §3. */
|
/** Swap point for docs/backend/PHASE-5-SELLER-PORTAL-CONTRACT.md §3. */
|
||||||
export const SELLER_GATEWAY = new InjectionToken<SellerGateway>('SELLER_GATEWAY', {
|
export const SELLER_GATEWAY = new InjectionToken<SellerGateway>('SELLER_GATEWAY', {
|
||||||
providedIn: 'root',
|
providedIn: 'root',
|
||||||
factory: () => inject(SellerLocalGateway),
|
factory: () => (environment.useMockData ? inject(SellerLocalGateway) : inject(SellerApiGateway)),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
import { TestBed } from '@angular/core/testing';
|
import { TestBed } from '@angular/core/testing';
|
||||||
import { of } from 'rxjs';
|
import { of } from 'rxjs';
|
||||||
import { AdminAnalyticsFacade } from './admin-analytics.facade';
|
import { AdminAnalyticsFacade } from './admin-analytics.facade';
|
||||||
import { AdminOrdersLocalGateway } from '../../orders/services/admin-orders-local.gateway';
|
import { AdminOrdersGateway } from '../../orders/services/admin-orders-gateway.interface';
|
||||||
import { AdminProductsLocalGateway } from '../../products/services/admin-products-local.gateway';
|
import { ADMIN_ORDERS_GATEWAY } from '../../orders/services/admin-orders-gateway.token';
|
||||||
|
import { AdminProductsGateway } from '../../products/services/admin-products-gateway.interface';
|
||||||
|
import { ADMIN_PRODUCTS_GATEWAY } from '../../products/services/admin-products-gateway.token';
|
||||||
import { ADMIN_CATEGORIES_GATEWAY } from '../../categories/services/admin-categories-gateway.token';
|
import { ADMIN_CATEGORIES_GATEWAY } from '../../categories/services/admin-categories-gateway.token';
|
||||||
import { AdminModerationLocalGateway } from '../../moderation/services/admin-moderation-local.gateway';
|
import { AdminModerationGateway } from '../../moderation/services/admin-moderation-gateway.interface';
|
||||||
|
import { ADMIN_MODERATION_GATEWAY } from '../../moderation/services/admin-moderation-gateway.token';
|
||||||
import { AdminDashboardFacade } from '../../dashboard/facade/admin-dashboard.facade';
|
import { AdminDashboardFacade } from '../../dashboard/facade/admin-dashboard.facade';
|
||||||
import { AdminOrder } from '../../orders/models/admin-order.model';
|
import { AdminOrder } from '../../orders/models/admin-order.model';
|
||||||
|
|
||||||
@@ -23,17 +26,17 @@ function makeOrder(overrides: Partial<AdminOrder> = {}): AdminOrder {
|
|||||||
|
|
||||||
describe('AdminAnalyticsFacade (never-fabricate-a-number contract)', () => {
|
describe('AdminAnalyticsFacade (never-fabricate-a-number contract)', () => {
|
||||||
let facade: AdminAnalyticsFacade;
|
let facade: AdminAnalyticsFacade;
|
||||||
let ordersGateway: jasmine.SpyObj<AdminOrdersLocalGateway>;
|
let ordersGateway: jasmine.SpyObj<AdminOrdersGateway>;
|
||||||
let productsGateway: jasmine.SpyObj<AdminProductsLocalGateway>;
|
let productsGateway: jasmine.SpyObj<AdminProductsGateway>;
|
||||||
let categoriesGateway: jasmine.SpyObj<{ loadCategories: () => unknown }>;
|
let categoriesGateway: jasmine.SpyObj<{ loadCategories: () => unknown }>;
|
||||||
let moderationGateway: jasmine.SpyObj<AdminModerationLocalGateway>;
|
let moderationGateway: jasmine.SpyObj<AdminModerationGateway>;
|
||||||
let dashboardFacade: jasmine.SpyObj<AdminDashboardFacade>;
|
let dashboardFacade: jasmine.SpyObj<AdminDashboardFacade>;
|
||||||
|
|
||||||
function configure(bootstrapPresent: boolean): void {
|
function configure(bootstrapPresent: boolean): void {
|
||||||
ordersGateway = jasmine.createSpyObj('AdminOrdersLocalGateway', ['loadOrders']);
|
ordersGateway = jasmine.createSpyObj('ADMIN_ORDERS_GATEWAY', ['loadOrders']);
|
||||||
productsGateway = jasmine.createSpyObj('AdminProductsLocalGateway', ['loadProducts']);
|
productsGateway = jasmine.createSpyObj('ADMIN_PRODUCTS_GATEWAY', ['loadProducts']);
|
||||||
categoriesGateway = jasmine.createSpyObj('ADMIN_CATEGORIES_GATEWAY', ['loadCategories']);
|
categoriesGateway = jasmine.createSpyObj('ADMIN_CATEGORIES_GATEWAY', ['loadCategories']);
|
||||||
moderationGateway = jasmine.createSpyObj('AdminModerationLocalGateway', ['loadReviews']);
|
moderationGateway = jasmine.createSpyObj('ADMIN_MODERATION_GATEWAY', ['loadReviews']);
|
||||||
dashboardFacade = jasmine.createSpyObj('AdminDashboardFacade', [
|
dashboardFacade = jasmine.createSpyObj('AdminDashboardFacade', [
|
||||||
'ensureLoaded', 'activityEntries', 'bootstrap', 'validationIssues', 'enabledWidgetsCount', 'staticPagesUnpublishedCount',
|
'ensureLoaded', 'activityEntries', 'bootstrap', 'validationIssues', 'enabledWidgetsCount', 'staticPagesUnpublishedCount',
|
||||||
]);
|
]);
|
||||||
@@ -50,10 +53,10 @@ describe('AdminAnalyticsFacade (never-fabricate-a-number contract)', () => {
|
|||||||
|
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
providers: [
|
providers: [
|
||||||
{ provide: AdminOrdersLocalGateway, useValue: ordersGateway },
|
{ provide: ADMIN_ORDERS_GATEWAY, useValue: ordersGateway },
|
||||||
{ provide: AdminProductsLocalGateway, useValue: productsGateway },
|
{ provide: ADMIN_PRODUCTS_GATEWAY, useValue: productsGateway },
|
||||||
{ provide: ADMIN_CATEGORIES_GATEWAY, useValue: categoriesGateway },
|
{ provide: ADMIN_CATEGORIES_GATEWAY, useValue: categoriesGateway },
|
||||||
{ provide: AdminModerationLocalGateway, useValue: moderationGateway },
|
{ provide: ADMIN_MODERATION_GATEWAY, useValue: moderationGateway },
|
||||||
{ provide: AdminDashboardFacade, useValue: dashboardFacade },
|
{ provide: AdminDashboardFacade, useValue: dashboardFacade },
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { HttpClient } from '@angular/common/http';
|
||||||
|
import { Injectable, inject } from '@angular/core';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { AdminDashboardMetrics } from '../models/admin-dashboard.model';
|
||||||
|
import { AdminDashboardMetricsGateway } from './admin-dashboard-metrics.gateway.interface';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* No dedicated dashboard-metrics contract doc exists yet. Phase 9 §7 lists
|
||||||
|
* the target metrics (GMV, paid orders, conversion, etc.) without a wire
|
||||||
|
* shape - at the conventional /api/admin/v2/{resource} path pending that.
|
||||||
|
*/
|
||||||
|
@Injectable({ providedIn: 'root' })
|
||||||
|
export class AdminDashboardMetricsApiGateway implements AdminDashboardMetricsGateway {
|
||||||
|
private readonly http = inject(HttpClient);
|
||||||
|
|
||||||
|
loadMetrics(): Observable<AdminDashboardMetrics> {
|
||||||
|
return this.http.get<AdminDashboardMetrics>('/api/admin/v2/dashboard/metrics');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,11 @@
|
|||||||
import { InjectionToken, inject } from '@angular/core';
|
import { InjectionToken, inject } from '@angular/core';
|
||||||
|
import { environment } from '../../../../../environments/environment';
|
||||||
import { AdminDashboardMetricsGateway } from './admin-dashboard-metrics.gateway.interface';
|
import { AdminDashboardMetricsGateway } from './admin-dashboard-metrics.gateway.interface';
|
||||||
import { AdminDashboardMetricsLocalGateway } from './admin-dashboard-metrics.local.gateway';
|
import { AdminDashboardMetricsLocalGateway } from './admin-dashboard-metrics.local.gateway';
|
||||||
|
import { AdminDashboardMetricsApiGateway } from './admin-dashboard-metrics-api.gateway';
|
||||||
|
|
||||||
/** Swap point for a future dedicated dashboard-metrics backend endpoint - today it composes existing backoffice data sources. */
|
/** Swap point. */
|
||||||
export const ADMIN_DASHBOARD_METRICS_GATEWAY = new InjectionToken<AdminDashboardMetricsGateway>('ADMIN_DASHBOARD_METRICS_GATEWAY', {
|
export const ADMIN_DASHBOARD_METRICS_GATEWAY = new InjectionToken<AdminDashboardMetricsGateway>('ADMIN_DASHBOARD_METRICS_GATEWAY', {
|
||||||
providedIn: 'root',
|
providedIn: 'root',
|
||||||
factory: () => inject(AdminDashboardMetricsLocalGateway),
|
factory: () => (environment.useMockData ? inject(AdminDashboardMetricsLocalGateway) : inject(AdminDashboardMetricsApiGateway)),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
import { HttpClient, HttpParams } from '@angular/common/http';
|
||||||
|
import { Injectable, inject } from '@angular/core';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { map } from 'rxjs/operators';
|
||||||
|
import { AdminReview, AdminReviewListFilters, AdminReviewsListResult, AdminReviewStatus } from '../models/admin-review.model';
|
||||||
|
import { AdminReport, AdminReportStatus } from '../models/admin-report.model';
|
||||||
|
import { AdminModerationGateway } from './admin-moderation-gateway.interface';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* No dedicated moderation contract doc exists yet - at the conventional
|
||||||
|
* /api/admin/v2/{resource} path used throughout the rest of docs/backend/.
|
||||||
|
*/
|
||||||
|
@Injectable({ providedIn: 'root' })
|
||||||
|
export class AdminModerationApiGateway implements AdminModerationGateway {
|
||||||
|
private readonly http = inject(HttpClient);
|
||||||
|
|
||||||
|
loadReviews(filters: AdminReviewListFilters): Observable<AdminReviewsListResult> {
|
||||||
|
let params = new HttpParams();
|
||||||
|
for (const [key, value] of Object.entries(filters ?? {})) {
|
||||||
|
if (value !== undefined && value !== null && value !== '') {
|
||||||
|
params = params.set(key, String(value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return this.http.get<AdminReviewsListResult>('/api/admin/v2/moderation/reviews', { params });
|
||||||
|
}
|
||||||
|
|
||||||
|
loadReview(id: string): Observable<AdminReview | null> {
|
||||||
|
return this.http.get<AdminReview | null>(`/api/admin/v2/moderation/reviews/${encodeURIComponent(id)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
setReviewStatus(id: string, status: AdminReviewStatus, note: string): Observable<AdminReview | null> {
|
||||||
|
return this.http.patch<AdminReview | null>(`/api/admin/v2/moderation/reviews/${encodeURIComponent(id)}`, { status, note });
|
||||||
|
}
|
||||||
|
|
||||||
|
setReviewVisible(id: string, visible: boolean): Observable<AdminReview | null> {
|
||||||
|
return this.http.patch<AdminReview | null>(`/api/admin/v2/moderation/reviews/${encodeURIComponent(id)}`, { visible });
|
||||||
|
}
|
||||||
|
|
||||||
|
setReviewPinned(id: string, pinned: boolean): Observable<AdminReview | null> {
|
||||||
|
return this.http.patch<AdminReview | null>(`/api/admin/v2/moderation/reviews/${encodeURIComponent(id)}`, { pinned });
|
||||||
|
}
|
||||||
|
|
||||||
|
setReviewFeatured(id: string, featured: boolean): Observable<AdminReview | null> {
|
||||||
|
return this.http.patch<AdminReview | null>(`/api/admin/v2/moderation/reviews/${encodeURIComponent(id)}`, { featured });
|
||||||
|
}
|
||||||
|
|
||||||
|
addModeratorNote(id: string, note: string): Observable<AdminReview | null> {
|
||||||
|
return this.http.post<AdminReview | null>(`/api/admin/v2/moderation/reviews/${encodeURIComponent(id)}/notes`, { note });
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteReview(id: string): Observable<void> {
|
||||||
|
return this.http
|
||||||
|
.delete(`/api/admin/v2/moderation/reviews/${encodeURIComponent(id)}`)
|
||||||
|
.pipe(map(() => undefined));
|
||||||
|
}
|
||||||
|
|
||||||
|
loadReports(): Observable<AdminReport[]> {
|
||||||
|
return this.http.get<AdminReport[]>('/api/admin/v2/moderation/reports');
|
||||||
|
}
|
||||||
|
|
||||||
|
setReportStatus(id: string, status: AdminReportStatus): Observable<AdminReport | null> {
|
||||||
|
return this.http.patch<AdminReport | null>(`/api/admin/v2/moderation/reports/${encodeURIComponent(id)}`, { status });
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,11 @@
|
|||||||
import { InjectionToken, inject } from '@angular/core';
|
import { InjectionToken, inject } from '@angular/core';
|
||||||
|
import { environment } from '../../../../../environments/environment';
|
||||||
import { AdminModerationGateway } from './admin-moderation-gateway.interface';
|
import { AdminModerationGateway } from './admin-moderation-gateway.interface';
|
||||||
import { AdminModerationLocalGateway } from './admin-moderation-local.gateway';
|
import { AdminModerationLocalGateway } from './admin-moderation-local.gateway';
|
||||||
|
import { AdminModerationApiGateway } from './admin-moderation-api.gateway';
|
||||||
|
|
||||||
/** Swap point for a real Moderation backend - see BACKEND-API-REFERENCE.md §8 (no seam existed before this token). */
|
/** Swap point. */
|
||||||
export const ADMIN_MODERATION_GATEWAY = new InjectionToken<AdminModerationGateway>('ADMIN_MODERATION_GATEWAY', {
|
export const ADMIN_MODERATION_GATEWAY = new InjectionToken<AdminModerationGateway>('ADMIN_MODERATION_GATEWAY', {
|
||||||
providedIn: 'root',
|
providedIn: 'root',
|
||||||
factory: () => inject(AdminModerationLocalGateway),
|
factory: () => (environment.useMockData ? inject(AdminModerationLocalGateway) : inject(AdminModerationApiGateway)),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import { HttpClient, HttpParams } from '@angular/common/http';
|
||||||
|
import { Injectable, inject } from '@angular/core';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { AdminMonitoringEvent, AdminMonitoringEventFilters, AdminQueue, AdminWebhookDelivery } from '../models/admin-monitoring.model';
|
||||||
|
import { AdminMonitoringGateway } from './admin-monitoring-gateway.interface';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* No dedicated monitoring contract doc exists yet - at the conventional
|
||||||
|
* /api/admin/v2/{resource} path per the interface's own doc comment
|
||||||
|
* (a future HTTP gateway implements the same shape).
|
||||||
|
*/
|
||||||
|
@Injectable({ providedIn: 'root' })
|
||||||
|
export class AdminMonitoringApiGateway implements AdminMonitoringGateway {
|
||||||
|
private readonly http = inject(HttpClient);
|
||||||
|
|
||||||
|
loadEvents(filters: AdminMonitoringEventFilters): Observable<AdminMonitoringEvent[]> {
|
||||||
|
let params = new HttpParams();
|
||||||
|
for (const [key, value] of Object.entries(filters ?? {})) {
|
||||||
|
if (value !== undefined && value !== null && value !== '') {
|
||||||
|
params = params.set(key, String(value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return this.http.get<AdminMonitoringEvent[]>('/api/admin/v2/monitoring/events', { params });
|
||||||
|
}
|
||||||
|
|
||||||
|
loadQueues(): Observable<AdminQueue[]> {
|
||||||
|
return this.http.get<AdminQueue[]>('/api/admin/v2/monitoring/queues');
|
||||||
|
}
|
||||||
|
|
||||||
|
loadWebhooks(): Observable<AdminWebhookDelivery[]> {
|
||||||
|
return this.http.get<AdminWebhookDelivery[]>('/api/admin/v2/monitoring/webhooks');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,11 @@
|
|||||||
import { InjectionToken, inject } from '@angular/core';
|
import { InjectionToken, inject } from '@angular/core';
|
||||||
|
import { environment } from '../../../../../environments/environment';
|
||||||
import { AdminMonitoringGateway } from './admin-monitoring-gateway.interface';
|
import { AdminMonitoringGateway } from './admin-monitoring-gateway.interface';
|
||||||
import { AdminMonitoringLocalGateway } from './admin-monitoring-local.gateway';
|
import { AdminMonitoringLocalGateway } from './admin-monitoring-local.gateway';
|
||||||
|
import { AdminMonitoringApiGateway } from './admin-monitoring-api.gateway';
|
||||||
|
|
||||||
/** Swap point for a real Monitoring backend - see BACKEND-API-REFERENCE.md §8 (no seam existed before this token). */
|
/** Swap point. */
|
||||||
export const ADMIN_MONITORING_GATEWAY = new InjectionToken<AdminMonitoringGateway>('ADMIN_MONITORING_GATEWAY', {
|
export const ADMIN_MONITORING_GATEWAY = new InjectionToken<AdminMonitoringGateway>('ADMIN_MONITORING_GATEWAY', {
|
||||||
providedIn: 'root',
|
providedIn: 'root',
|
||||||
factory: () => inject(AdminMonitoringLocalGateway),
|
factory: () => (environment.useMockData ? inject(AdminMonitoringLocalGateway) : inject(AdminMonitoringApiGateway)),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import { HttpClient, HttpParams } from '@angular/common/http';
|
||||||
|
import { Injectable, inject } from '@angular/core';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { map } from 'rxjs/operators';
|
||||||
|
import { AdminNotification, AdminNotificationFilters } from '../models/admin-notification.model';
|
||||||
|
import { AdminNotificationsGateway } from './admin-notifications-gateway.interface';
|
||||||
|
|
||||||
|
/** Contract: docs/backend/PHASE-2-ORDERS-NOTIFICATIONS-CONTRACT.md §7. */
|
||||||
|
@Injectable({ providedIn: 'root' })
|
||||||
|
export class AdminNotificationsApiGateway implements AdminNotificationsGateway {
|
||||||
|
private readonly http = inject(HttpClient);
|
||||||
|
|
||||||
|
loadNotifications(filters: AdminNotificationFilters): Observable<AdminNotification[]> {
|
||||||
|
let params = new HttpParams();
|
||||||
|
for (const [key, value] of Object.entries(filters ?? {})) {
|
||||||
|
if (value !== undefined && value !== null && value !== '') {
|
||||||
|
params = params.set(key, String(value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return this.http.get<AdminNotification[]>('/api/admin/v2/notifications', { params });
|
||||||
|
}
|
||||||
|
|
||||||
|
markRead(id: string): Observable<void> {
|
||||||
|
return this.http
|
||||||
|
.patch(`/api/admin/v2/notifications/${encodeURIComponent(id)}/read`, {})
|
||||||
|
.pipe(map(() => undefined));
|
||||||
|
}
|
||||||
|
|
||||||
|
markAllRead(): Observable<void> {
|
||||||
|
// Contract documents per-notification PATCH .../{id}/read only; a bulk
|
||||||
|
// endpoint isn't specified. Inferred at the collection path pending that
|
||||||
|
// being defined - revisit if the backend does this differently.
|
||||||
|
return this.http
|
||||||
|
.patch('/api/admin/v2/notifications/read-all', {})
|
||||||
|
.pipe(map(() => undefined));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,11 @@
|
|||||||
import { InjectionToken, inject } from '@angular/core';
|
import { InjectionToken, inject } from '@angular/core';
|
||||||
|
import { environment } from '../../../../../environments/environment';
|
||||||
import { AdminNotificationsGateway } from './admin-notifications-gateway.interface';
|
import { AdminNotificationsGateway } from './admin-notifications-gateway.interface';
|
||||||
import { AdminNotificationsLocalGateway } from './admin-notifications-local.gateway';
|
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 §6. */
|
/** Swap point for docs/backend/PHASE-2-ORDERS-NOTIFICATIONS-CONTRACT.md §7. */
|
||||||
export const ADMIN_NOTIFICATIONS_GATEWAY = new InjectionToken<AdminNotificationsGateway>('ADMIN_NOTIFICATIONS_GATEWAY', {
|
export const ADMIN_NOTIFICATIONS_GATEWAY = new InjectionToken<AdminNotificationsGateway>('ADMIN_NOTIFICATIONS_GATEWAY', {
|
||||||
providedIn: 'root',
|
providedIn: 'root',
|
||||||
factory: () => inject(AdminNotificationsLocalGateway),
|
factory: () => (environment.useMockData ? inject(AdminNotificationsLocalGateway) : inject(AdminNotificationsApiGateway)),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
import { HttpClient, HttpParams } from '@angular/common/http';
|
||||||
|
import { Injectable, inject } from '@angular/core';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { map } from 'rxjs/operators';
|
||||||
|
import { AdminOrder, AdminOrderListFilters, AdminOrdersListResult, AdminOrderStatus } from '../models/admin-order.model';
|
||||||
|
import { AdminOrdersGateway } from './admin-orders-gateway.interface';
|
||||||
|
|
||||||
|
/** Contract: docs/backend/PHASE-2-ORDERS-NOTIFICATIONS-CONTRACT.md. */
|
||||||
|
@Injectable({ providedIn: 'root' })
|
||||||
|
export class AdminOrdersApiGateway implements AdminOrdersGateway {
|
||||||
|
private readonly http = inject(HttpClient);
|
||||||
|
|
||||||
|
loadOrders(filters: AdminOrderListFilters): Observable<AdminOrdersListResult> {
|
||||||
|
let params = new HttpParams();
|
||||||
|
for (const [key, value] of Object.entries(filters ?? {})) {
|
||||||
|
if (value !== undefined && value !== null && value !== '') {
|
||||||
|
params = params.set(key, String(value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return this.http.get<AdminOrdersListResult>('/api/admin/v2/orders', { params });
|
||||||
|
}
|
||||||
|
|
||||||
|
loadOrder(id: string): Observable<AdminOrder | null> {
|
||||||
|
return this.http.get<AdminOrder | null>(`/api/admin/v2/orders/${encodeURIComponent(id)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateStatus(id: string, status: AdminOrderStatus): Observable<AdminOrder | null> {
|
||||||
|
return this.http.patch<AdminOrder | null>(`/api/admin/v2/orders/${encodeURIComponent(id)}/status`, { status });
|
||||||
|
}
|
||||||
|
|
||||||
|
requestRefund(id: string): Observable<AdminOrder | null> {
|
||||||
|
return this.http.post<AdminOrder | null>(`/api/admin/v2/orders/${encodeURIComponent(id)}/refund-request`, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
addNote(id: string, note: string, internal: boolean): Observable<AdminOrder | null> {
|
||||||
|
return this.http.post<AdminOrder | null>(`/api/admin/v2/orders/${encodeURIComponent(id)}/notes`, { note, internal });
|
||||||
|
}
|
||||||
|
|
||||||
|
archiveOrder(id: string): Observable<AdminOrder | null> {
|
||||||
|
return this.http.post<AdminOrder | null>(`/api/admin/v2/orders/${encodeURIComponent(id)}/archive`, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
restoreOrder(id: string): Observable<AdminOrder | null> {
|
||||||
|
return this.http.post<AdminOrder | null>(`/api/admin/v2/orders/${encodeURIComponent(id)}/restore`, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteOrder(id: string): Observable<void> {
|
||||||
|
return this.http.delete(`/api/admin/v2/orders/${encodeURIComponent(id)}`).pipe(map(() => undefined));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,11 @@
|
|||||||
import { InjectionToken, inject } from '@angular/core';
|
import { InjectionToken, inject } from '@angular/core';
|
||||||
|
import { environment } from '../../../../../environments/environment';
|
||||||
import { AdminOrdersGateway } from './admin-orders-gateway.interface';
|
import { AdminOrdersGateway } from './admin-orders-gateway.interface';
|
||||||
import { AdminOrdersLocalGateway } from './admin-orders-local.gateway';
|
import { AdminOrdersLocalGateway } from './admin-orders-local.gateway';
|
||||||
|
import { AdminOrdersApiGateway } from './admin-orders-api.gateway';
|
||||||
|
|
||||||
/** Swap point for a real Orders backend - see BACKEND-API-REFERENCE.md §8 (no seam existed before this token). */
|
/** Swap point for docs/backend/PHASE-2-ORDERS-NOTIFICATIONS-CONTRACT.md. */
|
||||||
export const ADMIN_ORDERS_GATEWAY = new InjectionToken<AdminOrdersGateway>('ADMIN_ORDERS_GATEWAY', {
|
export const ADMIN_ORDERS_GATEWAY = new InjectionToken<AdminOrdersGateway>('ADMIN_ORDERS_GATEWAY', {
|
||||||
providedIn: 'root',
|
providedIn: 'root',
|
||||||
factory: () => inject(AdminOrdersLocalGateway),
|
factory: () => (environment.useMockData ? inject(AdminOrdersLocalGateway) : inject(AdminOrdersApiGateway)),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
import { HttpClient, HttpParams } from '@angular/common/http';
|
||||||
|
import { Injectable, inject } from '@angular/core';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { map } from 'rxjs/operators';
|
||||||
|
import { AdminProduct, AdminProductCategoryOption, AdminProductListFilters, AdminProductsListResult } from '../models/admin-product.model';
|
||||||
|
import { AdminProductsGateway } from './admin-products-gateway.interface';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Contract: docs/backend/PHASE-3-CATALOG-OFFER-FULFILLMENT-CONTRACT.md §7
|
||||||
|
* (list/get/create/update on /api/admin/v2/products). duplicate/archive/
|
||||||
|
* restore aren't individually specified - inferred as sub-resource actions,
|
||||||
|
* consistent with the offers/{id}/publish pattern in the same contract.
|
||||||
|
*/
|
||||||
|
@Injectable({ providedIn: 'root' })
|
||||||
|
export class AdminProductsApiGateway implements AdminProductsGateway {
|
||||||
|
private readonly http = inject(HttpClient);
|
||||||
|
|
||||||
|
loadProducts(filters: AdminProductListFilters): Observable<AdminProductsListResult> {
|
||||||
|
let params = new HttpParams();
|
||||||
|
for (const [key, value] of Object.entries(filters ?? {})) {
|
||||||
|
if (value !== undefined && value !== null && value !== '') {
|
||||||
|
params = params.set(key, String(value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return this.http.get<AdminProductsListResult>('/api/admin/v2/products', { params });
|
||||||
|
}
|
||||||
|
|
||||||
|
loadProduct(id: string): Observable<AdminProduct | null> {
|
||||||
|
return this.http.get<AdminProduct | null>(`/api/admin/v2/products/${encodeURIComponent(id)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
loadCategories(): Observable<AdminProductCategoryOption[]> {
|
||||||
|
return this.http.get<AdminProductCategoryOption[]>('/api/admin/v2/products/categories');
|
||||||
|
}
|
||||||
|
|
||||||
|
createProduct(product: AdminProduct): Observable<AdminProduct> {
|
||||||
|
return this.http.post<AdminProduct>('/api/admin/v2/products', product);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateProduct(product: AdminProduct): Observable<AdminProduct> {
|
||||||
|
return this.http.patch<AdminProduct>(`/api/admin/v2/products/${encodeURIComponent(product.id)}`, product);
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteProduct(id: string): Observable<void> {
|
||||||
|
return this.http.delete(`/api/admin/v2/products/${encodeURIComponent(id)}`).pipe(map(() => undefined));
|
||||||
|
}
|
||||||
|
|
||||||
|
duplicateProduct(id: string): Observable<AdminProduct | null> {
|
||||||
|
return this.http.post<AdminProduct | null>(`/api/admin/v2/products/${encodeURIComponent(id)}/duplicate`, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
archiveProduct(id: string): Observable<void> {
|
||||||
|
return this.http
|
||||||
|
.post(`/api/admin/v2/products/${encodeURIComponent(id)}/archive`, {})
|
||||||
|
.pipe(map(() => undefined));
|
||||||
|
}
|
||||||
|
|
||||||
|
restoreProduct(id: string): Observable<AdminProduct | null> {
|
||||||
|
return this.http.post<AdminProduct | null>(`/api/admin/v2/products/${encodeURIComponent(id)}/restore`, {});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,11 @@
|
|||||||
import { InjectionToken, inject } from '@angular/core';
|
import { InjectionToken, inject } from '@angular/core';
|
||||||
|
import { environment } from '../../../../../environments/environment';
|
||||||
import { AdminProductsGateway } from './admin-products-gateway.interface';
|
import { AdminProductsGateway } from './admin-products-gateway.interface';
|
||||||
import { AdminProductsLocalGateway } from './admin-products-local.gateway';
|
import { AdminProductsLocalGateway } from './admin-products-local.gateway';
|
||||||
|
import { AdminProductsApiGateway } from './admin-products-api.gateway';
|
||||||
|
|
||||||
/** Swap point for a real Products backend - see BACKEND-API-REFERENCE.md §8 (no seam existed before this token). */
|
/** Swap point for docs/backend/PHASE-3-CATALOG-OFFER-FULFILLMENT-CONTRACT.md §7. */
|
||||||
export const ADMIN_PRODUCTS_GATEWAY = new InjectionToken<AdminProductsGateway>('ADMIN_PRODUCTS_GATEWAY', {
|
export const ADMIN_PRODUCTS_GATEWAY = new InjectionToken<AdminProductsGateway>('ADMIN_PRODUCTS_GATEWAY', {
|
||||||
providedIn: 'root',
|
providedIn: 'root',
|
||||||
factory: () => inject(AdminProductsLocalGateway),
|
factory: () => (environment.useMockData ? inject(AdminProductsLocalGateway) : inject(AdminProductsApiGateway)),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,7 +3,8 @@ import { signal } from '@angular/core';
|
|||||||
import { provideRouter } from '@angular/router';
|
import { provideRouter } from '@angular/router';
|
||||||
import { of } from 'rxjs';
|
import { of } from 'rxjs';
|
||||||
import { AdminOrderWatcherService } from './admin-order-watcher.service';
|
import { AdminOrderWatcherService } from './admin-order-watcher.service';
|
||||||
import { AdminOrdersLocalGateway } from '../../orders/services/admin-orders-local.gateway';
|
import { AdminOrdersGateway } from '../../orders/services/admin-orders-gateway.interface';
|
||||||
|
import { ADMIN_ORDERS_GATEWAY } from '../../orders/services/admin-orders-gateway.token';
|
||||||
import { AdminOrder, AdminOrdersListResult } from '../../orders/models/admin-order.model';
|
import { AdminOrder, AdminOrdersListResult } from '../../orders/models/admin-order.model';
|
||||||
import { UserNotificationService } from '../../../website/user-experience/services/user-notification.service';
|
import { UserNotificationService } from '../../../website/user-experience/services/user-notification.service';
|
||||||
import { AdminAuthService } from '@marketplaces/auth';
|
import { AdminAuthService } from '@marketplaces/auth';
|
||||||
@@ -65,7 +66,7 @@ describe('AdminOrderWatcherService', () => {
|
|||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
providers: [
|
providers: [
|
||||||
provideRouter([]),
|
provideRouter([]),
|
||||||
{ provide: AdminOrdersLocalGateway, useValue: fakeGateway() as unknown as AdminOrdersLocalGateway },
|
{ provide: ADMIN_ORDERS_GATEWAY, useValue: fakeGateway() as unknown as AdminOrdersGateway },
|
||||||
{ provide: AdminAuthService, useValue: fakeAdminAuth as unknown as AdminAuthService },
|
{ provide: AdminAuthService, useValue: fakeAdminAuth as unknown as AdminAuthService },
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
@@ -192,7 +193,7 @@ describe('AdminOrderWatcherService', () => {
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
it('stop() clears the interval so no further polls occur', fakeAsync(() => {
|
it('stop() clears the interval so no further polls occur', fakeAsync(() => {
|
||||||
const gateway = TestBed.inject(AdminOrdersLocalGateway);
|
const gateway = TestBed.inject(ADMIN_ORDERS_GATEWAY);
|
||||||
const loadOrdersSpy = spyOn(gateway, 'loadOrders').and.callThrough();
|
const loadOrdersSpy = spyOn(gateway, 'loadOrders').and.callThrough();
|
||||||
|
|
||||||
service.start();
|
service.start();
|
||||||
@@ -207,7 +208,7 @@ describe('AdminOrderWatcherService', () => {
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
it('reacts to AdminAuthService.isAuthenticated: starts, stops on logout, and resumes on re-login', fakeAsync(() => {
|
it('reacts to AdminAuthService.isAuthenticated: starts, stops on logout, and resumes on re-login', fakeAsync(() => {
|
||||||
const gateway = TestBed.inject(AdminOrdersLocalGateway);
|
const gateway = TestBed.inject(ADMIN_ORDERS_GATEWAY);
|
||||||
const loadOrdersSpy = spyOn(gateway, 'loadOrders').and.callThrough();
|
const loadOrdersSpy = spyOn(gateway, 'loadOrders').and.callThrough();
|
||||||
|
|
||||||
// Starts authenticated -> the effect should start polling on its own,
|
// Starts authenticated -> the effect should start polling on its own,
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import { HttpClient, HttpParams } from '@angular/common/http';
|
||||||
|
import { Injectable, inject } from '@angular/core';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { AdminTransaction, AdminTransactionListFilters, AdminTransactionsListResult } from '../models/admin-transaction.model';
|
||||||
|
import { AdminTransactionsGateway } from './admin-transactions-gateway.interface';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* No dedicated transactions contract doc exists yet - this reads as the
|
||||||
|
* admin-facing view of Payment/PaymentEvent from
|
||||||
|
* docs/backend/PHASE-1-MONEY-FX-PAYMENTS-CONTRACT.md §6, at the
|
||||||
|
* conventional /api/admin/v2/{resource} path used throughout that contract set.
|
||||||
|
*/
|
||||||
|
@Injectable({ providedIn: 'root' })
|
||||||
|
export class AdminTransactionsApiGateway implements AdminTransactionsGateway {
|
||||||
|
private readonly http = inject(HttpClient);
|
||||||
|
|
||||||
|
loadTransactions(filters: AdminTransactionListFilters): Observable<AdminTransactionsListResult> {
|
||||||
|
let params = new HttpParams();
|
||||||
|
for (const [key, value] of Object.entries(filters ?? {})) {
|
||||||
|
if (value !== undefined && value !== null && value !== '') {
|
||||||
|
params = params.set(key, String(value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return this.http.get<AdminTransactionsListResult>('/api/admin/v2/transactions', { params });
|
||||||
|
}
|
||||||
|
|
||||||
|
retryFailed(id: string): Observable<AdminTransaction | null> {
|
||||||
|
return this.http.post<AdminTransaction | null>(`/api/admin/v2/transactions/${encodeURIComponent(id)}/retry`, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
setFraudFlag(id: string, flagged: boolean): Observable<AdminTransaction | null> {
|
||||||
|
return this.http.patch<AdminTransaction | null>(`/api/admin/v2/transactions/${encodeURIComponent(id)}`, { flagged });
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,11 @@
|
|||||||
import { InjectionToken, inject } from '@angular/core';
|
import { InjectionToken, inject } from '@angular/core';
|
||||||
|
import { environment } from '../../../../../environments/environment';
|
||||||
import { AdminTransactionsGateway } from './admin-transactions-gateway.interface';
|
import { AdminTransactionsGateway } from './admin-transactions-gateway.interface';
|
||||||
import { AdminTransactionsLocalGateway } from './admin-transactions-local.gateway';
|
import { AdminTransactionsLocalGateway } from './admin-transactions-local.gateway';
|
||||||
|
import { AdminTransactionsApiGateway } from './admin-transactions-api.gateway';
|
||||||
|
|
||||||
/** Swap point for a real Transactions backend - see BACKEND-API-REFERENCE.md §8 (no seam existed before this token). */
|
/** Swap point for docs/backend/PHASE-1-MONEY-FX-PAYMENTS-CONTRACT.md §6. */
|
||||||
export const ADMIN_TRANSACTIONS_GATEWAY = new InjectionToken<AdminTransactionsGateway>('ADMIN_TRANSACTIONS_GATEWAY', {
|
export const ADMIN_TRANSACTIONS_GATEWAY = new InjectionToken<AdminTransactionsGateway>('ADMIN_TRANSACTIONS_GATEWAY', {
|
||||||
providedIn: 'root',
|
providedIn: 'root',
|
||||||
factory: () => inject(AdminTransactionsLocalGateway),
|
factory: () => (environment.useMockData ? inject(AdminTransactionsLocalGateway) : inject(AdminTransactionsApiGateway)),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,11 @@
|
|||||||
import { InjectionToken, inject } from '@angular/core';
|
import { InjectionToken, inject } from '@angular/core';
|
||||||
|
import { environment } from '../../../../../environments/environment';
|
||||||
import { AdminUsersGateway } from './admin-users-gateway.interface';
|
import { AdminUsersGateway } from './admin-users-gateway.interface';
|
||||||
import { AdminUsersLocalGateway } from './admin-users-local.gateway';
|
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', {
|
export const ADMIN_USERS_GATEWAY = new InjectionToken<AdminUsersGateway>('ADMIN_USERS_GATEWAY', {
|
||||||
providedIn: 'root',
|
providedIn: 'root',
|
||||||
factory: () => inject(AdminUsersLocalGateway),
|
factory: () => (environment.useMockData ? inject(AdminUsersLocalGateway) : inject(AdminUsersApiGateway)),
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user