Merge branch 'B2B'
Some checks failed
Architecture Governance / architecture (push) Has been cancelled
Deploy Frontend / deploy (push) Has been cancelled

This commit is contained in:
sdarbinyan
2026-08-18 21:38:45 +04:00
5 changed files with 761 additions and 0 deletions

View File

@@ -0,0 +1,155 @@
import { TestBed } from '@angular/core/testing';
import { of, throwError } from 'rxjs';
import { AdminModerationFacade } from './admin-moderation.facade';
import { ADMIN_MODERATION_GATEWAY } from '../services/admin-moderation-gateway.token';
import { AdminModerationGateway } from '../services/admin-moderation-gateway.interface';
import { AdminReview } from '../models/admin-review.model';
import { LocalStorageService } from '../../../../core/storage/local-storage.service';
function review(overrides: Partial<AdminReview> = {}): AdminReview {
return {
id: 'r1',
productId: 'p1',
productName: 'Widget',
customerName: 'Buyer',
customerEmail: 'b@example.com',
rating: 5,
text: 'Great',
photos: [],
status: 'pending',
visible: true,
pinned: false,
featured: false,
reportCount: 0,
moderatorNotes: '',
timeline: [],
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
...overrides,
};
}
describe('AdminModerationFacade', () => {
let facade: AdminModerationFacade;
let gateway: jasmine.SpyObj<AdminModerationGateway>;
beforeEach(() => {
gateway = jasmine.createSpyObj<AdminModerationGateway>('AdminModerationGateway', [
'loadReviews', 'loadReview', 'setReviewStatus', 'setReviewVisible', 'setReviewPinned',
'setReviewFeatured', 'addModeratorNote', 'deleteReview', 'loadReports', 'setReportStatus',
]);
TestBed.configureTestingModule({
providers: [
{ provide: ADMIN_MODERATION_GATEWAY, useValue: gateway },
{ provide: LocalStorageService, useValue: { getItem: () => null, setItem: () => {}, getJSON: () => null, setJSON: () => {} } },
],
});
facade = TestBed.inject(AdminModerationFacade);
});
describe('health', () => {
it('scores 100% when every check passes', () => {
const result = facade.health(review({ rating: 5, text: 'x', photos: ['a'], status: 'approved', reportCount: 0, visible: true }));
expect(result.completionPercent).toBe(100);
});
it('scores 0% when every check fails', () => {
const result = facade.health(review({ rating: 0, text: '', photos: [], status: 'pending', reportCount: 1, visible: false }));
expect(result.completionPercent).toBe(0);
});
it('treats whitespace-only text as no text', () => {
const result = facade.health(review({ text: ' ' }));
expect(result.hasText).toBeFalse();
});
it('any status other than pending counts as moderated', () => {
expect(facade.health(review({ status: 'approved' })).isModerated).toBeTrue();
expect(facade.health(review({ status: 'rejected' })).isModerated).toBeTrue();
expect(facade.health(review({ status: 'pending' })).isModerated).toBeFalse();
});
});
describe('loadDashboardStats', () => {
it('computes averageRating only from reviews that have a rating', () => {
gateway.loadReviews.and.returnValue(of({
items: [review({ rating: 4 }), review({ rating: 0 }), review({ rating: 5 })],
total: 3, page: 1, pageSize: 100000,
}));
facade.loadDashboardStats();
// (4 + 5) / 2 = 4.5 - the zero-rating review must not drag the average down.
expect(facade.dashboardStats()?.averageRating).toBe(4.5);
});
it('averageRating is null when no review has a rating, not 0', () => {
gateway.loadReviews.and.returnValue(of({
items: [review({ rating: 0 })],
total: 1, page: 1, pageSize: 100000,
}));
facade.loadDashboardStats();
expect(facade.dashboardStats()?.averageRating).toBeNull();
});
it('moderationHealthPercent is 100 for an empty queue, not a divide-by-zero artifact', () => {
gateway.loadReviews.and.returnValue(of({ items: [], total: 0, page: 1, pageSize: 100000 }));
facade.loadDashboardStats();
expect(facade.dashboardStats()?.moderationHealthPercent).toBe(100);
});
it('counts reported reviews independently of their moderation status', () => {
gateway.loadReviews.and.returnValue(of({
items: [review({ status: 'approved', reportCount: 2 }), review({ status: 'pending', reportCount: 0 })],
total: 2, page: 1, pageSize: 100000,
}));
facade.loadDashboardStats();
expect(facade.dashboardStats()?.reported).toBe(1);
});
});
describe('setReportStatus', () => {
it('reloads the reports list after a successful status change', () => {
gateway.setReportStatus.and.returnValue(of(null));
gateway.loadReports.and.returnValue(of([]));
facade.setReportStatus('rep1', 'resolved');
expect(gateway.loadReports).toHaveBeenCalled();
});
});
describe('loadReports error handling', () => {
it('clears reports and sets reportsError on failure', () => {
gateway.loadReports.and.returnValue(throwError(() => new Error('x')));
facade.loadReports();
expect(facade.reports()).toEqual([]);
expect(facade.reportsError()).toBeTrue();
expect(facade.reportsLoading()).toBeFalse();
});
});
describe('selection', () => {
it('toggleAll selects every loaded review id', () => {
gateway.loadReviews.and.returnValue(of({ items: [review({ id: 'a' }), review({ id: 'b' })], total: 2, page: 1, pageSize: 10 }));
facade.loadList();
facade.toggleAll(true);
expect(facade.selectedIds()).toEqual(['a', 'b']);
});
});
});

