switched to paddle inline checkout, removed paypal and most of stripe. added product sync between app and paddle.

This commit is contained in:
Codex Agent
2025-10-27 17:26:39 +01:00
parent ecf5a23b28
commit 5432456ffd
117 changed files with 4114 additions and 3639 deletions

View File

@@ -1,176 +1,65 @@
import React from 'react';
import { describe, expect, it, vi, beforeEach } from 'vitest';
import { act, fireEvent, render, screen, waitFor } from '@testing-library/react';
import {
StripeCheckoutForm,
PayPalCheckout,
} from '../pages/WelcomeOrderSummaryPage';
import { act, render, screen, waitFor } from '@testing-library/react';
import { PaddleCheckout } from '../pages/WelcomeOrderSummaryPage';
const stripeRef: { current: any } = { current: null };
const elementsRef: { current: any } = { current: null };
const paypalPropsRef: { current: any } = { current: null };
const {
confirmPaymentMock,
completePurchaseMock,
createPayPalOrderMock,
capturePayPalOrderMock,
} = vi.hoisted(() => ({
confirmPaymentMock: vi.fn(),
completePurchaseMock: vi.fn(),
createPayPalOrderMock: vi.fn(),
capturePayPalOrderMock: vi.fn(),
}));
vi.mock('@stripe/react-stripe-js', () => ({
useStripe: () => stripeRef.current,
useElements: () => elementsRef.current,
PaymentElement: () => <div data-testid="stripe-payment-element" />,
Elements: ({ children }: { children: React.ReactNode }) => <>{children}</>,
}));
vi.mock('@paypal/react-paypal-js', () => ({
PayPalScriptProvider: ({ children }: { children: React.ReactNode }) => <>{children}</>,
PayPalButtons: (props: any) => {
paypalPropsRef.current = props;
return <button type="button" data-testid="paypal-button">PayPal</button>;
},
const { createPaddleCheckoutMock } = vi.hoisted(() => ({
createPaddleCheckoutMock: vi.fn(),
}));
vi.mock('../../api', () => ({
completeTenantPackagePurchase: completePurchaseMock,
createTenantPackagePaymentIntent: vi.fn(),
assignFreeTenantPackage: vi.fn(),
createTenantPayPalOrder: createPayPalOrderMock,
captureTenantPayPalOrder: capturePayPalOrderMock,
createTenantPaddleCheckout: createPaddleCheckoutMock,
}));
describe('StripeCheckoutForm', () => {
describe('PaddleCheckout', () => {
beforeEach(() => {
confirmPaymentMock.mockReset();
completePurchaseMock.mockReset();
stripeRef.current = { confirmPayment: confirmPaymentMock };
elementsRef.current = {};
createPaddleCheckoutMock.mockReset();
});
const renderStripeForm = (overrides?: Partial<React.ComponentProps<typeof StripeCheckoutForm>>) =>
render(
<StripeCheckoutForm
clientSecret="secret"
packageId={42}
onSuccess={vi.fn()}
t={(key: string) => key}
{...overrides}
/>
);
it('completes the purchase when Stripe reports a successful payment', async () => {
const onSuccess = vi.fn();
confirmPaymentMock.mockResolvedValue({
error: null,
paymentIntent: { payment_method: 'pm_123' },
});
completePurchaseMock.mockResolvedValue(undefined);
const { container } = renderStripeForm({ onSuccess });
const form = container.querySelector('form');
expect(form).toBeTruthy();
fireEvent.submit(form!);
await waitFor(() => {
expect(completePurchaseMock).toHaveBeenCalledWith({
packageId: 42,
paymentMethodId: 'pm_123',
});
});
expect(onSuccess).toHaveBeenCalled();
});
it('shows Stripe errors returned by confirmPayment', async () => {
confirmPaymentMock.mockResolvedValue({
error: { message: 'Card declined' },
});
const { container } = renderStripeForm();
fireEvent.submit(container.querySelector('form')!);
await waitFor(() => {
expect(screen.getByText('Card declined')).toBeInTheDocument();
});
expect(completePurchaseMock).not.toHaveBeenCalled();
});
it('reports missing payment method id', async () => {
confirmPaymentMock.mockResolvedValue({
error: null,
paymentIntent: {},
});
const { container } = renderStripeForm();
fireEvent.submit(container.querySelector('form')!);
await waitFor(() => {
expect(screen.getByText('summary.stripe.missingPaymentId')).toBeInTheDocument();
});
expect(completePurchaseMock).not.toHaveBeenCalled();
});
});
describe('PayPalCheckout', () => {
beforeEach(() => {
paypalPropsRef.current = null;
createPayPalOrderMock.mockReset();
capturePayPalOrderMock.mockReset();
});
it('creates and captures a PayPal order successfully', async () => {
createPayPalOrderMock.mockResolvedValue('ORDER-123');
capturePayPalOrderMock.mockResolvedValue(undefined);
it('opens Paddle checkout when created successfully', async () => {
createPaddleCheckoutMock.mockResolvedValue({ checkout_url: 'https://paddle.example/checkout' });
const onSuccess = vi.fn();
const openSpy = vi.spyOn(window, 'open').mockImplementation(() => null);
render(
<PayPalCheckout
<PaddleCheckout
packageId={99}
onSuccess={onSuccess}
t={(key: string) => key}
/>
);
expect(paypalPropsRef.current).toBeTruthy();
const { createOrder, onApprove } = paypalPropsRef.current;
await act(async () => {
const orderId = await createOrder();
expect(orderId).toBe('ORDER-123');
});
await act(async () => {
await onApprove({ orderID: 'ORDER-123' });
screen.getByRole('button').click();
});
await waitFor(() => {
expect(createPayPalOrderMock).toHaveBeenCalledWith(99);
expect(capturePayPalOrderMock).toHaveBeenCalledWith('ORDER-123');
expect(createPaddleCheckoutMock).toHaveBeenCalledWith(99);
expect(openSpy).toHaveBeenCalledWith('https://paddle.example/checkout', '_blank', 'noopener');
expect(onSuccess).toHaveBeenCalled();
});
openSpy.mockRestore();
});
it('surfaces missing order id errors', async () => {
createPayPalOrderMock.mockResolvedValue('ORDER-123');
it('shows an error message on failure', async () => {
createPaddleCheckoutMock.mockRejectedValue(new Error('boom'));
render(
<PayPalCheckout
<PaddleCheckout
packageId={99}
onSuccess={vi.fn()}
t={(key: string) => key}
/>
);
const { onApprove } = paypalPropsRef.current;
await act(async () => {
await onApprove({ orderID: undefined });
screen.getByRole('button').click();
});
await waitFor(() => {
expect(screen.getByText('summary.paypal.missingOrderId')).toBeInTheDocument();
expect(screen.getByText('summary.paddle.genericError')).toBeInTheDocument();
});
expect(capturePayPalOrderMock).not.toHaveBeenCalled();
});
});