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

@@ -4,11 +4,12 @@ import { execSync } from 'child_process';
const LOGIN_EMAIL = 'checkout-e2e@example.com';
const LOGIN_PASSWORD = 'Password123!';
test.describe('Checkout Payment Step Stripe & PayPal states', () => {
test.describe('Checkout Payment Step Paddle flow', () => {
test.beforeAll(async () => {
execSync(
`php artisan tenant:add-dummy --email=${LOGIN_EMAIL} --password=${LOGIN_PASSWORD} --first_name=Checkout --last_name=Tester --address="Playwrightstr. 1" --phone="+4912345678"`
);
execSync(
`php artisan tinker --execute="App\\\\Models\\\\User::where('email', '${LOGIN_EMAIL}')->update(['email_verified_at' => now()]);"`
);
@@ -22,110 +23,103 @@ test.describe('Checkout Payment Step Stripe & PayPal states', () => {
await expect(page).toHaveURL(/dashboard/);
});
test('Stripe payment intent error surfaces descriptive status', async ({ page }) => {
await page.route('**/stripe/create-payment-intent', async (route) => {
test('opens Paddle checkout and shows success notice', async ({ page }) => {
await page.route('**/paddle/create-checkout', async (route) => {
const request = route.request();
const postData = request.postDataJSON() as { inline?: boolean } | null;
const inline = Boolean(postData?.inline);
if (inline) {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
mode: 'inline',
items: [
{ priceId: 'pri_123', quantity: 1 },
],
custom_data: {
tenant_id: '1',
package_id: '2',
checkout_session_id: 'cs_123',
},
customer: {
email: LOGIN_EMAIL,
},
}),
});
return;
}
await route.fulfill({
status: 422,
status: 200,
contentType: 'application/json',
body: JSON.stringify({ error: 'Test payment intent failure' }),
body: JSON.stringify({
checkout_url: 'https://paddle.test/checkout/success',
}),
});
});
await page.route('https://cdn.paddle.com/paddle/v2/paddle.js', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/javascript',
body: `
window.Paddle = {
Environment: { set: function(env) { window.__paddleEnv = env; } },
Initialize: function(opts) { window.__paddleInit = opts; },
Checkout: {
open: function(config) {
window.__paddleOpenConfig = config;
}
}
};
`,
});
});
await openCheckoutPaymentStep(page);
await page.evaluate(() => {
window.__openedUrls = [];
window.open = (url: string, target?: string | null, features?: string | null) => {
window.__openedUrls.push({ url, target: target ?? null, features: features ?? null });
return null;
};
});
await page.getByRole('button', { name: /Continue with Paddle|Weiter mit Paddle/ }).click();
await expect(
page.locator('text=/Fehler beim Laden der Zahlungsdaten|Error loading payment data/')
page.locator(
'text=/Paddle checkout is running in a secure overlay|Der Paddle-Checkout läuft jetzt in einem Overlay/'
)
).toBeVisible();
await expect(
page.locator('text=/Zahlungsformular bereit|Payment form ready/')
).not.toBeVisible();
await expect.poll(async () => {
return page.evaluate(() => window.__paddleOpenConfig?.items?.[0]?.priceId ?? null);
}).toBe('pri_123');
await expect.poll(async () => {
return page.evaluate(() => window.__openedUrls?.length ?? 0);
}).toBe(0);
});
test('Stripe payment intent ready state renders when backend responds', async ({ page }) => {
await page.route('**/stripe/create-payment-intent', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ client_secret: 'pi_test_secret' }),
});
});
await openCheckoutPaymentStep(page);
await expect(
page.locator('text=/Zahlungsformular bereit\\. Bitte gib deine Daten ein\\.|Payment form ready\\./')
).toBeVisible();
});
test('PayPal approval success updates status', async ({ page }) => {
await stubPayPalSdk(page);
await page.route('**/paypal/create-order', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ id: 'ORDER_TEST', status: 'CREATED' }),
});
});
await page.route('**/paypal/capture-order', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ status: 'captured' }),
});
});
await openCheckoutPaymentStep(page);
await selectPayPalMethod(page);
await page.waitForFunction(() => window.__paypalButtonsConfig !== undefined);
await page.evaluate(async () => {
const config = window.__paypalButtonsConfig;
if (!config) return;
await config.createOrder();
await config.onApprove({ orderID: 'ORDER_TEST' });
});
await expect(
page.locator('text=/Zahlung bestätigt\\. Bestellung wird abgeschlossen|Payment confirmed/')
).toBeVisible();
});
test('PayPal capture failure notifies user', async ({ page }) => {
await stubPayPalSdk(page);
await page.route('**/paypal/create-order', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ id: 'ORDER_FAIL', status: 'CREATED' }),
});
});
await page.route('**/paypal/capture-order', async (route) => {
test('shows error state when Paddle checkout creation fails', async ({ page }) => {
await page.route('**/paddle/create-checkout', async (route) => {
await route.fulfill({
status: 500,
contentType: 'application/json',
body: JSON.stringify({ error: 'capture_failed' }),
body: JSON.stringify({ message: 'test-error' }),
});
});
await openCheckoutPaymentStep(page);
await selectPayPalMethod(page);
await page.waitForFunction(() => window.__paypalButtonsConfig !== undefined);
await page.evaluate(async () => {
const config = window.__paypalButtonsConfig;
if (!config) return;
await config.createOrder();
await config.onApprove({ orderID: 'ORDER_FAIL' });
});
await page.getByRole('button', { name: /Continue with Paddle|Weiter mit Paddle/ }).click();
await expect(
page.locator('text=/PayPal capture failed|PayPal-Abbuchung fehlgeschlagen|PayPal capture error/')
page.locator('text=/Paddle checkout could not be started|Paddle-Checkout konnte nicht gestartet werden/')
).toBeVisible();
});
});
@@ -151,41 +145,11 @@ async function openCheckoutPaymentStep(page: import('@playwright/test').Page) {
await page.waitForSelector('text=/Zahlung|Payment/');
}
async function selectPayPalMethod(page: import('@playwright/test').Page) {
const paypalButton = page.getByRole('button', { name: /PayPal/ });
if (await paypalButton.isVisible()) {
await paypalButton.click();
}
}
async function stubPayPalSdk(page: import('@playwright/test').Page) {
await page.route('https://www.paypal.com/sdk/js**', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/javascript',
body: `
window.paypal = {
Buttons: function (config) {
window.__paypalButtonsConfig = config;
return {
render: function () {
// noop
},
};
},
};
`,
});
});
}
declare global {
interface Window {
__paypalButtonsConfig?: {
createOrder: () => Promise<string>;
onApprove: (data: { orderID: string }) => Promise<void>;
onError?: (error: unknown) => void;
};
__openedUrls?: Array<{ url: string; target?: string | null; features?: string | null }>;
__paddleOpenConfig?: { url?: string; items?: Array<{ priceId: string; quantity: number }>; settings?: { displayMode?: string } };
__paddleEnv?: string;
__paddleInit?: Record<string, unknown>;
}
}