View File

@@ -0,0 +1,54 @@
import { TestBed } from '@angular/core/testing';
import { of, throwError } from 'rxjs';
import { AdminMonitoringFacade } from './admin-monitoring.facade';
import { ADMIN_MONITORING_GATEWAY } from '../services/admin-monitoring-gateway.token';
import { AdminMonitoringGateway } from '../services/admin-monitoring-gateway.interface';
describe('AdminMonitoringFacade', () => {
let facade: AdminMonitoringFacade;
let gateway: jasmine.SpyObj<AdminMonitoringGateway>;
beforeEach(() => {
gateway = jasmine.createSpyObj<AdminMonitoringGateway>('AdminMonitoringGateway', [
'loadEvents', 'loadQueues', 'loadWebhooks',
]);
TestBed.configureTestingModule({ providers: [{ provide: ADMIN_MONITORING_GATEWAY, useValue: gateway }] });
facade = TestBed.inject(AdminMonitoringFacade);
});
it('loadAll populates events, queues, and webhooks independently', () => {
gateway.loadEvents.and.returnValue(of([{ id: 'e1' } as any]));
gateway.loadQueues.and.returnValue(of([{ id: 'q1' } as any]));
gateway.loadWebhooks.and.returnValue(of([{ id: 'w1' } as any]));
facade.loadAll();
expect(facade.events().length).toBe(1);
expect(facade.queues().length).toBe(1);
expect(facade.webhooks().length).toBe(1);
});
it('an events failure clears events and sets error, without blocking queues/webhooks from loading', () => {
gateway.loadEvents.and.returnValue(throwError(() => new Error('x')));
gateway.loadQueues.and.returnValue(of([{ id: 'q1' } as any]));
gateway.loadWebhooks.and.returnValue(of([]));
facade.loadAll();
expect(facade.events()).toEqual([]);
expect(facade.error()).toBeTrue();
expect(facade.queues().length).toBe(1);
});
it('updateFilters merges the patch and refetches events using the merged filters', () => {
gateway.loadEvents.and.returnValue(of([]));
gateway.loadQueues.and.returnValue(of([]));
gateway.loadWebhooks.and.returnValue(of([]));
facade.loadAll();
facade.updateFilters({ search: 'checkout' });
expect(facade.filters()).toEqual({ category: 'all', search: 'checkout' });
expect(gateway.loadEvents).toHaveBeenCalledWith({ category: 'all', search: 'checkout' });
});
});

