feat: introduce loading context management across services
- Refactor AuthService, CartService, CatalogService, CheckoutService, TenantService, and TicketService to extend BaseApiService for consistent loading context handling.
- Implement loading modes ('global', 'custom', 'none') using HttpContext for API requests.
- Update existing service methods to accept loading mode parameters and propagate them to HTTP requests.
- Enhance global loading interceptor to respect loading modes and prevent global loading for specific requests.
- Add unit tests to verify loading mode propagation in services and interceptors.
- Update component interactions to utilize the new loading mode functionality.
This commit is contained in:
@@ -29,7 +29,9 @@ export class PurchaseList implements OnInit {
|
||||
try {
|
||||
const tenantCode = this.tenantService.tenant()?.codigo || '';
|
||||
|
||||
const response = await this.checkoutService.getPurchases(tenantCode, 'paid');
|
||||
const response = await this.checkoutService
|
||||
.withCustomLoading()
|
||||
.getPurchases(tenantCode, 'paid');
|
||||
const mappedPurchases = response.data.map((purchase: PurchaseSummaryResponse) => ({
|
||||
id: purchase.id,
|
||||
date: this.formatDate(purchase.created_at),
|
||||
|
||||
@@ -44,7 +44,9 @@ export class PurchaseDetailPage implements OnInit {
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await this.checkoutService.getPurchase(tenant.codigo, purchaseId);
|
||||
const response = await this.checkoutService
|
||||
.withCustomLoading()
|
||||
.getPurchase(tenant.codigo, purchaseId);
|
||||
|
||||
this.purchase.set({
|
||||
id: response.id,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { inject, Injectable } from '@angular/core';
|
||||
import { firstValueFrom } from 'rxjs';
|
||||
|
||||
import { BaseApiService } from '../../../../../../core/services/base-api.service';
|
||||
import { TenantService } from '../../../../../../core/services/tenant.service';
|
||||
|
||||
export interface TicketResponse {
|
||||
@@ -21,13 +21,14 @@ export interface TicketResponse {
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class TicketService {
|
||||
private readonly http = inject(HttpClient);
|
||||
export class TicketService extends BaseApiService {
|
||||
private readonly tenantService = inject(TenantService);
|
||||
|
||||
getTickets(): Promise<TicketResponse[]> {
|
||||
return firstValueFrom(
|
||||
this.http.get<{ data: TicketResponse[] }>(`${this.tenantService.getTenantApiUrl()}/tickets`),
|
||||
this.http.get<{ data: TicketResponse[] }>(
|
||||
`${this.tenantService.getTenantApiUrl()}/tickets`,
|
||||
),
|
||||
).then((response) => response.data ?? []);
|
||||
}
|
||||
|
||||
@@ -36,7 +37,9 @@ export class TicketService {
|
||||
this.http.post(
|
||||
`${this.tenantService.getTenantApiUrl()}/tickets/pdf`,
|
||||
{ ticket_ids: ticketIds },
|
||||
{ responseType: 'blob' },
|
||||
{
|
||||
responseType: 'blob',
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -8,6 +8,14 @@ import { TicketComponent } from './components/ticket/ticket.component';
|
||||
import { TicketResponse, TicketService } from './ticket.service';
|
||||
import { TicketsPage } from './tickets-page';
|
||||
|
||||
function withCustomLoading<T extends object>(
|
||||
service: T,
|
||||
): T & { withCustomLoading: () => T } {
|
||||
return Object.assign(service, {
|
||||
withCustomLoading: () => service,
|
||||
});
|
||||
}
|
||||
|
||||
const ticket = (overrides: Partial<TicketResponse>): TicketResponse => ({
|
||||
id: 1,
|
||||
tenant_code: 'tenant',
|
||||
@@ -40,10 +48,10 @@ describe('TicketsPage', () => {
|
||||
providers: [
|
||||
{
|
||||
provide: TicketService,
|
||||
useValue: {
|
||||
useValue: withCustomLoading({
|
||||
getTickets: () => Promise.resolve([ticket({})]),
|
||||
downloadPdf: () => Promise.resolve(new Blob(['pdf-content'])),
|
||||
},
|
||||
}),
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -99,10 +107,10 @@ describe('TicketsPage', () => {
|
||||
providers: [
|
||||
{
|
||||
provide: TicketService,
|
||||
useValue: {
|
||||
useValue: withCustomLoading({
|
||||
getTickets: () => Promise.resolve([ticket({})]),
|
||||
downloadPdf,
|
||||
},
|
||||
}),
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -150,7 +158,10 @@ describe('TicketsPage', () => {
|
||||
.overrideComponent(TicketsPage, {
|
||||
set: {
|
||||
providers: [
|
||||
{ provide: TicketService, useValue: { getTickets: () => Promise.resolve([]) } },
|
||||
{
|
||||
provide: TicketService,
|
||||
useValue: withCustomLoading({ getTickets: () => Promise.resolve([]) }),
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
@@ -183,7 +194,10 @@ describe('TicketsPage', () => {
|
||||
.overrideComponent(TicketsPage, {
|
||||
set: {
|
||||
providers: [
|
||||
{ provide: TicketService, useValue: { getTickets: () => Promise.resolve(tickets) } },
|
||||
{
|
||||
provide: TicketService,
|
||||
useValue: withCustomLoading({ getTickets: () => Promise.resolve(tickets) }),
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
@@ -236,7 +250,9 @@ describe('TicketsPage', () => {
|
||||
providers: [
|
||||
{
|
||||
provide: TicketService,
|
||||
useValue: { getTickets: () => Promise.resolve([ticket({})]) },
|
||||
useValue: withCustomLoading({
|
||||
getTickets: () => Promise.resolve([ticket({})]),
|
||||
}),
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -35,7 +35,7 @@ export class TicketsPage implements OnInit {
|
||||
|
||||
async ngOnInit(): Promise<void> {
|
||||
try {
|
||||
this.tickets.set(await this.ticketService.getTickets());
|
||||
this.tickets.set(await this.ticketService.withCustomLoading().getTickets());
|
||||
} catch {
|
||||
this.toastService.danger('Hubo un error al cargar los tickets');
|
||||
} finally {
|
||||
@@ -151,7 +151,9 @@ export class TicketsPage implements OnInit {
|
||||
this.isGeneratingPdf.set(true);
|
||||
|
||||
try {
|
||||
return await this.ticketService.downloadPdf(tickets.map((ticket) => ticket.id));
|
||||
return await this.ticketService
|
||||
.withCustomLoading()
|
||||
.downloadPdf(tickets.map((ticket) => ticket.id));
|
||||
} catch {
|
||||
this.toastService.danger('No se pudo generar el PDF de los tickets.');
|
||||
return null;
|
||||
|
||||
@@ -69,7 +69,12 @@ describe('CategoryItemsPageComponent', () => {
|
||||
},
|
||||
{
|
||||
provide: CatalogService,
|
||||
useValue: { getCategoryItems },
|
||||
useValue: {
|
||||
getCategoryItems,
|
||||
withCustomLoading() {
|
||||
return this;
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: TenantService,
|
||||
|
||||
@@ -80,7 +80,10 @@ export class CategoryItemsPageComponent {
|
||||
return of(null);
|
||||
}
|
||||
|
||||
return this.catalogService.getCategoryItems(categoryId, { page }).pipe(
|
||||
return this.catalogService
|
||||
.withCustomLoading()
|
||||
.getCategoryItems(categoryId, { page })
|
||||
.pipe(
|
||||
catchError(() => {
|
||||
this.error.set('No pudimos cargar los productos de esta categoría.');
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ describe('CheckoutPageComponent payment validation', () => {
|
||||
generatePaymentIntent: ReturnType<typeof vi.fn>;
|
||||
getPurchase: ReturnType<typeof vi.fn>;
|
||||
submitPurchaseForReview: ReturnType<typeof vi.fn>;
|
||||
withCustomLoading: ReturnType<typeof vi.fn>;
|
||||
};
|
||||
let cartServiceStub: {
|
||||
cart: ReturnType<typeof signal>;
|
||||
@@ -60,7 +61,9 @@ describe('CheckoutPageComponent payment validation', () => {
|
||||
}),
|
||||
getPurchase: vi.fn().mockResolvedValue({ status: 'pending_payment' }),
|
||||
submitPurchaseForReview: vi.fn().mockResolvedValue({ status: 'in_review' }),
|
||||
withCustomLoading: vi.fn(),
|
||||
};
|
||||
checkoutServiceStub.withCustomLoading.mockReturnValue(checkoutServiceStub);
|
||||
cartServiceStub = {
|
||||
cart: signal({
|
||||
id: 10,
|
||||
@@ -363,7 +366,11 @@ describe('CheckoutPageComponent payment validation', () => {
|
||||
|
||||
expect(component.checkoutStepIndex()).toBe(1);
|
||||
expect(component.selectedPaymentMethod()).toBe('qr');
|
||||
expect(checkoutServiceStub.generatePaymentIntent).toHaveBeenCalledWith('tenant-test', 25, 'qr');
|
||||
expect(checkoutServiceStub.generatePaymentIntent).toHaveBeenCalledWith(
|
||||
'tenant-test',
|
||||
25,
|
||||
'qr',
|
||||
);
|
||||
expect(component.qrData()).toBe('qr-value');
|
||||
expect(component.qrPaymentStatus()).toBe('waiting');
|
||||
});
|
||||
|
||||
@@ -343,11 +343,9 @@ export class CheckoutPageComponent implements OnInit, OnDestroy {
|
||||
this.isGeneratingIntent.set(true);
|
||||
|
||||
try {
|
||||
const response = await this.checkoutService.generatePaymentIntent(
|
||||
tenant.codigo,
|
||||
purchaseId,
|
||||
method,
|
||||
);
|
||||
const response = await this.checkoutService
|
||||
.withCustomLoading()
|
||||
.generatePaymentIntent(tenant.codigo, purchaseId, method);
|
||||
|
||||
if (method === 'qr' && response.qr_data?.qr_code) {
|
||||
if (requestId !== this.paymentMethodRequestId || this.selectedPaymentMethod() !== 'qr') {
|
||||
@@ -381,12 +379,9 @@ export class CheckoutPageComponent implements OnInit, OnDestroy {
|
||||
this.transferValidationStatus.set('idle');
|
||||
this.isGeneratingIntent.set(true);
|
||||
try {
|
||||
const response = await this.checkoutService.generatePaymentIntent(
|
||||
tenant.codigo,
|
||||
purchaseId,
|
||||
'transfer',
|
||||
dni,
|
||||
);
|
||||
const response = await this.checkoutService
|
||||
.withCustomLoading()
|
||||
.generatePaymentIntent(tenant.codigo, purchaseId, 'transfer', dni);
|
||||
|
||||
if (response.transfer_data) {
|
||||
this.markPurchasePendingPayment();
|
||||
@@ -438,10 +433,9 @@ export class CheckoutPageComponent implements OnInit, OnDestroy {
|
||||
this.transferValidationStatus.set('checking');
|
||||
|
||||
try {
|
||||
const purchase = await this.checkoutService.submitPurchaseForReview(
|
||||
tenant.codigo,
|
||||
purchaseId,
|
||||
);
|
||||
const purchase = await this.checkoutService
|
||||
.withCustomLoading()
|
||||
.submitPurchaseForReview(tenant.codigo, purchaseId);
|
||||
|
||||
if (purchase.status === 'paid') {
|
||||
this.navigateToPurchaseStatus(purchaseId);
|
||||
@@ -494,7 +488,9 @@ export class CheckoutPageComponent implements OnInit, OnDestroy {
|
||||
this.isCheckingQrPayment.set(true);
|
||||
|
||||
try {
|
||||
const purchase = await this.checkoutService.getPurchase(tenant.codigo, purchaseId);
|
||||
const purchase = await this.checkoutService
|
||||
.withCustomLoading()
|
||||
.getPurchase(tenant.codigo, purchaseId);
|
||||
|
||||
if (runId !== this.qrPollingRunId) {
|
||||
return;
|
||||
@@ -575,7 +571,9 @@ export class CheckoutPageComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
try {
|
||||
const purchase = await this.checkoutService.getPurchase(tenant.codigo, purchaseId);
|
||||
const purchase = await this.checkoutService
|
||||
.withCustomLoading()
|
||||
.getPurchase(tenant.codigo, purchaseId);
|
||||
|
||||
if (
|
||||
purchase.status === 'in_review' ||
|
||||
|
||||
@@ -65,13 +65,17 @@ describe('ProductDetailPageComponent', () => {
|
||||
});
|
||||
catalogServiceStub = {
|
||||
getCatalogItem: vi.fn(),
|
||||
withCustomLoading: vi.fn(),
|
||||
};
|
||||
catalogServiceStub.withCustomLoading.mockReturnValue(catalogServiceStub);
|
||||
routerStub = {
|
||||
navigate: vi.fn(),
|
||||
};
|
||||
cartServiceStub = {
|
||||
addItem: vi.fn().mockReturnValue(of({ message: 'Producto agregado al carrito' })),
|
||||
withCustomLoading: vi.fn(),
|
||||
};
|
||||
cartServiceStub.withCustomLoading.mockReturnValue(cartServiceStub);
|
||||
toastServiceStub = {
|
||||
success: vi.fn(),
|
||||
danger: vi.fn(),
|
||||
|
||||
@@ -155,7 +155,10 @@ export class ProductDetailPageComponent implements OnInit, OnDestroy {
|
||||
this.variantLoading.set(true);
|
||||
|
||||
this.productSub?.unsubscribe();
|
||||
this.productSub = this.catalogService.getCatalogItem(productId, variantId).subscribe({
|
||||
this.productSub = this.catalogService
|
||||
.withCustomLoading()
|
||||
.getCatalogItem(productId, variantId)
|
||||
.subscribe({
|
||||
next: (prod) => {
|
||||
this.applyProduct(prod, false);
|
||||
this.variantLoading.set(false);
|
||||
@@ -246,7 +249,10 @@ export class ProductDetailPageComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
this.addingToCart.set(true);
|
||||
this.cartService.addItem(currentProduct.id, variant?.id ?? null, this.quantity()).subscribe({
|
||||
this.cartService
|
||||
.withCustomLoading()
|
||||
.addItem(currentProduct.id, variant?.id ?? null, this.quantity())
|
||||
.subscribe({
|
||||
next: (res) => {
|
||||
const msg = res.message || 'Producto agregado al carrito';
|
||||
this.toastService.success(msg);
|
||||
|
||||
@@ -66,6 +66,9 @@ describe('PurchaseStatusPageComponent', () => {
|
||||
async function render(hasGeneratedTickets: boolean) {
|
||||
const checkoutService = {
|
||||
getPurchase: vi.fn().mockResolvedValue(purchase(hasGeneratedTickets)),
|
||||
withCustomLoading() {
|
||||
return this;
|
||||
},
|
||||
};
|
||||
const router = {
|
||||
navigate: vi.fn().mockResolvedValue(true),
|
||||
|
||||
@@ -63,7 +63,9 @@ export class PurchaseStatusPageComponent implements OnInit {
|
||||
}
|
||||
|
||||
try {
|
||||
const purchase = await this.checkoutService.getPurchase(this.tenantCode, this.purchaseId);
|
||||
const purchase = await this.checkoutService
|
||||
.withCustomLoading()
|
||||
.getPurchase(this.tenantCode, this.purchaseId);
|
||||
this.status.set(this.resolveStatus(purchase));
|
||||
this.hasGeneratedTickets.set(purchase.has_generated_tickets === true);
|
||||
} catch (error) {
|
||||
|
||||
@@ -65,7 +65,12 @@ describe('SearchPageComponent', () => {
|
||||
},
|
||||
{
|
||||
provide: CatalogService,
|
||||
useValue: { searchCatalog },
|
||||
useValue: {
|
||||
searchCatalog,
|
||||
withCustomLoading() {
|
||||
return this;
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: TenantService,
|
||||
|
||||
@@ -112,7 +112,7 @@ export class SearchPageComponent {
|
||||
return of(null);
|
||||
}
|
||||
|
||||
return this.catalogService.searchCatalog({ q: query, page }).pipe(
|
||||
return this.catalogService.withCustomLoading().searchCatalog({ q: query, page }).pipe(
|
||||
catchError(() => {
|
||||
this.error.set('No pudimos realizar la búsqueda en este momento.');
|
||||
|
||||
|
||||
@@ -216,7 +216,9 @@ describe('StoreHomePageComponent', () => {
|
||||
const catalogServiceStub = {
|
||||
getCatalog: vi.fn(),
|
||||
getFeaturedGroupItems: vi.fn().mockReturnValue(of(createItemsPage(pageTwoItems, 2, 2))),
|
||||
withCustomLoading: vi.fn(),
|
||||
};
|
||||
catalogServiceStub.withCustomLoading.mockReturnValue(catalogServiceStub);
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [StoreHomePageComponent],
|
||||
@@ -232,6 +234,7 @@ describe('StoreHomePageComponent', () => {
|
||||
(element.querySelector('[data-testid="paginator-next"]') as HTMLButtonElement).click();
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(catalogServiceStub.withCustomLoading).toHaveBeenCalledOnce();
|
||||
expect(catalogServiceStub.getFeaturedGroupItems).toHaveBeenCalledWith(7, { page: 2 });
|
||||
expect(element.textContent).toContain('Mouse Gamer');
|
||||
expect(element.querySelector('[data-testid="paginator-status"]')?.textContent?.trim()).toBe(
|
||||
@@ -244,7 +247,9 @@ describe('StoreHomePageComponent', () => {
|
||||
const catalogServiceStub = {
|
||||
getCatalog: vi.fn(),
|
||||
getFeaturedGroupItems: vi.fn().mockReturnValue(nextPage.asObservable()),
|
||||
withCustomLoading: vi.fn(),
|
||||
};
|
||||
catalogServiceStub.withCustomLoading.mockReturnValue(catalogServiceStub);
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [StoreHomePageComponent],
|
||||
|
||||
@@ -105,7 +105,10 @@ export class StoreHomePageComponent implements OnInit, OnDestroy {
|
||||
this.setGroupLoading(groupId, true);
|
||||
this.groupRequestSubscriptions.get(groupId)?.unsubscribe();
|
||||
|
||||
const subscription = this.catalogService.getFeaturedGroupItems(groupId, { page }).subscribe({
|
||||
const subscription = this.catalogService
|
||||
.withCustomLoading()
|
||||
.getFeaturedGroupItems(groupId, { page })
|
||||
.subscribe({
|
||||
next: (items) => {
|
||||
this.catalog.update((groups) =>
|
||||
groups.map((candidate) =>
|
||||
@@ -182,7 +185,10 @@ export class StoreHomePageComponent implements OnInit, OnDestroy {
|
||||
this.error.set(null);
|
||||
this.catalogRequestSubscription?.unsubscribe();
|
||||
|
||||
this.catalogRequestSubscription = this.catalogService.getCatalog().subscribe({
|
||||
this.catalogRequestSubscription = this.catalogService
|
||||
.withCustomLoading()
|
||||
.getCatalog()
|
||||
.subscribe({
|
||||
next: (catalog) => this.catalog.set(catalog),
|
||||
error: () => {
|
||||
this.catalog.set([]);
|
||||
|
||||
Reference in New Issue
Block a user