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 { 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: () => 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 { 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: () => 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 { 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: () => 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 { 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: () => 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 { 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: () => 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 { 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: () => 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 { 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: () => 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 { 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: () => 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 { 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. */
|
||||
/** 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: () => 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 { 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: () => inject(SellerLocalGateway),
|
||||
factory: () => (environment.useMockData ? inject(SellerLocalGateway) : inject(SellerApiGateway)),
|
||||
});
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { of } from 'rxjs';
|
||||
import { AdminAnalyticsFacade } from './admin-analytics.facade';
|
||||
import { AdminOrdersLocalGateway } from '../../orders/services/admin-orders-local.gateway';
|
||||
import { AdminProductsLocalGateway } from '../../products/services/admin-products-local.gateway';
|
||||
import { AdminOrdersGateway } from '../../orders/services/admin-orders-gateway.interface';
|
||||
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 { 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 { AdminOrder } from '../../orders/models/admin-order.model';
|
||||
|
||||
@@ -23,17 +26,17 @@ function makeOrder(overrides: Partial<AdminOrder> = {}): AdminOrder {
|
||||
|
||||
describe('AdminAnalyticsFacade (never-fabricate-a-number contract)', () => {
|
||||
let facade: AdminAnalyticsFacade;
|
||||
let ordersGateway: jasmine.SpyObj<AdminOrdersLocalGateway>;
|
||||
let productsGateway: jasmine.SpyObj<AdminProductsLocalGateway>;
|
||||
let ordersGateway: jasmine.SpyObj<AdminOrdersGateway>;
|
||||
let productsGateway: jasmine.SpyObj<AdminProductsGateway>;
|
||||
let categoriesGateway: jasmine.SpyObj<{ loadCategories: () => unknown }>;
|
||||
let moderationGateway: jasmine.SpyObj<AdminModerationLocalGateway>;
|
||||
let moderationGateway: jasmine.SpyObj<AdminModerationGateway>;
|
||||
let dashboardFacade: jasmine.SpyObj<AdminDashboardFacade>;
|
||||
|
||||
function configure(bootstrapPresent: boolean): void {
|
||||
ordersGateway = jasmine.createSpyObj('AdminOrdersLocalGateway', ['loadOrders']);
|
||||
productsGateway = jasmine.createSpyObj('AdminProductsLocalGateway', ['loadProducts']);
|
||||
ordersGateway = jasmine.createSpyObj('ADMIN_ORDERS_GATEWAY', ['loadOrders']);
|
||||
productsGateway = jasmine.createSpyObj('ADMIN_PRODUCTS_GATEWAY', ['loadProducts']);
|
||||
categoriesGateway = jasmine.createSpyObj('ADMIN_CATEGORIES_GATEWAY', ['loadCategories']);
|
||||
moderationGateway = jasmine.createSpyObj('AdminModerationLocalGateway', ['loadReviews']);
|
||||
moderationGateway = jasmine.createSpyObj('ADMIN_MODERATION_GATEWAY', ['loadReviews']);
|
||||
dashboardFacade = jasmine.createSpyObj('AdminDashboardFacade', [
|
||||
'ensureLoaded', 'activityEntries', 'bootstrap', 'validationIssues', 'enabledWidgetsCount', 'staticPagesUnpublishedCount',
|
||||
]);
|
||||
@@ -50,10 +53,10 @@ describe('AdminAnalyticsFacade (never-fabricate-a-number contract)', () => {
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
{ provide: AdminOrdersLocalGateway, useValue: ordersGateway },
|
||||
{ provide: AdminProductsLocalGateway, useValue: productsGateway },
|
||||
{ provide: ADMIN_ORDERS_GATEWAY, useValue: ordersGateway },
|
||||
{ provide: ADMIN_PRODUCTS_GATEWAY, useValue: productsGateway },
|
||||
{ provide: ADMIN_CATEGORIES_GATEWAY, useValue: categoriesGateway },
|
||||
{ provide: AdminModerationLocalGateway, useValue: moderationGateway },
|
||||
{ provide: ADMIN_MODERATION_GATEWAY, useValue: moderationGateway },
|
||||
{ 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 { 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 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', {
|
||||
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 { 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 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', {
|
||||
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 { 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 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', {
|
||||
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 { 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 §6. */
|
||||
/** 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: () => 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 { 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 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', {
|
||||
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 { 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 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', {
|
||||
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 { of } from 'rxjs';
|
||||
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 { UserNotificationService } from '../../../website/user-experience/services/user-notification.service';
|
||||
import { AdminAuthService } from '@marketplaces/auth';
|
||||
@@ -65,7 +66,7 @@ describe('AdminOrderWatcherService', () => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
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 },
|
||||
],
|
||||
});
|
||||
@@ -192,7 +193,7 @@ describe('AdminOrderWatcherService', () => {
|
||||
}));
|
||||
|
||||
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();
|
||||
|
||||
service.start();
|
||||
@@ -207,7 +208,7 @@ describe('AdminOrderWatcherService', () => {
|
||||
}));
|
||||
|
||||
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();
|
||||
|
||||
// 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 { 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 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', {
|
||||
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 { environment } from '../../../../../environments/environment';
|
||||
import { AdminUsersGateway } from './admin-users-gateway.interface';
|
||||
import { AdminUsersLocalGateway } from './admin-users-local.gateway';
|
||||
import { AdminUsersApiGateway } from './admin-users-api.gateway';
|
||||
|
||||
/** Swap point for a real Users backend - see BACKEND-API-REFERENCE.md §8 (no seam existed before this token). */
|
||||
/** Swap point for docs/backend/TRACK-S-SECURITY-RBAC-CONTRACT.md §8. */
|
||||
export const ADMIN_USERS_GATEWAY = new InjectionToken<AdminUsersGateway>('ADMIN_USERS_GATEWAY', {
|
||||
providedIn: 'root',
|
||||
factory: () => inject(AdminUsersLocalGateway),
|
||||
factory: () => (environment.useMockData ? inject(AdminUsersLocalGateway) : inject(AdminUsersApiGateway)),
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user