View File

@@ -0,0 +1,237 @@
import { TestBed } from '@angular/core/testing';
import { of, throwError } from 'rxjs';
import { AdminOrdersFacade } from './admin-orders.facade';
import { ADMIN_ORDERS_GATEWAY } from '../services/admin-orders-gateway.token';
import { AdminOrdersGateway } from '../services/admin-orders-gateway.interface';
import { AdminOrder } from '../models/admin-order.model';
import { LocalStorageService } from '../../../../core/storage/local-storage.service';
function order(overrides: Partial<AdminOrder> = {}): AdminOrder {
return {
id: 'o1',
orderNumber: '1001',
status: 'pending',
customer: { name: 'Buyer', email: 'b@example.com', phone: '' },
payment: { method: 'card', status: 'paid', amount: 1000, currency: 'RUB' },
shipping: { address: '', method: '', trackingNumber: '' },
items: [],
total: 1000,
currency: 'RUB',
notes: '',
internalNotes: '',
timeline: [],
archived: false,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
...overrides,
};
}
describe('AdminOrdersFacade', () => {
let facade: AdminOrdersFacade;
let gateway: jasmine.SpyObj<AdminOrdersGateway>;
beforeEach(() => {
gateway = jasmine.createSpyObj<AdminOrdersGateway>('AdminOrdersGateway', [
'loadOrders', 'loadOrder', 'updateStatus', 'requestRefund', 'addNote',
'archiveOrder', 'restoreOrder', 'deleteOrder',
]);
TestBed.configureTestingModule({
providers: [
{ provide: ADMIN_ORDERS_GATEWAY, useValue: gateway },
{ provide: LocalStorageService, useValue: { getItem: () => null, setItem: () => {}, getJSON: () => null, setJSON: () => {} } },
],
});
facade = TestBed.inject(AdminOrdersFacade);
});
describe('loadList', () => {
it('populates orders and total on success', () => {
gateway.loadOrders.and.returnValue(of({ items: [order()], total: 1, page: 1, pageSize: 10 }));
facade.loadList();
expect(facade.orders().length).toBe(1);
expect(facade.total()).toBe(1);
expect(facade.loading()).toBeFalse();
expect(facade.error()).toBeNull();
});
it('clears the list and surfaces an error on failure, rather than leaving stale data on screen', () => {
gateway.loadOrders.and.returnValue(of({ items: [order()], total: 1, page: 1, pageSize: 10 }));
facade.loadList();
gateway.loadOrders.and.returnValue(throwError(() => new Error('network')));
facade.loadList();
expect(facade.orders()).toEqual([]);
expect(facade.total()).toBe(0);
expect(facade.error()).toBe('common.errorDescription');
});
});
describe('updateFilters', () => {
it('resets to page 1 whenever a filter other than page changes', () => {
gateway.loadOrders.and.returnValue(of({ items: [], total: 0, page: 1, pageSize: 10 }));
facade.updateFilters({ page: 5 });
facade.updateFilters({ status: 'processing' });
expect(facade.filters().page).toBe(1);
expect(facade.filters().status).toBe('processing');
});
it('reloads the list on every filter change', () => {
gateway.loadOrders.and.returnValue(of({ items: [], total: 0, page: 1, pageSize: 10 }));
facade.updateFilters({ search: 'x' });
expect(gateway.loadOrders).toHaveBeenCalled();
});
});
describe('addNote', () => {
it('does not call the gateway for a whitespace-only note', () => {
facade.addNote('o1', ' ', false);
expect(gateway.addNote).not.toHaveBeenCalled();
});
it('trims the note before sending it', () => {
gateway.addNote.and.returnValue(of(order()));
facade.addNote('o1', ' hello ', true);
expect(gateway.addNote).toHaveBeenCalledWith('o1', 'hello', true);
});
});
describe('selection', () => {
it('toggleAll selects every currently loaded order id', () => {
gateway.loadOrders.and.returnValue(of({ items: [order({ id: 'a' }), order({ id: 'b' })], total: 2, page: 1, pageSize: 10 }));
facade.loadList();
facade.toggleAll(true);
expect(facade.selectedIds()).toEqual(['a', 'b']);
expect(facade.hasSelection()).toBeTrue();
});
it('toggleSelection does not duplicate an id already selected', () => {
facade.toggleSelection('a', true);
facade.toggleSelection('a', true);
expect(facade.selectedIds()).toEqual(['a']);
});
it('clearSelection empties the selection', () => {
facade.toggleSelection('a', true);
facade.clearSelection();
expect(facade.hasSelection()).toBeFalse();
});
});
describe('bulk actions', () => {
it('applyBulkStatus calls updateStatus for every selected id then clears selection', () => {
gateway.updateStatus.and.returnValue(of(order()));
gateway.loadOrders.and.returnValue(of({ items: [], total: 0, page: 1, pageSize: 10 }));
facade.toggleSelection('a', true);
facade.toggleSelection('b', true);
facade.applyBulkStatus('cancelled');
expect(gateway.updateStatus).toHaveBeenCalledWith('a', 'cancelled');
expect(gateway.updateStatus).toHaveBeenCalledWith('b', 'cancelled');
expect(facade.hasSelection()).toBeFalse();
});
it('applyBulkArchive routes to archiveOrder when archiving true, restoreOrder when false', () => {
gateway.archiveOrder.and.returnValue(of(order()));
gateway.restoreOrder.and.returnValue(of(order()));
gateway.loadOrders.and.returnValue(of({ items: [], total: 0, page: 1, pageSize: 10 }));
facade.toggleSelection('a', true);
facade.applyBulkArchive(true);
expect(gateway.archiveOrder).toHaveBeenCalledWith('a');
facade.toggleSelection('b', true);
facade.applyBulkArchive(false);
expect(gateway.restoreOrder).toHaveBeenCalledWith('b');
});
});
describe('computeDashboardStats (via loadDashboardStats)', () => {
it('counts a customer with more than one order as returning', () => {
gateway.loadOrders.and.returnValue(of({
items: [
order({ id: 'a', customer: { name: 'X', email: 'x@example.com', phone: '' } }),
order({ id: 'b', customer: { name: 'X', email: 'x@example.com', phone: '' } }),
order({ id: 'c', customer: { name: 'Y', email: 'y@example.com', phone: '' } }),
],
total: 3, page: 1, pageSize: 100000,
}));
facade.loadDashboardStats();
expect(facade.dashboardStats()?.customers).toBe(2);
expect(facade.dashboardStats()?.returningCustomers).toBe(1);
});
it('flags refund_requested payments as an alert', () => {
gateway.loadOrders.and.returnValue(of({
items: [order({ payment: { method: 'card', status: 'refund_requested', amount: 500, currency: 'RUB' } })],
total: 1, page: 1, pageSize: 100000,
}));
facade.loadDashboardStats();
expect(facade.dashboardStats()?.alerts.some(a => a.labelKey === 'adminOrders.alertRefundRequested')).toBeTrue();
});
it('flags a pending order older than 48h as stuck', () => {
const old = new Date(Date.now() - 49 * 60 * 60 * 1000).toISOString();
gateway.loadOrders.and.returnValue(of({
items: [order({ status: 'pending', createdAt: old })],
total: 1, page: 1, pageSize: 100000,
}));
facade.loadDashboardStats();
expect(facade.dashboardStats()?.alerts.some(a => a.labelKey === 'adminOrders.alertStuckPending')).toBeTrue();
});
it('does not flag a pending order under 48h old', () => {
const recent = new Date(Date.now() - 1 * 60 * 60 * 1000).toISOString();
gateway.loadOrders.and.returnValue(of({
items: [order({ status: 'pending', createdAt: recent })],
total: 1, page: 1, pageSize: 100000,
}));
facade.loadDashboardStats();
expect(facade.dashboardStats()?.alerts.some(a => a.labelKey === 'adminOrders.alertStuckPending')).toBeFalse();
});
it('averageOrder is 0 for an empty order book, not NaN or a divide-by-zero crash', () => {
gateway.loadOrders.and.returnValue(of({ items: [], total: 0, page: 1, pageSize: 100000 }));
facade.loadDashboardStats();
expect(facade.dashboardStats()?.averageOrder).toBe(0);
});
});
describe('exportCsv', () => {
it('produces one header row plus one row per loaded order', () => {
gateway.loadOrders.and.returnValue(of({ items: [order(), order({ id: 'o2', orderNumber: '1002' })], total: 2, page: 1, pageSize: 10 }));
facade.loadList();
const csv = facade.exportCsv();
expect(csv.split('\n').length).toBe(3);
});
});
});

