Compare commits

..

3 Commits

4 changed files with 50 additions and 31 deletions

View File

@@ -201,6 +201,25 @@ export class CheckoutService {
}; };
} }
async submitPurchaseForReview(
tenantCode: string,
purchaseId: number,
): Promise<PurchaseStatusResponse> {
const response = await firstValueFrom(
this.http.post<{ data?: PurchaseStatusResponse } | PurchaseStatusResponse>(
`${environment.url}tenants/${tenantCode}/compras/${purchaseId}/review`,
{},
),
);
const purchase = this.extractResponseData<PurchaseStatusResponse>(response);
if (!purchase) {
throw new Error('Error al enviar la compra a revisi\u00f3n.');
}
return { status: purchase.status ?? null };
}
async cancelPurchase(tenantCode: string, purchaseId: number): Promise<PurchaseStatusResponse> { async cancelPurchase(tenantCode: string, purchaseId: number): Promise<PurchaseStatusResponse> {
const response = await firstValueFrom( const response = await firstValueFrom(
this.http.post<{ data?: PurchaseStatusResponse } | PurchaseStatusResponse>( this.http.post<{ data?: PurchaseStatusResponse } | PurchaseStatusResponse>(

View File

@@ -3,15 +3,14 @@
align-items: center; align-items: center;
justify-content: center; justify-content: center;
padding: 16px 28px; padding: 16px 28px;
background-color: #d9d9d9;
} }
.category-items__title { .category-items__title {
color: #313131; font-size: 24px;
font-size: 30px; font-weight: bold;
font-weight: 400;
line-height: 1.2; line-height: 1.2;
text-align: center; text-align: center;
color:var(--bs-primary)
} }
.category-items__alert-error { .category-items__alert-error {

View File

@@ -21,6 +21,7 @@ describe('CheckoutPageComponent payment validation', () => {
cancelPurchase: ReturnType<typeof vi.fn>; cancelPurchase: ReturnType<typeof vi.fn>;
generatePaymentIntent: ReturnType<typeof vi.fn>; generatePaymentIntent: ReturnType<typeof vi.fn>;
getPurchase: ReturnType<typeof vi.fn>; getPurchase: ReturnType<typeof vi.fn>;
submitPurchaseForReview: ReturnType<typeof vi.fn>;
}; };
let cartServiceStub: { let cartServiceStub: {
cart: ReturnType<typeof signal>; cart: ReturnType<typeof signal>;
@@ -58,6 +59,7 @@ describe('CheckoutPageComponent payment validation', () => {
qr_data: { qr_code: 'qr-value' }, qr_data: { qr_code: 'qr-value' },
}), }),
getPurchase: vi.fn().mockResolvedValue({ status: 'pending_payment' }), getPurchase: vi.fn().mockResolvedValue({ status: 'pending_payment' }),
submitPurchaseForReview: vi.fn().mockResolvedValue({ status: 'in_review' }),
}; };
cartServiceStub = { cartServiceStub = {
cart: signal({ cart: signal({
@@ -189,37 +191,37 @@ describe('CheckoutPageComponent payment validation', () => {
expect(checkoutServiceStub.getPurchase).not.toHaveBeenCalled(); expect(checkoutServiceStub.getPurchase).not.toHaveBeenCalled();
}); });
it('shows the verification error without redirecting while a transfer is pending', async () => { it.each(['created', 'pending_payment', 'in_review'])(
const { component } = createComponent(); 'shows the payment error when a transfer remains %s',
component.selectedPaymentMethod.set('transfer'); async (status) => {
checkoutServiceStub.submitPurchaseForReview.mockResolvedValue({ status });
const { component } = createComponent();
component.selectedPaymentMethod.set('transfer');
await component.onComplete(); await component.onComplete();
expect(checkoutServiceStub.getPurchase).toHaveBeenCalledTimes(1);
expect(component.transferValidationStatus()).toBe('pending');
expect(cartServiceStub.clearCart).not.toHaveBeenCalled();
expect(routerStub.navigate).not.toHaveBeenCalled();
await vi.advanceTimersByTimeAsync(30_000); expect(checkoutServiceStub.submitPurchaseForReview).toHaveBeenCalledWith('tenant-test', 25);
expect(checkoutServiceStub.getPurchase).toHaveBeenCalledTimes(1); expect(component.transferValidationStatus()).toBe('error');
expect(cartServiceStub.clearCart).not.toHaveBeenCalled();
expect(routerStub.navigate).not.toHaveBeenCalled();
},
);
await component.onComplete(); it('submits a paid transfer and navigates to its confirmation page', async () => {
expect(checkoutServiceStub.getPurchase).toHaveBeenCalledTimes(2); checkoutServiceStub.submitPurchaseForReview.mockResolvedValue({ status: 'paid' });
});
it('navigates after a transfer is confirmed as paid', async () => {
checkoutServiceStub.getPurchase.mockResolvedValue({ status: 'paid' });
const { component } = createComponent(); const { component } = createComponent();
component.selectedPaymentMethod.set('transfer'); component.selectedPaymentMethod.set('transfer');
await component.onComplete(); await component.onComplete();
expect(checkoutServiceStub.submitPurchaseForReview).toHaveBeenCalledWith('tenant-test', 25);
expect(cartServiceStub.clearCart).not.toHaveBeenCalled(); expect(cartServiceStub.clearCart).not.toHaveBeenCalled();
expect(routerStub.navigate).toHaveBeenCalledWith(['/checkout/status', 25]); expect(routerStub.navigate).toHaveBeenCalledWith(['/checkout/status', 25]);
}); });
it('shows a retryable state when transfer validation fails', async () => { it('shows a retryable state when transfer validation fails', async () => {
vi.spyOn(console, 'error').mockImplementation(() => undefined); vi.spyOn(console, 'error').mockImplementation(() => undefined);
checkoutServiceStub.getPurchase.mockRejectedValue(new Error('network error')); checkoutServiceStub.submitPurchaseForReview.mockRejectedValue(new Error('network error'));
const { component } = createComponent(); const { component } = createComponent();
component.selectedPaymentMethod.set('transfer'); component.selectedPaymentMethod.set('transfer');
@@ -365,7 +367,7 @@ describe('CheckoutPageComponent payment validation', () => {
expect(component.qrPaymentStatus()).toBe('waiting'); expect(component.qrPaymentStatus()).toBe('waiting');
}); });
it.each(['paid', 'cancelled', 'rejected', 'expired'])( it.each(['in_review', 'paid', 'cancelled', 'rejected', 'expired'])(
'redirects a %s purchase to its status page', 'redirects a %s purchase to its status page',
async (status) => { async (status) => {
routeQueryParamMap = convertToParamMap({ purchase: 25 }); routeQueryParamMap = convertToParamMap({ purchase: 25 });

View File

@@ -438,21 +438,19 @@ export class CheckoutPageComponent implements OnInit, OnDestroy {
this.transferValidationStatus.set('checking'); this.transferValidationStatus.set('checking');
try { try {
const purchase = await this.checkoutService.getPurchase(tenant.codigo, purchaseId); const purchase = await this.checkoutService.submitPurchaseForReview(
tenant.codigo,
purchaseId,
);
if (purchase.status === 'paid') { if (purchase.status === 'paid') {
this.handleConfirmedPayment(purchaseId); this.navigateToPurchaseStatus(purchaseId);
return; return;
} }
if (purchase.status === 'pending_payment') { this.transferValidationStatus.set('error');
this.transferValidationStatus.set('pending');
return;
}
this.transferValidationStatus.set('pending');
} catch (error) { } catch (error) {
console.error('Failed to validate transfer payment:', error); console.error('Failed to submit purchase for review:', error);
this.transferValidationStatus.set('error'); this.transferValidationStatus.set('error');
} }
} }
@@ -580,6 +578,7 @@ export class CheckoutPageComponent implements OnInit, OnDestroy {
const purchase = await this.checkoutService.getPurchase(tenant.codigo, purchaseId); const purchase = await this.checkoutService.getPurchase(tenant.codigo, purchaseId);
if ( if (
purchase.status === 'in_review' ||
purchase.status === 'paid' || purchase.status === 'paid' ||
purchase.status === 'cancelled' || purchase.status === 'cancelled' ||
purchase.status === 'rejected' || purchase.status === 'rejected' ||