switched to paddle inline checkout, removed paypal and most of stripe. added product sync between app and paddle.
This commit is contained in:
@@ -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>;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,39 +1,109 @@
|
||||
import type { Page } from '@playwright/test';
|
||||
|
||||
import { test, expectFixture as expect } from './utils/test-fixtures';
|
||||
|
||||
test.describe('Tenant Admin – core flows', () => {
|
||||
test('dashboard shows key sections for seeded tenant', async ({ signInTenantAdmin, page }) => {
|
||||
await signInTenantAdmin();
|
||||
const futureDate = (daysAhead = 10): string => {
|
||||
const date = new Date();
|
||||
date.setDate(date.getDate() + daysAhead);
|
||||
return date.toISOString().slice(0, 10);
|
||||
};
|
||||
|
||||
await expect(page).toHaveURL(/\/event-admin(\/welcome)?/);
|
||||
async function ensureOnDashboard(page: Page): Promise<void> {
|
||||
await page.goto('/event-admin/dashboard');
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
if (page.url().includes('/event-admin/welcome')) {
|
||||
await page.getByRole('button', { name: /Direkt zum Dashboard/i }).click();
|
||||
if (page.url().includes('/event-admin/welcome')) {
|
||||
const directButton = page.getByRole('button', { name: /Direkt zum Dashboard/i });
|
||||
if (await directButton.isVisible()) {
|
||||
await directButton.click();
|
||||
await page.waitForURL(/\/event-admin\/dashboard$/, { timeout: 15_000 });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
test.describe('Tenant Admin PWA – end-to-end coverage', () => {
|
||||
test.beforeEach(async ({ signInTenantAdmin }) => {
|
||||
await signInTenantAdmin();
|
||||
});
|
||||
|
||||
test('dashboard highlights core stats and quick actions', async ({ page }) => {
|
||||
await ensureOnDashboard(page);
|
||||
|
||||
await expect(page.getByRole('heading', { name: /Hallo/i })).toBeVisible();
|
||||
await expect(page.getByRole('button', { name: /Neues Event/i })).toBeVisible();
|
||||
await expect(page.getByRole('button', { name: /Guided Setup/i })).toBeVisible();
|
||||
|
||||
await expect(page.getByRole('heading', { name: /Hallo Lumen Moments!/i })).toBeVisible();
|
||||
await expect(page.getByRole('button', { name: /Neues Event/i })).toBeVisible();
|
||||
await expect(page.getByRole('button', { name: /Guided Setup/i })).toBeVisible();
|
||||
await expect(page.getByText(/Schnellaktionen/i)).toBeVisible();
|
||||
await expect(page.getByText(/Kommende Events/i)).toBeVisible();
|
||||
});
|
||||
|
||||
test('events overview lists published and draft events', async ({ signInTenantAdmin, page }) => {
|
||||
await signInTenantAdmin();
|
||||
test('event creation flow and detail subsections', async ({ page }) => {
|
||||
const eventName = `Playwright Event ${Date.now()}`;
|
||||
const eventDate = futureDate(14);
|
||||
|
||||
await page.goto('/event-admin/events/new');
|
||||
await page.waitForLoadState('networkidle');
|
||||
await expect(page.getByRole('heading', { name: /Eventdetails/i })).toBeVisible();
|
||||
|
||||
await page.getByLabel(/Eventname/i).fill(eventName);
|
||||
await page.getByLabel(/Datum/i).fill(eventDate);
|
||||
|
||||
const eventTypeTrigger = page.getByRole('combobox', { name: /Event-Typ/i });
|
||||
await eventTypeTrigger.click();
|
||||
const firstOption = page.getByRole('option').first();
|
||||
await expect(firstOption).toBeVisible({ timeout: 5_000 });
|
||||
await firstOption.click();
|
||||
|
||||
await page.getByRole('button', { name: /^Speichern/i }).click();
|
||||
await expect(page).toHaveURL(/\/event-admin\/events\/[a-z0-9-]+$/, { timeout: 20_000 });
|
||||
const createdSlug = page.url().split('/').pop() ?? '';
|
||||
|
||||
await expect(page.getByRole('heading', { name: /Eventdetails/i })).toBeVisible();
|
||||
|
||||
await page.goto('/event-admin/events');
|
||||
await page.waitForLoadState('networkidle');
|
||||
await expect(page.getByText(eventName, { exact: false })).toBeVisible();
|
||||
|
||||
await expect(page.getByRole('heading', { name: /Deine Events/i })).toBeVisible({ timeout: 15_000 });
|
||||
await expect(page.getByRole('button', { name: /Neues Event/i })).toBeVisible();
|
||||
await page.goto(`/event-admin/events/${createdSlug}/photos`);
|
||||
await expect(page.getByRole('heading', { name: /Fotos moderieren/i })).toBeVisible();
|
||||
await expect(page.getByText(/Noch keine Fotos vorhanden/i)).toBeVisible();
|
||||
|
||||
await page.goto(`/event-admin/events/${createdSlug}/members`);
|
||||
await expect(page.getByRole('heading', { name: /Event-Mitglieder/i })).toBeVisible();
|
||||
|
||||
await page.goto(`/event-admin/events/${createdSlug}/tasks`);
|
||||
await expect(page.getByRole('heading', { name: /Event-Tasks/i })).toBeVisible();
|
||||
await expect(page.getByText(/Noch keine Tasks zugewiesen/i)).toBeVisible();
|
||||
});
|
||||
|
||||
test('billing page lists the active package and history', async ({ signInTenantAdmin, page }) => {
|
||||
await signInTenantAdmin();
|
||||
await page.goto('/event-admin/billing');
|
||||
test('task library allows creating custom tasks', async ({ page }) => {
|
||||
await page.goto('/event-admin/tasks');
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
await expect(page.getByRole('heading', { name: /Pakete & Abrechnung/i })).toBeVisible({ timeout: 15_000 });
|
||||
await expect(page.getByRole('heading', { name: /Paketübersicht/i })).toBeVisible();
|
||||
await expect(page.getByText(/Paket-Historie/)).toBeVisible();
|
||||
await expect(page.getByRole('heading', { name: /Task Bibliothek/i })).toBeVisible();
|
||||
|
||||
const taskTitle = `Playwright Task ${Date.now()}`;
|
||||
await page.getByRole('button', { name: /^Neu$/i }).click();
|
||||
await page.getByLabel(/Titel/i).fill(taskTitle);
|
||||
await page.getByLabel(/Beschreibung/i).fill('Automatisierter Testfall');
|
||||
await page.getByRole('button', { name: /^Speichern$/i }).click();
|
||||
|
||||
await expect(page.getByText(taskTitle)).toBeVisible({ timeout: 10_000 });
|
||||
|
||||
await page.goto('/event-admin/task-collections');
|
||||
await page.waitForLoadState('networkidle');
|
||||
await expect(page.getByRole('heading', { name: /Aufgabenvorlagen/i })).toBeVisible();
|
||||
});
|
||||
|
||||
test('supporting sections (emotions, billing, settings) load successfully', async ({ page }) => {
|
||||
await page.goto('/event-admin/emotions');
|
||||
await page.waitForLoadState('networkidle');
|
||||
await expect(page.getByRole('heading', { name: /Emotionen/i })).toBeVisible();
|
||||
|
||||
await page.goto('/event-admin/billing');
|
||||
await page.waitForLoadState('networkidle');
|
||||
await expect(page.getByRole('heading', { name: /Pakete & Abrechnung/i })).toBeVisible();
|
||||
|
||||
await page.goto('/event-admin/settings');
|
||||
await page.waitForLoadState('networkidle');
|
||||
await expect(page.getByRole('heading', { name: /Einstellungen/i })).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6,7 +6,7 @@ import { test, expectFixture as expect } from './utils/test-fixtures';
|
||||
* This suite is currently skipped until we have stable seed data and
|
||||
* authentication helpers for Playwright. Once those are in place we can
|
||||
* remove the skip and let the flow exercise the welcome -> packages -> summary
|
||||
* steps with mocked Stripe/PayPal APIs.
|
||||
* steps with mocked Stripe/Paddle APIs.
|
||||
*/
|
||||
test.describe('Tenant Onboarding Welcome Flow', () => {
|
||||
test('redirects unauthenticated users to login', async ({ page }) => {
|
||||
@@ -47,7 +47,7 @@ test.describe('Tenant Onboarding Welcome Flow', () => {
|
||||
await expect(page).toHaveURL(/\/event-admin\/welcome\/summary/);
|
||||
await expect(page.getByRole('heading', { name: /Bestellübersicht/i })).toBeVisible();
|
||||
|
||||
// Validate payment sections. Depending on env we either see Stripe/PayPal widgets or configuration warnings.
|
||||
// Validate payment sections. Depending on env we either see Stripe/Paddle widgets or configuration warnings.
|
||||
const stripeConfigured = Boolean(process.env.VITE_STRIPE_PUBLISHABLE_KEY);
|
||||
if (stripeConfigured) {
|
||||
await expect(page.getByRole('heading', { name: /Kartenzahlung \(Stripe\)/i })).toBeVisible();
|
||||
@@ -57,12 +57,7 @@ test.describe('Tenant Onboarding Welcome Flow', () => {
|
||||
).toBeVisible();
|
||||
}
|
||||
|
||||
const paypalConfigured = Boolean(process.env.VITE_PAYPAL_CLIENT_ID);
|
||||
if (paypalConfigured) {
|
||||
await expect(page.getByRole('heading', { name: /^PayPal$/i })).toBeVisible();
|
||||
} else {
|
||||
await expect(page.getByText(/PayPal nicht konfiguriert/i)).toBeVisible();
|
||||
}
|
||||
await expect(page.getByRole('heading', { name: /^Paddle$/i })).toBeVisible();
|
||||
|
||||
// Continue to the setup step without completing a purchase.
|
||||
await page.getByRole('button', { name: /Weiter zum Setup/i }).click();
|
||||
|
||||
@@ -64,6 +64,7 @@ type StoredTokenPayload = {
|
||||
refreshToken: string;
|
||||
expiresAt: number;
|
||||
scope?: string;
|
||||
clientId?: string;
|
||||
};
|
||||
|
||||
async function exchangeTokens(request: APIRequestContext): Promise<StoredTokenPayload> {
|
||||
@@ -124,6 +125,7 @@ async function exchangeTokens(request: APIRequestContext): Promise<StoredTokenPa
|
||||
refreshToken: body.refresh_token,
|
||||
expiresAt: Date.now() + Math.max(expiresIn - 30, 0) * 1000,
|
||||
scope: body.scope,
|
||||
clientId,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user