View File

@@ -0,0 +1,226 @@
import { TestBed } from '@angular/core/testing';
import { of, throwError } from 'rxjs';
import { AdminProductsFacade } from './admin-products.facade';
import { ADMIN_PRODUCTS_GATEWAY } from '../services/admin-products-gateway.token';
import { AdminProductsGateway } from '../services/admin-products-gateway.interface';
import { AdminProductsFormFactory } from '../services/admin-products-form.factory';
import { ProjectEditorFacade } from '../../../project-editor/facade/project-editor.facade';
import { LocalStorageService } from '../../../../core/storage/local-storage.service';
import { AdminProduct } from '../models/admin-product.model';
function product(overrides: Partial<AdminProduct> = {}): AdminProduct {
return {
id: 'p1',
name: 'Widget',
slug: 'widget',
sku: 'W-1',
barcode: '',
brand: '',
categoryId: 'c1',
visible: true,
archived: false,
priority: 0,
media: { images: ['a.jpg'], gallery: [], videos: [] },
price: 1000,
discount: 0,
currency: 'RUB',
quantity: 10,
stockStatus: 'in_stock',
availability: '',
shortDescription: 'A widget',
htmlDescription: '',
specifications: [],
attributes: [],
variantAttributes: [],
visits: 0,
variants: [],
relatedProductIds: [],
translations: {},
seo: { metaTitle: 'Widget', metaDescription: 'A fine widget', keywords: '' },
featured: false,
recommended: false,
isNew: false,
bestseller: false,
badges: [],
reviews: [],
questions: [],
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
...overrides,
} as AdminProduct;
}
describe('AdminProductsFacade', () => {
let facade: AdminProductsFacade;
let gateway: jasmine.SpyObj<AdminProductsGateway>;
beforeEach(() => {
gateway = jasmine.createSpyObj<AdminProductsGateway>('AdminProductsGateway', [
'loadProducts', 'loadProduct', 'loadCategories', 'createProduct', 'updateProduct',
'deleteProduct', 'duplicateProduct', 'archiveProduct', 'restoreProduct',
]);
TestBed.configureTestingModule({
providers: [
{ provide: ADMIN_PRODUCTS_GATEWAY, useValue: gateway },
{ provide: AdminProductsFormFactory, useValue: { createEmpty: () => product({ id: '' }) } },
{ provide: ProjectEditorFacade, useValue: { bootstrap: () => null, loadBootstrap: () => {} } },
{ provide: LocalStorageService, useValue: { getItem: () => null, setItem: () => {}, getJSON: () => null, setJSON: () => {} } },
],
});
facade = TestBed.inject(AdminProductsFacade);
});
describe('health', () => {
it('scores 100% when every check passes', () => {
const result = facade.health(product());
expect(result.completionPercent).toBe(100);
});
it('flags a missing SEO description even when the title is present', () => {
const result = facade.health(product({ seo: { metaTitle: 'x', metaDescription: ' ', keywords: '' } }));
expect(result.hasSeo).toBeFalse();
});
it('counts either images or gallery as satisfying hasImages', () => {
expect(facade.health(product({ media: { images: [], gallery: ['g.jpg'], videos: [] } })).hasImages).toBeTrue();
expect(facade.health(product({ media: { images: [], gallery: [], videos: [] } })).hasImages).toBeFalse();
});
it('treats a zero price as missing, not a valid free product', () => {
expect(facade.health(product({ price: 0 })).hasPrice).toBeFalse();
});
});
describe('computeDashboardStats (via loadDashboardStats)', () => {
it('recommends adding images before anything else, when any product lacks them', () => {
gateway.loadProducts.and.returnValue(of({
items: [product({ id: 'a', media: { images: [], gallery: [], videos: [] } })],
total: 1, page: 1, pageSize: 100000,
}));
facade.loadDashboardStats();
expect(facade.dashboardStats()?.recommendation.labelKey).toBe('adminProducts.recommendAddImages');
expect(facade.dashboardStats()?.recommendation.productId).toBe('a');
});
it('falls through to restock recommendation once images and SEO are fine', () => {
gateway.loadProducts.and.returnValue(of({
items: [product({ id: 'a', stockStatus: 'out_of_stock' })],
total: 1, page: 1, pageSize: 100000,
}));
facade.loadDashboardStats();
expect(facade.dashboardStats()?.recommendation.labelKey).toBe('adminProducts.recommendRestock');
});
it('recommends nothing when the catalog has no issues', () => {
gateway.loadProducts.and.returnValue(of({ items: [product()], total: 1, page: 1, pageSize: 100000 }));
facade.loadDashboardStats();
expect(facade.dashboardStats()?.recommendation.labelKey).toBe('adminProducts.recommendNone');
});
it('counts published as visible and not archived, distinct from drafts', () => {
gateway.loadProducts.and.returnValue(of({
items: [
product({ id: 'a', visible: true, archived: false }),
product({ id: 'b', visible: false, archived: false }),
product({ id: 'c', visible: true, archived: true }),
],
total: 3, page: 1, pageSize: 100000,
}));
facade.loadDashboardStats();
expect(facade.dashboardStats()?.published).toBe(1);
expect(facade.dashboardStats()?.drafts).toBe(1);
});
});
describe('saveDraft', () => {
it('calls createProduct in create mode', () => {
gateway.createProduct.and.returnValue(of(product()));
gateway.loadProducts.and.returnValue(of({ items: [], total: 0, page: 1, pageSize: 10 }));
facade.startCreate();
facade.saveDraft();
expect(gateway.createProduct).toHaveBeenCalled();
expect(gateway.updateProduct).not.toHaveBeenCalled();
});
it('calls updateProduct in edit mode', () => {
gateway.loadProduct.and.returnValue(of(product()));
gateway.updateProduct.and.returnValue(of(product()));
gateway.loadProducts.and.returnValue(of({ items: [], total: 0, page: 1, pageSize: 10 }));
facade.loadForEdit('p1', 'edit');
facade.saveDraft();
expect(gateway.updateProduct).toHaveBeenCalled();
expect(gateway.createProduct).not.toHaveBeenCalled();
});
it('does nothing when there is no draft loaded', () => {
facade.saveDraft();
expect(gateway.createProduct).not.toHaveBeenCalled();
expect(gateway.updateProduct).not.toHaveBeenCalled();
});
it('sets mutationError on failure rather than silently discarding the draft', () => {
gateway.createProduct.and.returnValue(throwError(() => new Error('x')));
facade.startCreate();
facade.saveDraft();
expect(facade.mutationError()).toBe('common.errorDescription');
});
it('calls onSuccess only after the save actually succeeds', () => {
gateway.createProduct.and.returnValue(of(product()));
gateway.loadProducts.and.returnValue(of({ items: [], total: 0, page: 1, pageSize: 10 }));
facade.startCreate();
const onSuccess = jasmine.createSpy('onSuccess');
facade.saveDraft(onSuccess);
expect(onSuccess).toHaveBeenCalled();
});
});
describe('updateDraft', () => {
it('marks the facade dirty and applies the patch', () => {
gateway.loadProducts.and.returnValue(of({ items: [], total: 0, page: 1, pageSize: 10 }));
facade.startCreate();
facade.updateDraft({ name: 'Renamed' });
expect(facade.dirty()).toBeTrue();
expect(facade.draft()?.name).toBe('Renamed');
});
it('stamps updatedAt with a valid ISO timestamp', () => {
facade.startCreate();
facade.updateDraft({ name: 'Renamed' });
expect(new Date(facade.draft()!.updatedAt).toString()).not.toBe('Invalid Date');
});
});
describe('loadList error handling', () => {
it('clears the list and does not throw when the gateway errors', () => {
gateway.loadProducts.and.returnValue(throwError(() => new Error('x')));
expect(() => facade.loadList()).not.toThrow();
});
});
});

View File

@@ -0,0 +1,89 @@
import { TestBed } from '@angular/core/testing';
import { of, throwError } from 'rxjs';
import { AdminTransactionsFacade } from './admin-transactions.facade';
import { ADMIN_TRANSACTIONS_GATEWAY } from '../services/admin-transactions-gateway.token';
import { AdminTransactionsGateway } from '../services/admin-transactions-gateway.interface';
import { AdminTransaction } from '../models/admin-transaction.model';
function transaction(overrides: Partial<AdminTransaction> = {}): AdminTransaction {
return {
id: 't1',
orderId: 'o1',
orderNumber: '1001',
type: 'payment',
method: 'card',
status: 'success',
amount: 1000,
currency: 'RUB',
fraudFlag: false,
audit: [],
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
...overrides,
};
}
describe('AdminTransactionsFacade', () => {
let facade: AdminTransactionsFacade;
let gateway: jasmine.SpyObj<AdminTransactionsGateway>;
beforeEach(() => {
gateway = jasmine.createSpyObj<AdminTransactionsGateway>('AdminTransactionsGateway', [
'loadTransactions', 'retryFailed', 'setFraudFlag',
]);
TestBed.configureTestingModule({ providers: [{ provide: ADMIN_TRANSACTIONS_GATEWAY, useValue: gateway }] });
facade = TestBed.inject(AdminTransactionsFacade);
});
it('loads transactions and total on success', () => {
gateway.loadTransactions.and.returnValue(of({ items: [transaction()], total: 1, page: 1, pageSize: 10 }));
facade.loadList();
expect(facade.transactions().length).toBe(1);
expect(facade.total()).toBe(1);
});
it('clears the list and sets an error on failure', () => {
gateway.loadTransactions.and.returnValue(throwError(() => new Error('x')));
facade.loadList();
expect(facade.transactions()).toEqual([]);
expect(facade.error()).toBe('common.errorDescription');
});
it('updateFilters resets page to 1 for a non-page change', () => {
gateway.loadTransactions.and.returnValue(of({ items: [], total: 0, page: 1, pageSize: 10 }));
facade.updateFilters({ page: 3 });
facade.updateFilters({ search: 'x' });
expect(facade.filters().page).toBe(1);
});
it('retryFailed reloads the list after the gateway call resolves', () => {
gateway.retryFailed.and.returnValue(of(transaction()));
gateway.loadTransactions.and.returnValue(of({ items: [], total: 0, page: 1, pageSize: 10 }));
facade.retryFailed('t1');
expect(gateway.loadTransactions).toHaveBeenCalled();
});
it('toggleFraudFlag sends the requested flag value, not its inverse', () => {
gateway.setFraudFlag.and.returnValue(of(transaction()));
gateway.loadTransactions.and.returnValue(of({ items: [], total: 0, page: 1, pageSize: 10 }));
facade.toggleFraudFlag('t1', true);
expect(gateway.setFraudFlag).toHaveBeenCalledWith('t1', true);
});
it('exportCsv emits one row per transaction plus a header', () => {
gateway.loadTransactions.and.returnValue(of({ items: [transaction(), transaction({ id: 't2' })], total: 2, page: 1, pageSize: 10 }));
facade.loadList();
expect(facade.exportCsv().split('\n').length).toBe(3);
});
});