Update PayPal references and tests
This commit is contained in:
@@ -7,6 +7,7 @@ use Illuminate\Http\Client\Factory as HttpFactory;
|
|||||||
use Illuminate\Http\Client\PendingRequest;
|
use Illuminate\Http\Client\PendingRequest;
|
||||||
use Illuminate\Http\Client\RequestException;
|
use Illuminate\Http\Client\RequestException;
|
||||||
use Illuminate\Support\Arr;
|
use Illuminate\Support\Arr;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
class LemonSqueezyClient
|
class LemonSqueezyClient
|
||||||
{
|
{
|
||||||
@@ -68,7 +69,7 @@ class LemonSqueezyClient
|
|||||||
throw new LemonSqueezyException('Lemon Squeezy API key is not configured.');
|
throw new LemonSqueezyException('Lemon Squeezy API key is not configured.');
|
||||||
}
|
}
|
||||||
|
|
||||||
$baseUrl = rtrim((string) config('lemonsqueezy.base_url'), '/');
|
$baseUrl = $this->normalizeBaseUrl((string) config('lemonsqueezy.base_url'));
|
||||||
|
|
||||||
return $this->http
|
return $this->http
|
||||||
->baseUrl($baseUrl)
|
->baseUrl($baseUrl)
|
||||||
@@ -81,4 +82,30 @@ class LemonSqueezyClient
|
|||||||
->acceptJson()
|
->acceptJson()
|
||||||
->asJson();
|
->asJson();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected function normalizeBaseUrl(string $baseUrl): string
|
||||||
|
{
|
||||||
|
$baseUrl = trim($baseUrl);
|
||||||
|
|
||||||
|
if ($baseUrl === '') {
|
||||||
|
$baseUrl = 'https://api.lemonsqueezy.com/v1';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! Str::startsWith($baseUrl, ['http://', 'https://'])) {
|
||||||
|
$baseUrl = 'https://api.lemonsqueezy.com/'.ltrim($baseUrl, '/');
|
||||||
|
}
|
||||||
|
|
||||||
|
$parsedHost = parse_url($baseUrl, PHP_URL_HOST);
|
||||||
|
if (! is_string($parsedHost) || $parsedHost === '' || ! Str::endsWith($parsedHost, 'lemonsqueezy.com')) {
|
||||||
|
$baseUrl = 'https://api.lemonsqueezy.com/v1';
|
||||||
|
}
|
||||||
|
|
||||||
|
$baseUrl = rtrim($baseUrl, '/');
|
||||||
|
|
||||||
|
if (! Str::endsWith($baseUrl, '/v1')) {
|
||||||
|
$baseUrl .= '/v1';
|
||||||
|
}
|
||||||
|
|
||||||
|
return $baseUrl;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,14 +11,14 @@
|
|||||||
"contact": "Kontakt",
|
"contact": "Kontakt",
|
||||||
"vat_id": "Umsatzsteuer-ID: DE123456789",
|
"vat_id": "Umsatzsteuer-ID: DE123456789",
|
||||||
"monetization": "Monetarisierung",
|
"monetization": "Monetarisierung",
|
||||||
"monetization_desc": "Wir monetarisieren über Packages (Einmalkäufe und Abos) via Lemon Squeezy. Preise exkl. MwSt. Support: support@fotospiel.de",
|
"monetization_desc": "Wir monetarisieren über Packages (Einmalkäufe) via PayPal. Preise exkl. MwSt. Support: support@fotospiel.de",
|
||||||
"register_court": "Registergericht: Amtsgericht Musterstadt",
|
"register_court": "Registergericht: Amtsgericht Musterstadt",
|
||||||
"commercial_register": "Handelsregister: HRB 12345",
|
"commercial_register": "Handelsregister: HRB 12345",
|
||||||
"datenschutz_intro": "Wir nehmen den Schutz Ihrer persönlichen Daten sehr ernst und halten uns strikt an die Regeln der Datenschutzgesetze.",
|
"datenschutz_intro": "Wir nehmen den Schutz Ihrer persönlichen Daten sehr ernst und halten uns strikt an die Regeln der Datenschutzgesetze.",
|
||||||
"responsible": "Verantwortlich: S.E.B. Fotografie, Musterstraße 1, 12345 Musterstadt",
|
"responsible": "Verantwortlich: S.E.B. Fotografie, Musterstraße 1, 12345 Musterstadt",
|
||||||
"data_collection": "Datenerfassung: Keine PII-Speicherung, anonyme Sessions für Gäste. E-Mails werden nur für Kontaktzwecke verarbeitet.",
|
"data_collection": "Datenerfassung: Keine PII-Speicherung, anonyme Sessions für Gäste. E-Mails werden nur für Kontaktzwecke verarbeitet.",
|
||||||
"payments": "Zahlungen und Packages",
|
"payments": "Zahlungen und Packages",
|
||||||
"payments_desc": "Wir verarbeiten Zahlungen für Packages über Lemon Squeezy. Zahlungsdaten werden als Merchant of Record sicher und verschlüsselt durch Lemon Squeezy verarbeitet.",
|
"payments_desc": "Wir verarbeiten Zahlungen für Packages über PayPal. Zahlungsdaten werden sicher und verschlüsselt durch PayPal verarbeitet.",
|
||||||
"data_retention": "Package-Daten (Limits, Features) sind anonymisiert und werden nur für die Funktionalität benötigt. Consent für Zahlungen und E-Mails wird bei Kauf eingeholt. Daten werden nach 10 Jahren gelöscht.",
|
"data_retention": "Package-Daten (Limits, Features) sind anonymisiert und werden nur für die Funktionalität benötigt. Consent für Zahlungen und E-Mails wird bei Kauf eingeholt. Daten werden nach 10 Jahren gelöscht.",
|
||||||
"rights": "Ihre Rechte: Auskunft, Löschung, Widerspruch.",
|
"rights": "Ihre Rechte: Auskunft, Löschung, Widerspruch.",
|
||||||
"cookies": "Cookies: Nur funktionale Cookies für die PWA.",
|
"cookies": "Cookies: Nur funktionale Cookies für die PWA.",
|
||||||
|
|||||||
@@ -10,14 +10,14 @@
|
|||||||
"contact": "Contact",
|
"contact": "Contact",
|
||||||
"vat_id": "VAT ID: DE123456789",
|
"vat_id": "VAT ID: DE123456789",
|
||||||
"monetization": "Monetization",
|
"monetization": "Monetization",
|
||||||
"monetization_desc": "We monetize through Packages (one-time purchases and subscriptions) via Lemon Squeezy. Prices excl. VAT. Support: support@fotospiel.de",
|
"monetization_desc": "Wir monetarisieren ?ber Packages (Einmalk?ufe) via PayPal. Preise exkl. MwSt. Support: support@fotospiel.de",
|
||||||
"register_court": "Register Court: District Court Musterstadt",
|
"register_court": "Register Court: District Court Musterstadt",
|
||||||
"commercial_register": "Commercial Register: HRB 12345",
|
"commercial_register": "Commercial Register: HRB 12345",
|
||||||
"datenschutz_intro": "We take the protection of your personal data very seriously and strictly adhere to the rules of data protection laws.",
|
"datenschutz_intro": "We take the protection of your personal data very seriously and strictly adhere to the rules of data protection laws.",
|
||||||
"responsible": "Responsible: S.E.B. Fotografie, Musterstraße 1, 12345 Musterstadt",
|
"responsible": "Responsible: S.E.B. Fotografie, Musterstraße 1, 12345 Musterstadt",
|
||||||
"data_collection": "Data collection: No PII storage, anonymous sessions for guests. Emails are only processed for contact purposes.",
|
"data_collection": "Data collection: No PII storage, anonymous sessions for guests. Emails are only processed for contact purposes.",
|
||||||
"payments": "Payments and Packages",
|
"payments": "Payments and Packages",
|
||||||
"payments_desc": "We process payments for Packages via Lemon Squeezy. Payment data is handled securely and encrypted by Lemon Squeezy as the merchant of record.",
|
"payments_desc": "Wir verarbeiten Zahlungen f?r Packages ?ber PayPal. Zahlungsdaten werden sicher und verschl?sselt durch PayPal verarbeitet.",
|
||||||
"data_retention": "Package data (limits, features) is anonymized and only required for functionality. Consent for payments and emails is obtained at purchase. Data is deleted after 10 years.",
|
"data_retention": "Package data (limits, features) is anonymized and only required for functionality. Consent for payments and emails is obtained at purchase. Data is deleted after 10 years.",
|
||||||
"rights": "Your rights: Information, deletion, objection. Contact us under Contact.",
|
"rights": "Your rights: Information, deletion, objection. Contact us under Contact.",
|
||||||
"cookies": "Cookies: Only functional cookies for the PWA.",
|
"cookies": "Cookies: Only functional cookies for the PWA.",
|
||||||
|
|||||||
@@ -1,6 +1,18 @@
|
|||||||
{
|
{
|
||||||
"mobileProfile": {
|
"mobileProfile": {
|
||||||
"settings": "Einstellungen"
|
"title": "Profil",
|
||||||
|
"settings": "Einstellungen",
|
||||||
|
"account": "Account bearbeiten",
|
||||||
|
"language": "Sprache",
|
||||||
|
"languageDe": "Deutsch",
|
||||||
|
"languageEn": "Englisch",
|
||||||
|
"theme": "Theme",
|
||||||
|
"themeLight": "Hell",
|
||||||
|
"themeDark": "Dunkel",
|
||||||
|
"themeSystem": "System",
|
||||||
|
"logout": "Abmelden",
|
||||||
|
"logoutTitle": "Ausloggen",
|
||||||
|
"logoutHint": "Aus der App ausloggen"
|
||||||
},
|
},
|
||||||
"readiness": {
|
"readiness": {
|
||||||
"steps": {
|
"steps": {
|
||||||
@@ -15,7 +27,7 @@
|
|||||||
"actions": {
|
"actions": {
|
||||||
"refresh": "Aktualisieren",
|
"refresh": "Aktualisieren",
|
||||||
"exportCsv": "Export als CSV",
|
"exportCsv": "Export als CSV",
|
||||||
"portal": "Im Lemon Squeezy-Portal verwalten",
|
"portal": "Im PayPal-Portal verwalten",
|
||||||
"portalBusy": "Portal wird geöffnet...",
|
"portalBusy": "Portal wird geöffnet...",
|
||||||
"openPackages": "Pakete öffnen",
|
"openPackages": "Pakete öffnen",
|
||||||
"contactSupport": "Support kontaktieren"
|
"contactSupport": "Support kontaktieren"
|
||||||
@@ -42,7 +54,7 @@
|
|||||||
"errors": {
|
"errors": {
|
||||||
"load": "Paketdaten konnten nicht geladen werden.",
|
"load": "Paketdaten konnten nicht geladen werden.",
|
||||||
"more": "Weitere Einträge konnten nicht geladen werden.",
|
"more": "Weitere Einträge konnten nicht geladen werden.",
|
||||||
"portal": "Lemon Squeezy-Portal konnte nicht geöffnet werden."
|
"portal": "PayPal-Portal konnte nicht geöffnet werden."
|
||||||
},
|
},
|
||||||
"checkoutSuccess": "Checkout abgeschlossen. Dein Paket wird in Kürze aktiviert.",
|
"checkoutSuccess": "Checkout abgeschlossen. Dein Paket wird in Kürze aktiviert.",
|
||||||
"checkoutCancelled": "Checkout wurde abgebrochen.",
|
"checkoutCancelled": "Checkout wurde abgebrochen.",
|
||||||
@@ -128,9 +140,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"transactions": {
|
"transactions": {
|
||||||
"title": "Lemon Squeezy-Transaktionen",
|
"title": "PayPal-Transaktionen",
|
||||||
"description": "Neueste Lemon Squeezy-Transaktionen für dieses Kundenkonto.",
|
"description": "Neueste PayPal-Transaktionen für dieses Kundenkonto.",
|
||||||
"empty": "Noch keine Lemon Squeezy-Transaktionen.",
|
"empty": "Noch keine PayPal-Transaktionen.",
|
||||||
"labels": {
|
"labels": {
|
||||||
"transactionId": "Transaktion {{id}}",
|
"transactionId": "Transaktion {{id}}",
|
||||||
"checkoutId": "Checkout-ID: {{id}}",
|
"checkoutId": "Checkout-ID: {{id}}",
|
||||||
@@ -2295,8 +2307,8 @@
|
|||||||
},
|
},
|
||||||
"mobileDashboard": {
|
"mobileDashboard": {
|
||||||
"title": "Dashboard",
|
"title": "Dashboard",
|
||||||
"shortcutAnalytics": "Analytics",
|
"shortcutAnalytics": "Statistiken",
|
||||||
"quickActionsTitle": "Experience",
|
"quickActionsTitle": "Schnellzugriff",
|
||||||
"readyForLiftoff": "Alles erledigt.",
|
"readyForLiftoff": "Alles erledigt.",
|
||||||
"selectEvent": "Wähle ein Event, um fortzufahren",
|
"selectEvent": "Wähle ein Event, um fortzufahren",
|
||||||
"emptyBadge": "Willkommen!",
|
"emptyBadge": "Willkommen!",
|
||||||
@@ -2418,8 +2430,6 @@
|
|||||||
"shortcutInvites": "Team-/Helfer-Einladungen",
|
"shortcutInvites": "Team-/Helfer-Einladungen",
|
||||||
"shortcutSettings": "Event-Einstellungen",
|
"shortcutSettings": "Event-Einstellungen",
|
||||||
"shortcutBranding": "Branding & Moderation",
|
"shortcutBranding": "Branding & Moderation",
|
||||||
"shortcutAnalytics": "Statistiken",
|
|
||||||
"quickActionsTitle": "Schnellzugriff",
|
|
||||||
"kpiTitle": "Wichtigste Kennzahlen",
|
"kpiTitle": "Wichtigste Kennzahlen",
|
||||||
"kpiTasks": "Offene Fotoaufgaben",
|
"kpiTasks": "Offene Fotoaufgaben",
|
||||||
"kpiPhotos": "Fotos",
|
"kpiPhotos": "Fotos",
|
||||||
@@ -2672,25 +2682,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"mobileProfile": {
|
"mobileSettings": {
|
||||||
"title": "Profil",
|
"title": "Einstellungen",
|
||||||
"settings": "Einstellungen",
|
"accountTitle": "Account",
|
||||||
"account": "Account bearbeiten",
|
"tenantBadge": "Konto #{{id}}",
|
||||||
"language": "Sprache",
|
|
||||||
"languageDe": "Deutsch",
|
|
||||||
"languageEn": "Englisch",
|
|
||||||
"theme": "Theme",
|
|
||||||
"themeLight": "Hell",
|
|
||||||
"themeDark": "Dunkel",
|
|
||||||
"themeSystem": "System",
|
|
||||||
"logout": "Abmelden",
|
|
||||||
"logoutTitle": "Ausloggen",
|
|
||||||
"logoutHint": "Aus der App ausloggen"
|
|
||||||
},
|
|
||||||
"mobileSettings": {
|
|
||||||
"title": "Einstellungen",
|
|
||||||
"accountTitle": "Account",
|
|
||||||
"tenantBadge": "Konto #{{id}}",
|
|
||||||
"notificationsTitle": "Benachrichtigungen",
|
"notificationsTitle": "Benachrichtigungen",
|
||||||
"notificationsLoading": "Lade Einstellungen ...",
|
"notificationsLoading": "Lade Einstellungen ...",
|
||||||
"pushTitle": "App Push",
|
"pushTitle": "App Push",
|
||||||
@@ -2861,7 +2856,7 @@
|
|||||||
"validation": "Füge Titel, Nachricht und ggf. einen Ziel-Gast hinzu."
|
"validation": "Füge Titel, Nachricht und ggf. einen Ziel-Gast hinzu."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dataExports": {
|
"dataExports": {
|
||||||
"title": "Datenexporte",
|
"title": "Datenexporte",
|
||||||
"request": {
|
"request": {
|
||||||
"title": "Exportanfrage",
|
"title": "Exportanfrage",
|
||||||
@@ -3034,5 +3029,12 @@
|
|||||||
"checkout": "Checkout fehlgeschlagen"
|
"checkout": "Checkout fehlgeschlagen"
|
||||||
},
|
},
|
||||||
"selectDisabled": "Nicht verfügbar"
|
"selectDisabled": "Nicht verfügbar"
|
||||||
|
},
|
||||||
|
"billingOverview": {
|
||||||
|
"transactions": {
|
||||||
|
"title": "PayPal-Transaktionen",
|
||||||
|
"description": "Neueste PayPal-Transaktionen für dieses Kundenkonto.",
|
||||||
|
"empty": "Noch keine PayPal-Transaktionen."
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
{
|
{
|
||||||
"layout": {
|
"layout": {
|
||||||
"eyebrow": "Fotospiel Kunden-Admin",
|
"eyebrow": "Fotospiel Kunden-Admin",
|
||||||
"title": "Willkommen im Event-Erlebnisstudio",
|
"title": "Willkommen im Event-Erlebnisstudio",
|
||||||
@@ -193,24 +193,24 @@
|
|||||||
"errorMessage": "Kostenloses Paket konnte nicht aktiviert werden."
|
"errorMessage": "Kostenloses Paket konnte nicht aktiviert werden."
|
||||||
},
|
},
|
||||||
"lemonsqueezy": {
|
"lemonsqueezy": {
|
||||||
"sectionTitle": "Lemon Squeezy",
|
"sectionTitle": "PayPal",
|
||||||
"heading": "Checkout mit Lemon Squeezy",
|
"heading": "Checkout mit PayPal",
|
||||||
"genericError": "Der Lemon Squeezy-Checkout konnte nicht geöffnet werden. Bitte versuche es erneut.",
|
"genericError": "Der PayPal-Checkout konnte nicht geöffnet werden. Bitte versuche es erneut.",
|
||||||
"errorTitle": "Lemon Squeezy-Fehler",
|
"errorTitle": "PayPal-Fehler",
|
||||||
"processing": "Lemon Squeezy-Checkout wird geöffnet …",
|
"processing": "PayPal-Checkout wird geöffnet …",
|
||||||
"cta": "Lemon Squeezy-Checkout öffnen",
|
"cta": "PayPal-Checkout öffnen",
|
||||||
"hint": "Es öffnet sich ein neuer Tab über Lemon Squeezy (Merchant of Record). Schließe dort die Zahlung ab und kehre anschließend zurück."
|
"hint": "Es öffnet sich ein neuer Tab über PayPal. Schließe dort die Zahlung ab und kehre anschließend zurück."
|
||||||
},
|
},
|
||||||
"nextStepsTitle": "Nächste Schritte",
|
"nextStepsTitle": "Nächste Schritte",
|
||||||
"nextSteps": [
|
"nextSteps": [
|
||||||
"Optional: Abrechnung über Lemon Squeezy im Billing-Bereich abschließen.",
|
"Optional: Abrechnung über PayPal im Billing-Bereich abschließen.",
|
||||||
"Event-Setup durchlaufen und Fotoaufgaben, Team & Galerie konfigurieren.",
|
"Event-Setup durchlaufen und Fotoaufgaben, Team & Galerie konfigurieren.",
|
||||||
"Vor dem Go-Live Event-Kontingent prüfen und Gäste-Link teilen."
|
"Vor dem Go-Live Event-Kontingent prüfen und Gäste-Link teilen."
|
||||||
],
|
],
|
||||||
"cta": {
|
"cta": {
|
||||||
"billing": {
|
"billing": {
|
||||||
"label": "Abrechnung starten",
|
"label": "Abrechnung starten",
|
||||||
"description": "Öffnet den Billing-Bereich mit Lemon Squeezy- und Kontingent-Optionen.",
|
"description": "Öffnet den Billing-Bereich mit PayPal- und Kontingent-Optionen.",
|
||||||
"button": "Zu Billing & Zahlung"
|
"button": "Zu Billing & Zahlung"
|
||||||
},
|
},
|
||||||
"setup": {
|
"setup": {
|
||||||
@@ -267,5 +267,19 @@
|
|||||||
"button": "Eventliste"
|
"button": "Eventliste"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"lemonsqueezy": {
|
||||||
|
"sectionTitle": "PayPal",
|
||||||
|
"heading": "Checkout mit PayPal",
|
||||||
|
"genericError": "Der PayPal-Checkout konnte nicht geöffnet werden. Bitte versuche es erneut.",
|
||||||
|
"errorTitle": "PayPal-Fehler",
|
||||||
|
"processing": "PayPal-Checkout wird geöffnet …",
|
||||||
|
"cta": "PayPal-Checkout öffnen",
|
||||||
|
"hint": "Es öffnet sich ein neuer Tab über PayPal. Schließe dort die Zahlung ab und kehre anschließend zurück."
|
||||||
|
},
|
||||||
|
"tasks": {
|
||||||
|
"billing": {
|
||||||
|
"description": "Öffnet den Billing-Bereich mit PayPal- und Kontingent-Optionen."
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,18 @@
|
|||||||
{
|
{
|
||||||
"mobileProfile": {
|
"mobileProfile": {
|
||||||
"settings": "Settings"
|
"title": "Profile",
|
||||||
|
"settings": "Settings",
|
||||||
|
"account": "Edit account",
|
||||||
|
"language": "Language",
|
||||||
|
"languageDe": "Deutsch",
|
||||||
|
"languageEn": "English",
|
||||||
|
"theme": "Theme",
|
||||||
|
"themeLight": "Light",
|
||||||
|
"themeDark": "Dark",
|
||||||
|
"themeSystem": "System",
|
||||||
|
"logout": "Log out",
|
||||||
|
"logoutTitle": "Sign out",
|
||||||
|
"logoutHint": "Sign out from this app."
|
||||||
},
|
},
|
||||||
"readiness": {
|
"readiness": {
|
||||||
"steps": {
|
"steps": {
|
||||||
@@ -15,7 +27,7 @@
|
|||||||
"actions": {
|
"actions": {
|
||||||
"refresh": "Refresh",
|
"refresh": "Refresh",
|
||||||
"exportCsv": "Export CSV",
|
"exportCsv": "Export CSV",
|
||||||
"portal": "Manage in Lemon Squeezy",
|
"portal": "Manage in PayPal",
|
||||||
"portalBusy": "Opening portal...",
|
"portalBusy": "Opening portal...",
|
||||||
"openPackages": "Open packages",
|
"openPackages": "Open packages",
|
||||||
"contactSupport": "Contact support"
|
"contactSupport": "Contact support"
|
||||||
@@ -42,7 +54,7 @@
|
|||||||
"errors": {
|
"errors": {
|
||||||
"load": "Unable to load package data.",
|
"load": "Unable to load package data.",
|
||||||
"more": "Unable to load more entries.",
|
"more": "Unable to load more entries.",
|
||||||
"portal": "Unable to open the Lemon Squeezy portal."
|
"portal": "Unable to open the PayPal portal."
|
||||||
},
|
},
|
||||||
"checkoutSuccess": "Checkout completed. Your package will activate shortly.",
|
"checkoutSuccess": "Checkout completed. Your package will activate shortly.",
|
||||||
"checkoutCancelled": "Checkout was cancelled.",
|
"checkoutCancelled": "Checkout was cancelled.",
|
||||||
@@ -128,9 +140,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"transactions": {
|
"transactions": {
|
||||||
"title": "Lemon Squeezy transactions",
|
"title": "PayPal transactions",
|
||||||
"description": "Recent Lemon Squeezy transactions for this customer account.",
|
"description": "Recent PayPal transactions for this customer account.",
|
||||||
"empty": "No Lemon Squeezy transactions yet.",
|
"empty": "No PayPal transactions yet.",
|
||||||
"labels": {
|
"labels": {
|
||||||
"transactionId": "Transaction {{id}}",
|
"transactionId": "Transaction {{id}}",
|
||||||
"checkoutId": "Checkout ID: {{id}}",
|
"checkoutId": "Checkout ID: {{id}}",
|
||||||
@@ -2298,7 +2310,7 @@
|
|||||||
"mobileDashboard": {
|
"mobileDashboard": {
|
||||||
"title": "Dashboard",
|
"title": "Dashboard",
|
||||||
"shortcutAnalytics": "Analytics",
|
"shortcutAnalytics": "Analytics",
|
||||||
"quickActionsTitle": "Experience",
|
"quickActionsTitle": "Quick actions",
|
||||||
"readyForLiftoff": "Ready for Liftoff",
|
"readyForLiftoff": "Ready for Liftoff",
|
||||||
"selectEvent": "Select an event to continue",
|
"selectEvent": "Select an event to continue",
|
||||||
"emptyBadge": "Welcome aboard",
|
"emptyBadge": "Welcome aboard",
|
||||||
@@ -2420,8 +2432,6 @@
|
|||||||
"shortcutInvites": "Team / helper invites",
|
"shortcutInvites": "Team / helper invites",
|
||||||
"shortcutSettings": "Event settings",
|
"shortcutSettings": "Event settings",
|
||||||
"shortcutBranding": "Branding & moderation",
|
"shortcutBranding": "Branding & moderation",
|
||||||
"shortcutAnalytics": "Analytics",
|
|
||||||
"quickActionsTitle": "Quick actions",
|
|
||||||
"kpiTitle": "Key performance indicators",
|
"kpiTitle": "Key performance indicators",
|
||||||
"kpiTasks": "Open photo tasks",
|
"kpiTasks": "Open photo tasks",
|
||||||
"kpiPhotos": "Photos",
|
"kpiPhotos": "Photos",
|
||||||
@@ -2674,21 +2684,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"mobileProfile": {
|
|
||||||
"title": "Profile",
|
|
||||||
"settings": "Settings",
|
|
||||||
"account": "Edit account",
|
|
||||||
"language": "Language",
|
|
||||||
"languageDe": "Deutsch",
|
|
||||||
"languageEn": "English",
|
|
||||||
"theme": "Theme",
|
|
||||||
"themeLight": "Light",
|
|
||||||
"themeDark": "Dark",
|
|
||||||
"themeSystem": "System",
|
|
||||||
"logout": "Log out",
|
|
||||||
"logoutTitle": "Sign out",
|
|
||||||
"logoutHint": "Sign out from this app."
|
|
||||||
},
|
|
||||||
"mobileSettings": {
|
"mobileSettings": {
|
||||||
"title": "Settings",
|
"title": "Settings",
|
||||||
"accountTitle": "Account",
|
"accountTitle": "Account",
|
||||||
@@ -2863,13 +2858,13 @@
|
|||||||
"validation": "Add a title, message, and target guest when needed."
|
"validation": "Add a title, message, and target guest when needed."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dataExports": {
|
"dataExports": {
|
||||||
"title": "Data exports",
|
"title": "Data exports",
|
||||||
"request": {
|
"request": {
|
||||||
"title": "Export request",
|
"title": "Export request",
|
||||||
"hint": "Export account data or a specific event archive.",
|
"hint": "Export account data or a specific event archive.",
|
||||||
"progress": "Export is running. This list refreshes automatically."
|
"progress": "Export is running. This list refreshes automatically."
|
||||||
},
|
},
|
||||||
"fields": {
|
"fields": {
|
||||||
"scope": "Scope",
|
"scope": "Scope",
|
||||||
"event": "Event",
|
"event": "Event",
|
||||||
@@ -3036,5 +3031,12 @@
|
|||||||
"checkout": "Checkout failed"
|
"checkout": "Checkout failed"
|
||||||
},
|
},
|
||||||
"selectDisabled": "Not available"
|
"selectDisabled": "Not available"
|
||||||
|
},
|
||||||
|
"billingOverview": {
|
||||||
|
"transactions": {
|
||||||
|
"title": "PayPal transactions",
|
||||||
|
"description": "Recent PayPal transactions for this customer account.",
|
||||||
|
"empty": "No PayPal transactions yet."
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
{
|
{
|
||||||
"layout": {
|
"layout": {
|
||||||
"eyebrow": "Fotospiel Customer Admin",
|
"eyebrow": "Fotospiel Customer Admin",
|
||||||
"title": "Welcome to your event studio",
|
"title": "Welcome to your event studio",
|
||||||
@@ -193,24 +193,24 @@
|
|||||||
"errorMessage": "The free package could not be activated."
|
"errorMessage": "The free package could not be activated."
|
||||||
},
|
},
|
||||||
"lemonsqueezy": {
|
"lemonsqueezy": {
|
||||||
"sectionTitle": "Lemon Squeezy",
|
"sectionTitle": "PayPal",
|
||||||
"heading": "Checkout with Lemon Squeezy",
|
"heading": "Checkout with PayPal",
|
||||||
"genericError": "The Lemon Squeezy checkout could not be opened. Please try again.",
|
"genericError": "The PayPal checkout could not be opened. Please try again.",
|
||||||
"errorTitle": "Lemon Squeezy error",
|
"errorTitle": "PayPal error",
|
||||||
"processing": "Opening the Lemon Squeezy checkout …",
|
"processing": "Opening the PayPal checkout ?",
|
||||||
"cta": "Open Lemon Squeezy checkout",
|
"cta": "Open PayPal checkout",
|
||||||
"hint": "A new tab opens via Lemon Squeezy (merchant of record). Complete the payment there, then return to continue."
|
"hint": "A new tab opens via PayPal. Complete the payment there, then return to continue."
|
||||||
},
|
},
|
||||||
"nextStepsTitle": "Next steps",
|
"nextStepsTitle": "Next steps",
|
||||||
"nextSteps": [
|
"nextSteps": [
|
||||||
"Optional: finish billing via Lemon Squeezy inside the billing area.",
|
"Optional: finish billing via PayPal inside the billing area.",
|
||||||
"Complete the event setup and configure photo tasks, team, and gallery.",
|
"Complete the event setup and configure photo tasks, team, and gallery.",
|
||||||
"Check your event bundle before go-live and share your guest link."
|
"Check your event bundle before go-live and share your guest link."
|
||||||
],
|
],
|
||||||
"cta": {
|
"cta": {
|
||||||
"billing": {
|
"billing": {
|
||||||
"label": "Start billing",
|
"label": "Start billing",
|
||||||
"description": "Opens the billing area with Lemon Squeezy bundle options.",
|
"description": "Opens the billing area with PayPal bundle options.",
|
||||||
"button": "Go to billing"
|
"button": "Go to billing"
|
||||||
},
|
},
|
||||||
"setup": {
|
"setup": {
|
||||||
@@ -267,5 +267,19 @@
|
|||||||
"button": "Open event list"
|
"button": "Open event list"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"lemonsqueezy": {
|
||||||
|
"sectionTitle": "PayPal",
|
||||||
|
"heading": "Checkout with PayPal",
|
||||||
|
"genericError": "The PayPal checkout could not be opened. Please try again.",
|
||||||
|
"errorTitle": "PayPal error",
|
||||||
|
"processing": "Opening the PayPal checkout ?",
|
||||||
|
"cta": "Open PayPal checkout",
|
||||||
|
"hint": "A new tab opens via PayPal. Complete the payment there, then return to continue."
|
||||||
|
},
|
||||||
|
"tasks": {
|
||||||
|
"billing": {
|
||||||
|
"description": "Opens the billing area with PayPal billing options."
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -188,7 +188,7 @@ return [
|
|||||||
],
|
],
|
||||||
'lemonsqueezy_health' => [
|
'lemonsqueezy_health' => [
|
||||||
'navigation' => [
|
'navigation' => [
|
||||||
'label' => 'Lemon Squeezy-Status',
|
'label' => 'PayPal-Status',
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
'integrations_health' => [
|
'integrations_health' => [
|
||||||
@@ -203,7 +203,7 @@ return [
|
|||||||
'unknown' => 'Unbekannt',
|
'unknown' => 'Unbekannt',
|
||||||
],
|
],
|
||||||
'heading' => 'Integrationen-Status',
|
'heading' => 'Integrationen-Status',
|
||||||
'help' => 'Operativer Überblick über Lemon Squeezy/RevenueCat-Webhooks, Queue-Backlog und jüngste Fehler.',
|
'help' => 'Operativer Überblick über PayPal/RevenueCat-Webhooks, Queue-Backlog und jüngste Fehler.',
|
||||||
'configured' => 'Konfiguriert',
|
'configured' => 'Konfiguriert',
|
||||||
'unconfigured' => 'Nicht konfiguriert',
|
'unconfigured' => 'Nicht konfiguriert',
|
||||||
'last_received' => 'Zuletzt empfangen',
|
'last_received' => 'Zuletzt empfangen',
|
||||||
|
|||||||
@@ -12,14 +12,14 @@ return [
|
|||||||
'contact' => 'Kontakt',
|
'contact' => 'Kontakt',
|
||||||
'vat_id' => 'Umsatzsteuer-ID: DE123456789',
|
'vat_id' => 'Umsatzsteuer-ID: DE123456789',
|
||||||
'monetization' => 'Monetarisierung',
|
'monetization' => 'Monetarisierung',
|
||||||
'monetization_desc' => 'Wir monetarisieren über Packages (Einmalkäufe und Abos) via Lemon Squeezy. Preise exkl. MwSt. Support: support@fotospiel.de',
|
'monetization_desc' => 'Wir monetarisieren über Packages (Einmalkäufe) via PayPal. Preise exkl. MwSt. Support: support@fotospiel.de',
|
||||||
'register_court' => 'Registergericht: Amtsgericht Musterstadt',
|
'register_court' => 'Registergericht: Amtsgericht Musterstadt',
|
||||||
'commercial_register' => 'Handelsregister: HRB 12345',
|
'commercial_register' => 'Handelsregister: HRB 12345',
|
||||||
'datenschutz_intro' => 'Wir nehmen den Schutz Ihrer persönlichen Daten sehr ernst und halten uns strikt an die Regeln der Datenschutzgesetze.',
|
'datenschutz_intro' => 'Wir nehmen den Schutz Ihrer persönlichen Daten sehr ernst und halten uns strikt an die Regeln der Datenschutzgesetze.',
|
||||||
'responsible' => 'Verantwortlich: S.E.B. Fotografie, Musterstraße 1, 12345 Musterstadt',
|
'responsible' => 'Verantwortlich: S.E.B. Fotografie, Musterstraße 1, 12345 Musterstadt',
|
||||||
'data_collection' => 'Datenerfassung: Keine PII-Speicherung, anonyme Sessions für Gäste. E-Mails werden nur für Kontaktzwecke verarbeitet.',
|
'data_collection' => 'Datenerfassung: Keine PII-Speicherung, anonyme Sessions für Gäste. E-Mails werden nur für Kontaktzwecke verarbeitet.',
|
||||||
'payments' => 'Zahlungen und Packages',
|
'payments' => 'Zahlungen und Packages',
|
||||||
'payments_desc' => 'Wir verarbeiten Zahlungen für Packages über Lemon Squeezy. Zahlungsinformationen werden sicher und verschlüsselt durch Lemon Squeezy als Merchant of Record verarbeitet.',
|
'payments_desc' => 'Wir verarbeiten Zahlungen für Packages über PayPal. Zahlungsinformationen werden sicher und verschlüsselt durch PayPal verarbeitet.',
|
||||||
'data_retention' => 'Package-Daten (Limits, Features) sind anonymisiert und werden nur für die Funktionalität benötigt. Consent für Zahlungen und E-Mails wird bei Kauf eingeholt. Daten werden nach 10 Jahren gelöscht.',
|
'data_retention' => 'Package-Daten (Limits, Features) sind anonymisiert und werden nur für die Funktionalität benötigt. Consent für Zahlungen und E-Mails wird bei Kauf eingeholt. Daten werden nach 10 Jahren gelöscht.',
|
||||||
'rights' => 'Ihre Rechte: Auskunft, Löschung, Widerspruch.',
|
'rights' => 'Ihre Rechte: Auskunft, Löschung, Widerspruch.',
|
||||||
'cookies' => 'Cookies: Nur funktionale Cookies für die PWA.',
|
'cookies' => 'Cookies: Nur funktionale Cookies für die PWA.',
|
||||||
|
|||||||
@@ -188,7 +188,7 @@ return [
|
|||||||
],
|
],
|
||||||
'lemonsqueezy_health' => [
|
'lemonsqueezy_health' => [
|
||||||
'navigation' => [
|
'navigation' => [
|
||||||
'label' => 'Lemon Squeezy health',
|
'label' => 'PayPal health',
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
'integrations_health' => [
|
'integrations_health' => [
|
||||||
@@ -203,7 +203,7 @@ return [
|
|||||||
'unknown' => 'Unknown',
|
'unknown' => 'Unknown',
|
||||||
],
|
],
|
||||||
'heading' => 'Integrations health',
|
'heading' => 'Integrations health',
|
||||||
'help' => 'Operational snapshot of Lemon Squeezy/RevenueCat webhooks, queue backlog, and recent failures.',
|
'help' => 'Operational snapshot of PayPal/RevenueCat webhooks, queue backlog, and recent failures.',
|
||||||
'configured' => 'Configured',
|
'configured' => 'Configured',
|
||||||
'unconfigured' => 'Unconfigured',
|
'unconfigured' => 'Unconfigured',
|
||||||
'last_received' => 'Last received',
|
'last_received' => 'Last received',
|
||||||
|
|||||||
@@ -12,14 +12,14 @@ return [
|
|||||||
'contact' => 'Contact',
|
'contact' => 'Contact',
|
||||||
'vat_id' => 'VAT ID: DE123456789',
|
'vat_id' => 'VAT ID: DE123456789',
|
||||||
'monetization' => 'Monetization',
|
'monetization' => 'Monetization',
|
||||||
'monetization_desc' => 'We monetize through Packages (one-time purchases and subscriptions) via Lemon Squeezy. Prices excl. VAT. Support: support@fotospiel.de',
|
'monetization_desc' => 'We monetize through Packages (one-time purchases) via PayPal. Prices excl. VAT. Support: support@fotospiel.de',
|
||||||
'register_court' => 'Register Court: District Court Musterstadt',
|
'register_court' => 'Register Court: District Court Musterstadt',
|
||||||
'commercial_register' => 'Commercial Register: HRB 12345',
|
'commercial_register' => 'Commercial Register: HRB 12345',
|
||||||
'datenschutz_intro' => 'We take the protection of your personal data very seriously and strictly adhere to the rules of data protection laws.',
|
'datenschutz_intro' => 'We take the protection of your personal data very seriously and strictly adhere to the rules of data protection laws.',
|
||||||
'responsible' => 'Responsible: S.E.B. Fotografie, Musterstraße 1, 12345 Musterstadt',
|
'responsible' => 'Responsible: S.E.B. Fotografie, Musterstraße 1, 12345 Musterstadt',
|
||||||
'data_collection' => 'Data collection: No PII storage, anonymous sessions for guests. Emails are only processed for contact purposes.',
|
'data_collection' => 'Data collection: No PII storage, anonymous sessions for guests. Emails are only processed for contact purposes.',
|
||||||
'payments' => 'Payments and Packages',
|
'payments' => 'Payments and Packages',
|
||||||
'payments_desc' => 'We process payments for Packages via Lemon Squeezy. Payment information is handled securely and encrypted by Lemon Squeezy as the merchant of record.',
|
'payments_desc' => 'We process payments for Packages via PayPal. Payment information is handled securely and encrypted by PayPal.',
|
||||||
'data_retention' => 'Package data (limits, features) is anonymized and only required for functionality. Consent for payments and emails is obtained at purchase. Data is deleted after 10 years.',
|
'data_retention' => 'Package data (limits, features) is anonymized and only required for functionality. Consent for payments and emails is obtained at purchase. Data is deleted after 10 years.',
|
||||||
'rights' => 'Your rights: Information, deletion, objection. Contact us under Contact.',
|
'rights' => 'Your rights: Information, deletion, objection. Contact us under Contact.',
|
||||||
'cookies' => 'Cookies: Only functional cookies for the PWA.',
|
'cookies' => 'Cookies: Only functional cookies for the PWA.',
|
||||||
|
|||||||
40
tests/Unit/LemonSqueezyClientTest.php
Normal file
40
tests/Unit/LemonSqueezyClientTest.php
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Unit;
|
||||||
|
|
||||||
|
use App\Services\LemonSqueezy\LemonSqueezyClient;
|
||||||
|
use Illuminate\Support\Facades\Http;
|
||||||
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
class LemonSqueezyClientTest extends TestCase
|
||||||
|
{
|
||||||
|
public function test_base_url_is_normalized_when_missing_scheme(): void
|
||||||
|
{
|
||||||
|
$this->app['config']->set('lemonsqueezy.api_key', 'test-token');
|
||||||
|
$this->app['config']->set('lemonsqueezy.base_url', '/v1');
|
||||||
|
|
||||||
|
Http::fake(function ($request) {
|
||||||
|
$this->assertSame('https://api.lemonsqueezy.com/v1/products', $request->url());
|
||||||
|
|
||||||
|
return Http::response(['data' => []], 200);
|
||||||
|
});
|
||||||
|
|
||||||
|
$client = $this->app->make(LemonSqueezyClient::class);
|
||||||
|
$client->get('/products');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_base_url_is_forced_to_api_when_pointing_to_local_host(): void
|
||||||
|
{
|
||||||
|
$this->app['config']->set('lemonsqueezy.api_key', 'test-token');
|
||||||
|
$this->app['config']->set('lemonsqueezy.base_url', 'http://localhost/v1');
|
||||||
|
|
||||||
|
Http::fake(function ($request) {
|
||||||
|
$this->assertSame('https://api.lemonsqueezy.com/v1/products', $request->url());
|
||||||
|
|
||||||
|
return Http::response(['data' => []], 200);
|
||||||
|
});
|
||||||
|
|
||||||
|
$client = $this->app->make(LemonSqueezyClient::class);
|
||||||
|
$client->get('/products');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,31 +5,46 @@ const demoTenantCredentials = {
|
|||||||
password: process.env.E2E_DEMO_TENANT_PASSWORD ?? 'Demo1234!',
|
password: process.env.E2E_DEMO_TENANT_PASSWORD ?? 'Demo1234!',
|
||||||
};
|
};
|
||||||
|
|
||||||
test.describe('Checkout Payment Step – Lemon Squeezy flow', () => {
|
test.describe('Checkout Payment Step - PayPal flow', () => {
|
||||||
test('opens Lemon Squeezy checkout and shows success notice', async ({ page }) => {
|
test('creates PayPal order and completes capture', async ({ page }) => {
|
||||||
await page.route('**/lemonsqueezy/create-checkout', async (route) => {
|
let createPayload: Record<string, unknown> | null = null;
|
||||||
|
let capturePayload: Record<string, unknown> | null = null;
|
||||||
|
|
||||||
|
await page.route('**/paypal/create-order', async (route) => {
|
||||||
|
createPayload = route.request().postDataJSON() as Record<string, unknown>;
|
||||||
await route.fulfill({
|
await route.fulfill({
|
||||||
status: 200,
|
status: 200,
|
||||||
contentType: 'application/json',
|
contentType: 'application/json',
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
checkout_url: 'https://fotospiel.lemonsqueezy.com/checkout/success',
|
order_id: 'order_test_123',
|
||||||
|
checkout_session_id: 'session_test_123',
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
await page.route('https://app.lemonsqueezy.com/js/lemon.js', async (route) => {
|
await page.route('**/paypal/capture-order', async (route) => {
|
||||||
|
capturePayload = route.request().postDataJSON() as Record<string, unknown>;
|
||||||
|
await route.fulfill({
|
||||||
|
status: 200,
|
||||||
|
contentType: 'application/json',
|
||||||
|
body: JSON.stringify({
|
||||||
|
status: 'completed',
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.route('https://www.paypal.com/sdk/js**', async (route) => {
|
||||||
await route.fulfill({
|
await route.fulfill({
|
||||||
status: 200,
|
status: 200,
|
||||||
contentType: 'application/javascript',
|
contentType: 'application/javascript',
|
||||||
body: `
|
body: `
|
||||||
window.createLemonSqueezy = function() {};
|
window.paypal = {
|
||||||
window.LemonSqueezy = {
|
Buttons: function(options) {
|
||||||
Setup: function(options) { window.__lemonEventHandler = options?.eventHandler || null; },
|
window.__paypalOptions = options;
|
||||||
Url: {
|
return {
|
||||||
Open: function(url) {
|
render: function() { return Promise.resolve(); },
|
||||||
window.__lemonOpenedUrl = url;
|
};
|
||||||
}
|
},
|
||||||
}
|
|
||||||
};
|
};
|
||||||
`,
|
`,
|
||||||
});
|
});
|
||||||
@@ -38,58 +53,27 @@ test.describe('Checkout Payment Step – Lemon Squeezy flow', () => {
|
|||||||
await openCheckoutPaymentStep(page, demoTenantCredentials);
|
await openCheckoutPaymentStep(page, demoTenantCredentials);
|
||||||
await acceptCheckoutTerms(page);
|
await acceptCheckoutTerms(page);
|
||||||
|
|
||||||
await page.evaluate(() => {
|
await expect.poll(async () => {
|
||||||
window.__openedUrls = [];
|
return page.evaluate(() => Boolean(window.__paypalOptions));
|
||||||
window.open = (url: string, target?: string | null, features?: string | null) => {
|
}).toBe(true);
|
||||||
window.__openedUrls.push({ url, target: target ?? null, features: features ?? null });
|
|
||||||
return null;
|
const orderId = await page.evaluate(async () => {
|
||||||
};
|
return window.__paypalOptions?.createOrder?.();
|
||||||
});
|
});
|
||||||
|
|
||||||
await page.getByRole('button', { name: /Continue with Lemon Squeezy|Weiter mit Lemon Squeezy/ }).first().click();
|
expect(orderId).toBe('order_test_123');
|
||||||
|
await expect(page.getByText(/PayPal-Checkout ist bereit|PayPal checkout is ready/i)).toBeVisible();
|
||||||
|
|
||||||
await expect(
|
await page.evaluate(async () => {
|
||||||
page.locator(
|
await window.__paypalOptions?.onApprove?.({ orderID: 'order_test_123' });
|
||||||
'text=/secure overlay|Overlay|neuen Tab|new tab/i'
|
});
|
||||||
)
|
|
||||||
).toBeVisible();
|
|
||||||
|
|
||||||
let mode: 'inline' | 'hosted' | null = null;
|
expect(createPayload?.package_id).toBeDefined();
|
||||||
for (let i = 0; i < 8; i++) {
|
expect(capturePayload?.order_id).toBe('order_test_123');
|
||||||
const state = await page.evaluate(() => ({
|
|
||||||
inline: Boolean(window.__lemonOpenedUrl),
|
|
||||||
opened: window.__openedUrls?.length ?? 0,
|
|
||||||
}));
|
|
||||||
|
|
||||||
if (state.inline) {
|
|
||||||
mode = 'inline';
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (state.opened > 0) {
|
|
||||||
mode = 'hosted';
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
await page.waitForTimeout(300);
|
|
||||||
}
|
|
||||||
|
|
||||||
expect(mode).not.toBeNull();
|
|
||||||
|
|
||||||
if (mode === 'inline') {
|
|
||||||
const inlineUrl = await page.evaluate(() => window.__lemonOpenedUrl ?? null);
|
|
||||||
expect(inlineUrl).not.toBeNull();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mode === 'hosted') {
|
|
||||||
await expect.poll(async () => {
|
|
||||||
return page.evaluate(() => window.__openedUrls?.[0]?.url ?? null);
|
|
||||||
}).toContain('lemonsqueezy');
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('shows error state when Lemon Squeezy checkout creation fails', async ({ page }) => {
|
test('shows error state when PayPal checkout creation fails', async ({ page }) => {
|
||||||
await page.route('**/lemonsqueezy/create-checkout', async (route) => {
|
await page.route('**/paypal/create-order', async (route) => {
|
||||||
await route.fulfill({
|
await route.fulfill({
|
||||||
status: 500,
|
status: 500,
|
||||||
contentType: 'application/json',
|
contentType: 'application/json',
|
||||||
@@ -97,19 +81,18 @@ test.describe('Checkout Payment Step – Lemon Squeezy flow', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
await page.route('https://app.lemonsqueezy.com/js/lemon.js', async (route) => {
|
await page.route('https://www.paypal.com/sdk/js**', async (route) => {
|
||||||
await route.fulfill({
|
await route.fulfill({
|
||||||
status: 200,
|
status: 200,
|
||||||
contentType: 'application/javascript',
|
contentType: 'application/javascript',
|
||||||
body: `
|
body: `
|
||||||
window.createLemonSqueezy = function() {};
|
window.paypal = {
|
||||||
window.LemonSqueezy = {
|
Buttons: function(options) {
|
||||||
Setup: function(options) { window.__lemonEventHandler = options?.eventHandler || null; },
|
window.__paypalOptions = options;
|
||||||
Url: {
|
return {
|
||||||
Open: function() {
|
render: function() { return Promise.resolve(); },
|
||||||
throw new Error('forced Lemon Squeezy failure');
|
};
|
||||||
}
|
},
|
||||||
}
|
|
||||||
};
|
};
|
||||||
`,
|
`,
|
||||||
});
|
});
|
||||||
@@ -118,10 +101,20 @@ test.describe('Checkout Payment Step – Lemon Squeezy flow', () => {
|
|||||||
await openCheckoutPaymentStep(page, demoTenantCredentials);
|
await openCheckoutPaymentStep(page, demoTenantCredentials);
|
||||||
await acceptCheckoutTerms(page);
|
await acceptCheckoutTerms(page);
|
||||||
|
|
||||||
await page.getByRole('button', { name: /Continue with Lemon Squeezy|Weiter mit Lemon Squeezy/ }).first().click();
|
await expect.poll(async () => {
|
||||||
|
return page.evaluate(() => Boolean(window.__paypalOptions));
|
||||||
|
}).toBe(true);
|
||||||
|
|
||||||
|
await page.evaluate(async () => {
|
||||||
|
try {
|
||||||
|
await window.__paypalOptions?.createOrder?.();
|
||||||
|
} catch {
|
||||||
|
// swallow error for assertion below
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
page.locator('text=/Lemon Squeezy-Checkout konnte nicht gestartet werden|Lemon Squeezy checkout could not be started/i')
|
page.locator('text=/PayPal-Checkout konnte nicht gestartet werden|PayPal checkout could not be started/i')
|
||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -168,8 +161,9 @@ async function acceptCheckoutTerms(page: import('@playwright/test').Page) {
|
|||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
__openedUrls?: Array<{ url: string; target?: string | null; features?: string | null }>;
|
__paypalOptions?: {
|
||||||
__lemonOpenedUrl?: string | null;
|
createOrder?: () => Promise<string>;
|
||||||
__lemonEventHandler?: ((event: { event: string; data?: unknown }) => void) | null;
|
onApprove?: (data: { orderID: string }) => Promise<void>;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,38 +0,0 @@
|
|||||||
import { expect, test } from '@playwright/test';
|
|
||||||
|
|
||||||
const shouldRun = process.env.E2E_LEMONSQUEEZY_SANDBOX === '1' || process.env.E2E_PADDLE_SANDBOX === '1';
|
|
||||||
|
|
||||||
test.describe('Lemon Squeezy sandbox checkout (staging)', () => {
|
|
||||||
test.skip(!shouldRun, 'Set E2E_LEMONSQUEEZY_SANDBOX=1 to run live sandbox checkout on staging.');
|
|
||||||
|
|
||||||
test('creates Lemon Squeezy checkout session from packages page', async ({ page }) => {
|
|
||||||
const base = process.env.E2E_BASE_URL ?? 'https://test-y0k0.fotospiel.app';
|
|
||||||
|
|
||||||
await page.goto(`${base}/packages`);
|
|
||||||
|
|
||||||
const acceptCookies = page.getByRole('button', { name: /akzeptieren|accept/i });
|
|
||||||
if (await acceptCookies.isVisible()) {
|
|
||||||
await acceptCookies.click();
|
|
||||||
}
|
|
||||||
|
|
||||||
const checkoutButtons = page.locator('a:has-text("Paket") , a:has-text("Checkout"), a:has-text("Jetzt"), button:has-text("Checkout")');
|
|
||||||
const count = await checkoutButtons.count();
|
|
||||||
|
|
||||||
if (count === 0) {
|
|
||||||
test.skip('No checkout CTA found on packages page');
|
|
||||||
}
|
|
||||||
|
|
||||||
const [requestPromise] = await Promise.all([
|
|
||||||
page.waitForRequest('**/lemonsqueezy/create-checkout'),
|
|
||||||
checkoutButtons.first().click(),
|
|
||||||
]);
|
|
||||||
|
|
||||||
const checkoutRequest = await requestPromise.response();
|
|
||||||
expect(checkoutRequest, 'Expected lemonsqueezy/create-checkout request to resolve').toBeTruthy();
|
|
||||||
expect(checkoutRequest!.status()).toBeLessThan(400);
|
|
||||||
|
|
||||||
const body = await checkoutRequest!.json();
|
|
||||||
const checkoutUrl = body.checkout_url ?? body.url ?? '';
|
|
||||||
expect(checkoutUrl).toContain('lemonsqueezy');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,433 +0,0 @@
|
|||||||
import type { Page } from '@playwright/test';
|
|
||||||
import fs from 'node:fs/promises';
|
|
||||||
|
|
||||||
import { dismissConsentBanner, expectFixture as expect, test } from '../helpers/test-fixtures';
|
|
||||||
|
|
||||||
const shouldRun = process.env.E2E_LEMONSQUEEZY_SANDBOX === '1' || process.env.E2E_PADDLE_SANDBOX === '1';
|
|
||||||
const baseUrl = process.env.E2E_BASE_URL ?? 'https://test-y0k0.fotospiel.app';
|
|
||||||
const locale = process.env.E2E_LOCALE ?? 'de';
|
|
||||||
const checkoutSlug = locale === 'en' ? 'checkout' : 'bestellen';
|
|
||||||
const tenantEmail = buildTenantEmail();
|
|
||||||
const tenantPassword = process.env.E2E_TENANT_PASSWORD ?? null;
|
|
||||||
const sandboxCard = {
|
|
||||||
number: process.env.E2E_LEMONSQUEEZY_CARD_NUMBER ?? process.env.E2E_PADDLE_CARD_NUMBER ?? '4242 4242 4242 4242',
|
|
||||||
expiry: process.env.E2E_LEMONSQUEEZY_CARD_EXPIRY ?? process.env.E2E_PADDLE_CARD_EXPIRY ?? '12/34',
|
|
||||||
cvc: process.env.E2E_LEMONSQUEEZY_CARD_CVC ?? process.env.E2E_PADDLE_CARD_CVC ?? '123',
|
|
||||||
name: process.env.E2E_LEMONSQUEEZY_CARD_NAME ?? process.env.E2E_PADDLE_CARD_NAME ?? 'Playwright Tester',
|
|
||||||
postal: process.env.E2E_LEMONSQUEEZY_CARD_POSTAL ?? process.env.E2E_PADDLE_CARD_POSTAL ?? '10115',
|
|
||||||
};
|
|
||||||
|
|
||||||
test.use({
|
|
||||||
channel: process.env.E2E_BROWSER_CHANNEL ?? 'chrome',
|
|
||||||
userAgent:
|
|
||||||
process.env.E2E_USER_AGENT ??
|
|
||||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36',
|
|
||||||
launchOptions: {
|
|
||||||
args: ['--disable-blink-features=AutomationControlled'],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
test.describe('Lemon Squeezy sandbox full flow (staging)', () => {
|
|
||||||
test.skip(!shouldRun, 'Set E2E_LEMONSQUEEZY_SANDBOX=1 to run live sandbox checkout on staging.');
|
|
||||||
test.skip(!tenantEmail || !tenantPassword, 'Set E2E_TENANT_EMAIL and E2E_TENANT_PASSWORD for sandbox flow.');
|
|
||||||
|
|
||||||
test('register, pay via Lemon Squeezy sandbox, and login to event admin', async ({ page, request }, testInfo) => {
|
|
||||||
const lemonsqueezyNetworkLog: string[] = [];
|
|
||||||
|
|
||||||
page.on('response', async (response) => {
|
|
||||||
const url = response.url();
|
|
||||||
if (!/lemonsqueezy|lemon/i.test(url)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const status = response.status();
|
|
||||||
let bodySnippet = '';
|
|
||||||
if (status >= 400 || /checkout|error/i.test(url)) {
|
|
||||||
try {
|
|
||||||
const text = await response.text();
|
|
||||||
bodySnippet = text.trim().slice(0, 2000);
|
|
||||||
} catch {
|
|
||||||
bodySnippet = '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const entry = [`[${status}] ${url}`, bodySnippet].filter(Boolean).join('\n');
|
|
||||||
lemonsqueezyNetworkLog.push(entry);
|
|
||||||
if (lemonsqueezyNetworkLog.length > 40) {
|
|
||||||
lemonsqueezyNetworkLog.shift();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
await page.addInitScript(() => {
|
|
||||||
Object.defineProperty(navigator, 'webdriver', {
|
|
||||||
get: () => undefined,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Jump directly into wizard for Classic package (2)
|
|
||||||
await page.goto(`${baseUrl}/${locale}/${checkoutSlug}/2`);
|
|
||||||
|
|
||||||
await dismissConsentBanner(page);
|
|
||||||
|
|
||||||
await proceedToAccountStep(page);
|
|
||||||
|
|
||||||
await completeRegistrationOrLogin(page, {
|
|
||||||
email: tenantEmail!,
|
|
||||||
password: tenantPassword!,
|
|
||||||
});
|
|
||||||
|
|
||||||
const termsCheckbox = page.locator('#checkout-terms-hero');
|
|
||||||
await expect(termsCheckbox).toBeVisible();
|
|
||||||
await termsCheckbox.click();
|
|
||||||
|
|
||||||
const checkoutCta = page.getByRole('button', { name: /Weiter mit Lemon Squeezy|Continue with Lemon Squeezy/i }).first();
|
|
||||||
await expect(checkoutCta).toBeVisible({ timeout: 20000 });
|
|
||||||
|
|
||||||
const [apiResponse] = await Promise.all([
|
|
||||||
page.waitForResponse((resp) => resp.url().includes('/lemonsqueezy/create-checkout') && resp.status() < 500),
|
|
||||||
checkoutCta.click(),
|
|
||||||
]);
|
|
||||||
|
|
||||||
const rawBody = await apiResponse.text();
|
|
||||||
let checkoutPayload: Record<string, unknown> | null = null;
|
|
||||||
try {
|
|
||||||
checkoutPayload = rawBody && rawBody.trim().startsWith('{') ? JSON.parse(rawBody) : null;
|
|
||||||
} catch {
|
|
||||||
checkoutPayload = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const inlineMode = checkoutPayload?.mode === 'inline' || checkoutPayload?.inline === true;
|
|
||||||
const checkoutUrl = extractCheckoutUrl(checkoutPayload, rawBody);
|
|
||||||
|
|
||||||
if (!inlineMode) {
|
|
||||||
expect(checkoutUrl).toContain('lemonsqueezy');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Navigate to Lemon Squeezy hosted checkout and complete payment.
|
|
||||||
if (inlineMode) {
|
|
||||||
await expect(
|
|
||||||
page.getByText(/Lemon Squeezy|Checkout geöffnet|Checkout opened/i).first()
|
|
||||||
).toBeVisible({ timeout: 20_000 });
|
|
||||||
await waitForLemonCardInputs(page, ['input[autocomplete="cc-number"]', 'input[name="cardnumber"]', 'input[name="card_number"]']);
|
|
||||||
} else if (checkoutUrl) {
|
|
||||||
await page.goto(checkoutUrl);
|
|
||||||
await expect(page).toHaveURL(/lemonsqueezy/);
|
|
||||||
} else {
|
|
||||||
throw new Error(`Missing Lemon Squeezy checkout URL. Response: ${rawBody}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
await completeHostedLemonSqueezyCheckout(page, sandboxCard);
|
|
||||||
|
|
||||||
await expect
|
|
||||||
.poll(
|
|
||||||
async () => {
|
|
||||||
const latestCompleted = await request.get('/api/_testing/checkout/sessions/latest', {
|
|
||||||
params: { status: 'completed', email: tenantEmail },
|
|
||||||
});
|
|
||||||
if (!latestCompleted.ok()) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const json = await latestCompleted.json();
|
|
||||||
return json?.data?.status ?? null;
|
|
||||||
},
|
|
||||||
{ timeout: 120_000 }
|
|
||||||
)
|
|
||||||
.toBe('completed');
|
|
||||||
|
|
||||||
await page.goto(`${baseUrl}/event-admin/login`);
|
|
||||||
await dismissConsentBanner(page);
|
|
||||||
|
|
||||||
await page.locator('input[name="identifier"]').fill(tenantEmail!);
|
|
||||||
await page.locator('input[name="password"]').fill(tenantPassword!);
|
|
||||||
await page.getByRole('button', { name: /Anmelden|Login/i }).click();
|
|
||||||
|
|
||||||
await expect(page).toHaveURL(/\/event-admin\/mobile\/(dashboard|welcome)/i, { timeout: 30_000 });
|
|
||||||
await expect(page.getByText(/Dashboard|Willkommen/i)).toBeVisible();
|
|
||||||
} finally {
|
|
||||||
if (lemonsqueezyNetworkLog.length > 0) {
|
|
||||||
const logPath = testInfo.outputPath('lemonsqueezy-network-log.txt');
|
|
||||||
await fs.writeFile(logPath, lemonsqueezyNetworkLog.join('\n\n'), 'utf8');
|
|
||||||
await testInfo.attach('lemonsqueezy-network-log', {
|
|
||||||
path: logPath,
|
|
||||||
contentType: 'text/plain',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
async function completeRegistrationOrLogin(page: Page, credentials: { email: string; password: string }): Promise<void> {
|
|
||||||
const identifierInput = page.locator('input[name="identifier"]');
|
|
||||||
if (await identifierInput.isVisible()) {
|
|
||||||
await identifierInput.fill(credentials.email);
|
|
||||||
await page.locator('input[name="password"]').fill(credentials.password);
|
|
||||||
await page.getByRole('button', { name: /^Anmelden$|Login/i }).last().click();
|
|
||||||
await expect(page.getByPlaceholder(/Gutscheincode|Coupon/i)).toBeVisible({ timeout: 20_000 });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await page.fill('input[name="first_name"]', 'Play');
|
|
||||||
await page.fill('input[name="last_name"]', 'Wright');
|
|
||||||
await page.fill('input[name="email"]', credentials.email);
|
|
||||||
const addressInput = page.locator('input[name="address"]');
|
|
||||||
if (await addressInput.isVisible()) {
|
|
||||||
await addressInput.fill('Teststrasse 1, 12345 Berlin');
|
|
||||||
}
|
|
||||||
|
|
||||||
const phoneInput = page.locator('input[name="phone"]');
|
|
||||||
if (await phoneInput.isVisible()) {
|
|
||||||
await phoneInput.fill('+49123456789');
|
|
||||||
}
|
|
||||||
|
|
||||||
const usernameInput = page.locator('input[name="username"]');
|
|
||||||
if (await usernameInput.isVisible()) {
|
|
||||||
await usernameInput.fill(credentials.email);
|
|
||||||
}
|
|
||||||
await page.fill('input[name="password"]', credentials.password);
|
|
||||||
await page.fill('input[name="password_confirmation"]', credentials.password);
|
|
||||||
|
|
||||||
await page.check('input[name="privacy_consent"]');
|
|
||||||
await page.getByRole('button', { name: /Registrieren|Register/i }).last().click();
|
|
||||||
|
|
||||||
try {
|
|
||||||
await expect(page.getByPlaceholder(/Gutscheincode|Coupon/i)).toBeVisible({ timeout: 20_000 });
|
|
||||||
return;
|
|
||||||
} catch {
|
|
||||||
const loginButton = page.getByRole('button', { name: /^Anmelden$|Login/i }).first();
|
|
||||||
if (await loginButton.isVisible()) {
|
|
||||||
await loginButton.click();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await expect(page.locator('input[name="identifier"]')).toBeVisible({ timeout: 10_000 });
|
|
||||||
await page.locator('input[name="identifier"]').fill(credentials.email);
|
|
||||||
await page.locator('input[name="password"]').fill(credentials.password);
|
|
||||||
await page.getByRole('button', { name: /^Anmelden$|Login/i }).last().click();
|
|
||||||
await expect(page.getByPlaceholder(/Gutscheincode|Coupon/i)).toBeVisible({ timeout: 20_000 });
|
|
||||||
}
|
|
||||||
|
|
||||||
async function proceedToAccountStep(page: Page, timeoutMs = 30_000): Promise<void> {
|
|
||||||
const deadline = Date.now() + timeoutMs;
|
|
||||||
|
|
||||||
while (Date.now() < deadline) {
|
|
||||||
const accountForm = page.locator('input[name="first_name"], input[name="identifier"]');
|
|
||||||
if (await accountForm.isVisible()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await dismissConsentBanner(page);
|
|
||||||
|
|
||||||
const continueButton = page.getByRole('button', { name: /Weiter|Continue/i }).first();
|
|
||||||
if (await continueButton.isVisible()) {
|
|
||||||
if (await continueButton.isEnabled()) {
|
|
||||||
await continueButton.click();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await page.waitForTimeout(500);
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error('Account step did not load in time.');
|
|
||||||
}
|
|
||||||
|
|
||||||
async function completeHostedLemonSqueezyCheckout(
|
|
||||||
page: Page,
|
|
||||||
card: { number: string; expiry: string; cvc: string; name: string; postal: string }
|
|
||||||
): Promise<void> {
|
|
||||||
const cardNumberSelectors = [
|
|
||||||
'input[autocomplete="cc-number"]',
|
|
||||||
'input[name="cardnumber"]',
|
|
||||||
'input[name="card_number"]',
|
|
||||||
];
|
|
||||||
const expirySelectors = [
|
|
||||||
'input[autocomplete="cc-exp"]',
|
|
||||||
'input[name="exp-date"]',
|
|
||||||
'input[name="exp_date"]',
|
|
||||||
'input[name="expiry"]',
|
|
||||||
];
|
|
||||||
const cvcSelectors = [
|
|
||||||
'input[autocomplete="cc-csc"]',
|
|
||||||
'input[name="cvc"]',
|
|
||||||
'input[name="security-code"]',
|
|
||||||
'input[name="cvv"]',
|
|
||||||
];
|
|
||||||
const nameSelectors = [
|
|
||||||
'input[autocomplete="cc-name"]',
|
|
||||||
'input[name="cardholder"]',
|
|
||||||
'input[name="cardholder_name"]',
|
|
||||||
'input[name="cardholder-name"]',
|
|
||||||
];
|
|
||||||
const postalSelectors = [
|
|
||||||
'input[autocomplete="postal-code"]',
|
|
||||||
'input[name="postal"]',
|
|
||||||
'input[name="postal_code"]',
|
|
||||||
'input[name="zip"]',
|
|
||||||
];
|
|
||||||
|
|
||||||
await maybeFillInAnyFrame(page, nameSelectors, card.name);
|
|
||||||
await fillInAnyFrame(page, cardNumberSelectors, card.number);
|
|
||||||
await fillInAnyFrame(page, expirySelectors, card.expiry);
|
|
||||||
await fillInAnyFrame(page, cvcSelectors, card.cvc);
|
|
||||||
await maybeFillInAnyFrame(page, postalSelectors, card.postal);
|
|
||||||
|
|
||||||
const payButton = page.getByRole('button', {
|
|
||||||
name: /Jetzt bezahlen|Pay now|Complete order|Order now|Kaufen|Bezahlen|Zahlung abschließen/i,
|
|
||||||
}).first();
|
|
||||||
await expect(payButton).toBeVisible({ timeout: 20_000 });
|
|
||||||
await payButton.click();
|
|
||||||
}
|
|
||||||
|
|
||||||
async function waitForLemonCardInputs(page: Page, selectors: string[], timeoutMs = 30_000): Promise<void> {
|
|
||||||
const deadline = Date.now() + timeoutMs;
|
|
||||||
|
|
||||||
while (Date.now() < deadline) {
|
|
||||||
if (await hasAnySelector(page, selectors)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (await hasAnyText(page, /Something went wrong|try again later/i)) {
|
|
||||||
throw new Error('Lemon Squeezy inline checkout returned an error in the iframe.');
|
|
||||||
}
|
|
||||||
|
|
||||||
await page.waitForTimeout(500);
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error('Lemon Squeezy card inputs did not appear in time.');
|
|
||||||
}
|
|
||||||
|
|
||||||
async function hasAnySelector(page: Page, selectors: string[]): Promise<boolean> {
|
|
||||||
if (await hasSelectorInFrame(page, selectors)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const frame of page.frames()) {
|
|
||||||
if (await hasSelectorInFrame(frame, selectors)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function hasSelectorInFrame(scope: Page | import('@playwright/test').Frame, selectors: string[]): Promise<boolean> {
|
|
||||||
for (const selector of selectors) {
|
|
||||||
if ((await scope.locator(selector).count()) > 0) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function hasAnyText(page: Page, matcher: RegExp): Promise<boolean> {
|
|
||||||
if ((await page.getByText(matcher).count()) > 0) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const frame of page.frames()) {
|
|
||||||
if ((await frame.getByText(matcher).count()) > 0) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
function extractCheckoutUrl(payload: Record<string, unknown> | null, rawBody: string): string | null {
|
|
||||||
if (payload) {
|
|
||||||
const directUrl = payload.checkout_url ?? payload.url ?? payload.checkoutUrl;
|
|
||||||
if (typeof directUrl === 'string') {
|
|
||||||
return directUrl;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const trimmed = rawBody.trim();
|
|
||||||
if (/^https?:\/\//i.test(trimmed)) {
|
|
||||||
return trimmed;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (trimmed.startsWith('<')) {
|
|
||||||
const match = trimmed.match(/https?:\/\/["'a-zA-Z0-9._~:/?#@!$&'()*+,;=%-]+/);
|
|
||||||
if (match) {
|
|
||||||
return match[0];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildTenantEmail(): string | null {
|
|
||||||
const rawEmail = process.env.E2E_TENANT_EMAIL ?? process.env.E2E_LEMONSQUEEZY_EMAIL ?? process.env.E2E_PADDLE_EMAIL ?? null;
|
|
||||||
if (!rawEmail) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!rawEmail.includes('{timestamp}')) {
|
|
||||||
return rawEmail;
|
|
||||||
}
|
|
||||||
|
|
||||||
const timestamp = Math.floor(Date.now() / 1000).toString();
|
|
||||||
return rawEmail.replaceAll('{timestamp}', timestamp);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function fillInAnyFrame(page: Page, selectors: string[], value: string): Promise<void> {
|
|
||||||
const filled = await attemptFill(page, selectors, value);
|
|
||||||
if (filled) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const frames = page.frames();
|
|
||||||
for (const frame of frames) {
|
|
||||||
const frameFilled = await attemptFill(frame, selectors, value);
|
|
||||||
if (frameFilled) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error(`Unable to find input for selectors: ${selectors.join(', ')}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function maybeFillInAnyFrame(page: Page, selectors: string[], value: string): Promise<void> {
|
|
||||||
const filled = await attemptFill(page, selectors, value);
|
|
||||||
if (filled) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const frames = page.frames();
|
|
||||||
for (const frame of frames) {
|
|
||||||
const frameFilled = await attemptFill(frame, selectors, value);
|
|
||||||
if (frameFilled) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function attemptFill(
|
|
||||||
scope: Page | import('@playwright/test').Frame,
|
|
||||||
selectors: string[],
|
|
||||||
value: string
|
|
||||||
): Promise<boolean> {
|
|
||||||
for (const selector of selectors) {
|
|
||||||
const locator = scope.locator(selector).first();
|
|
||||||
if ((await locator.count()) === 0) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await locator.fill(value);
|
|
||||||
return true;
|
|
||||||
} catch {
|
|
||||||
try {
|
|
||||||
await locator.click();
|
|
||||||
await locator.type(value, { delay: 25 });
|
|
||||||
return true;
|
|
||||||
} catch {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
109
tests/ui/purchase/paypal-sandbox-checkout.test.ts
Normal file
109
tests/ui/purchase/paypal-sandbox-checkout.test.ts
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
import { expect, test } from '@playwright/test';
|
||||||
|
|
||||||
|
const shouldRun =
|
||||||
|
process.env.E2E_PAYPAL_SANDBOX === '1' ||
|
||||||
|
process.env.E2E_LEMONSQUEEZY_SANDBOX === '1' ||
|
||||||
|
process.env.E2E_PADDLE_SANDBOX === '1';
|
||||||
|
|
||||||
|
const base = process.env.E2E_BASE_URL ?? 'https://test-y0k0.fotospiel.app';
|
||||||
|
const locale = process.env.E2E_LOCALE ?? 'de';
|
||||||
|
const checkoutSlug = locale === 'en' ? 'checkout' : 'bestellen';
|
||||||
|
const tenantEmail = process.env.E2E_TENANT_EMAIL ?? null;
|
||||||
|
const tenantPassword = process.env.E2E_TENANT_PASSWORD ?? null;
|
||||||
|
|
||||||
|
test.describe('PayPal sandbox checkout (staging)', () => {
|
||||||
|
test.skip(!shouldRun, 'Set E2E_PAYPAL_SANDBOX=1 to run sandbox checkout on staging.');
|
||||||
|
test.skip(!tenantEmail || !tenantPassword, 'Set E2E_TENANT_EMAIL and E2E_TENANT_PASSWORD for sandbox flow.');
|
||||||
|
|
||||||
|
test('creates PayPal order from the checkout wizard', async ({ 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(options) {
|
||||||
|
window.__paypalOptions = options;
|
||||||
|
return {
|
||||||
|
render: function() { return Promise.resolve(); },
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
`,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.route('**/paypal/create-order', async (route) => {
|
||||||
|
await route.fulfill({
|
||||||
|
status: 200,
|
||||||
|
contentType: 'application/json',
|
||||||
|
body: JSON.stringify({
|
||||||
|
order_id: 'order_test_123',
|
||||||
|
checkout_session_id: 'session_test_123',
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.goto(`${base}/${locale}/${checkoutSlug}/2`);
|
||||||
|
|
||||||
|
await proceedToAccountStep(page);
|
||||||
|
await login(page, { email: tenantEmail!, password: tenantPassword! });
|
||||||
|
|
||||||
|
const termsCheckbox = page.locator('#checkout-terms-hero');
|
||||||
|
await expect(termsCheckbox).toBeVisible();
|
||||||
|
await termsCheckbox.click();
|
||||||
|
|
||||||
|
await expect.poll(async () => {
|
||||||
|
return page.evaluate(() => Boolean(window.__paypalOptions));
|
||||||
|
}).toBe(true);
|
||||||
|
|
||||||
|
const orderId = await page.evaluate(async () => {
|
||||||
|
return window.__paypalOptions?.createOrder?.();
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(orderId).toBe('order_test_123');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
async function proceedToAccountStep(page: import('@playwright/test').Page, timeoutMs = 30_000): Promise<void> {
|
||||||
|
const deadline = Date.now() + timeoutMs;
|
||||||
|
|
||||||
|
while (Date.now() < deadline) {
|
||||||
|
const accountForm = page.locator('input[name="first_name"], input[name="identifier"]');
|
||||||
|
if (await accountForm.isVisible()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const continueButton = page.getByRole('button', { name: /Weiter|Continue/i }).first();
|
||||||
|
if (await continueButton.isVisible()) {
|
||||||
|
if (await continueButton.isEnabled()) {
|
||||||
|
await continueButton.click();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await page.waitForTimeout(500);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error('Account step did not load in time.');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function login(page: import('@playwright/test').Page, credentials: { email: string; password: string }) {
|
||||||
|
const identifierInput = page.locator('input[name="identifier"]');
|
||||||
|
if (!(await identifierInput.isVisible())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await identifierInput.fill(credentials.email);
|
||||||
|
await page.locator('input[name="password"]').fill(credentials.password);
|
||||||
|
await page.getByRole('button', { name: /^Anmelden$|Login/i }).last().click();
|
||||||
|
|
||||||
|
await expect(page.getByPlaceholder(/Gutscheincode|Coupon/i)).toBeVisible({ timeout: 20_000 });
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
__paypalOptions?: {
|
||||||
|
createOrder?: () => Promise<string>;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
248
tests/ui/purchase/paypal-sandbox-full.test.ts
Normal file
248
tests/ui/purchase/paypal-sandbox-full.test.ts
Normal file
@@ -0,0 +1,248 @@
|
|||||||
|
import type { Page } from '@playwright/test';
|
||||||
|
import fs from 'node:fs/promises';
|
||||||
|
|
||||||
|
import { dismissConsentBanner, expectFixture as expect, test } from '../helpers/test-fixtures';
|
||||||
|
|
||||||
|
const shouldRun =
|
||||||
|
process.env.E2E_PAYPAL_SANDBOX === '1' ||
|
||||||
|
process.env.E2E_LEMONSQUEEZY_SANDBOX === '1' ||
|
||||||
|
process.env.E2E_PADDLE_SANDBOX === '1';
|
||||||
|
const baseUrl = process.env.E2E_BASE_URL ?? 'https://test-y0k0.fotospiel.app';
|
||||||
|
const locale = process.env.E2E_LOCALE ?? 'de';
|
||||||
|
const checkoutSlug = locale === 'en' ? 'checkout' : 'bestellen';
|
||||||
|
const tenantEmail = buildTenantEmail();
|
||||||
|
const tenantPassword = process.env.E2E_TENANT_PASSWORD ?? null;
|
||||||
|
|
||||||
|
test.use({
|
||||||
|
channel: process.env.E2E_BROWSER_CHANNEL ?? 'chrome',
|
||||||
|
userAgent:
|
||||||
|
process.env.E2E_USER_AGENT ??
|
||||||
|
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36',
|
||||||
|
launchOptions: {
|
||||||
|
args: ['--disable-blink-features=AutomationControlled'],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
test.describe('PayPal sandbox full flow (staging)', () => {
|
||||||
|
test.skip(!shouldRun, 'Set E2E_PAYPAL_SANDBOX=1 to run sandbox checkout on staging.');
|
||||||
|
test.skip(!tenantEmail || !tenantPassword, 'Set E2E_TENANT_EMAIL and E2E_TENANT_PASSWORD for sandbox flow.');
|
||||||
|
|
||||||
|
test('register, simulate PayPal completion, and login to event admin', async ({ page }, testInfo) => {
|
||||||
|
const paypalNetworkLog: string[] = [];
|
||||||
|
|
||||||
|
page.on('response', async (response) => {
|
||||||
|
const url = response.url();
|
||||||
|
if (!/paypal/i.test(url)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const status = response.status();
|
||||||
|
let bodySnippet = '';
|
||||||
|
if (status >= 400 || /checkout|error/i.test(url)) {
|
||||||
|
try {
|
||||||
|
const text = await response.text();
|
||||||
|
bodySnippet = text.trim().slice(0, 2000);
|
||||||
|
} catch {
|
||||||
|
bodySnippet = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const entry = [`[${status}] ${url}`, bodySnippet].filter(Boolean).join('
|
||||||
|
');
|
||||||
|
paypalNetworkLog.push(entry);
|
||||||
|
if (paypalNetworkLog.length > 40) {
|
||||||
|
paypalNetworkLog.shift();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.addInitScript(() => {
|
||||||
|
Object.defineProperty(navigator, 'webdriver', {
|
||||||
|
get: () => undefined,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.route('https://www.paypal.com/sdk/js**', async (route) => {
|
||||||
|
await route.fulfill({
|
||||||
|
status: 200,
|
||||||
|
contentType: 'application/javascript',
|
||||||
|
body: `
|
||||||
|
window.paypal = {
|
||||||
|
Buttons: function(options) {
|
||||||
|
window.__paypalOptions = options;
|
||||||
|
return {
|
||||||
|
render: function() { return Promise.resolve(); },
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
`,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.route('**/paypal/create-order', async (route) => {
|
||||||
|
await route.fulfill({
|
||||||
|
status: 200,
|
||||||
|
contentType: 'application/json',
|
||||||
|
body: JSON.stringify({
|
||||||
|
order_id: 'order_test_123',
|
||||||
|
checkout_session_id: 'session_test_123',
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.route('**/paypal/capture-order', async (route) => {
|
||||||
|
await route.fulfill({
|
||||||
|
status: 200,
|
||||||
|
contentType: 'application/json',
|
||||||
|
body: JSON.stringify({
|
||||||
|
status: 'completed',
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await page.goto(`${baseUrl}/${locale}/${checkoutSlug}/2`);
|
||||||
|
|
||||||
|
await dismissConsentBanner(page);
|
||||||
|
|
||||||
|
await proceedToAccountStep(page);
|
||||||
|
|
||||||
|
await completeRegistrationOrLogin(page, {
|
||||||
|
email: tenantEmail!,
|
||||||
|
password: tenantPassword!,
|
||||||
|
});
|
||||||
|
|
||||||
|
const termsCheckbox = page.locator('#checkout-terms-hero');
|
||||||
|
await expect(termsCheckbox).toBeVisible();
|
||||||
|
await termsCheckbox.click();
|
||||||
|
|
||||||
|
await expect.poll(async () => {
|
||||||
|
return page.evaluate(() => Boolean(window.__paypalOptions));
|
||||||
|
}).toBe(true);
|
||||||
|
|
||||||
|
await page.evaluate(async () => {
|
||||||
|
await window.__paypalOptions?.createOrder?.();
|
||||||
|
await window.__paypalOptions?.onApprove?.({ orderID: 'order_test_123' });
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(page.getByText(/Marketing-Dashboard/)).toBeVisible();
|
||||||
|
|
||||||
|
await page.goto(`${baseUrl}/event-admin/login`);
|
||||||
|
await dismissConsentBanner(page);
|
||||||
|
|
||||||
|
await page.locator('input[name="identifier"]').fill(tenantEmail!);
|
||||||
|
await page.locator('input[name="password"]').fill(tenantPassword!);
|
||||||
|
await page.getByRole('button', { name: /Anmelden|Login/i }).click();
|
||||||
|
|
||||||
|
await expect(page).toHaveURL(/\/event-admin\/mobile\/(dashboard|welcome)/i, { timeout: 30_000 });
|
||||||
|
await expect(page.getByText(/Dashboard|Willkommen/i)).toBeVisible();
|
||||||
|
} finally {
|
||||||
|
if (paypalNetworkLog.length > 0) {
|
||||||
|
const logPath = testInfo.outputPath('paypal-network-log.txt');
|
||||||
|
await fs.writeFile(logPath, paypalNetworkLog.join('
|
||||||
|
|
||||||
|
'), 'utf8');
|
||||||
|
await testInfo.attach('paypal-network-log', {
|
||||||
|
path: logPath,
|
||||||
|
contentType: 'text/plain',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
async function completeRegistrationOrLogin(page: Page, credentials: { email: string; password: string }): Promise<void> {
|
||||||
|
const identifierInput = page.locator('input[name="identifier"]');
|
||||||
|
if (await identifierInput.isVisible()) {
|
||||||
|
await identifierInput.fill(credentials.email);
|
||||||
|
await page.locator('input[name="password"]').fill(credentials.password);
|
||||||
|
await page.getByRole('button', { name: /^Anmelden$|Login/i }).last().click();
|
||||||
|
await expect(page.getByPlaceholder(/Gutscheincode|Coupon/i)).toBeVisible({ timeout: 20_000 });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await page.fill('input[name="first_name"]', 'Play');
|
||||||
|
await page.fill('input[name="last_name"]', 'Wright');
|
||||||
|
await page.fill('input[name="email"]', credentials.email);
|
||||||
|
const addressInput = page.locator('input[name="address"]');
|
||||||
|
if (await addressInput.isVisible()) {
|
||||||
|
await addressInput.fill('Teststrasse 1, 12345 Berlin');
|
||||||
|
}
|
||||||
|
|
||||||
|
const phoneInput = page.locator('input[name="phone"]');
|
||||||
|
if (await phoneInput.isVisible()) {
|
||||||
|
await phoneInput.fill('+49123456789');
|
||||||
|
}
|
||||||
|
|
||||||
|
const usernameInput = page.locator('input[name="username"]');
|
||||||
|
if (await usernameInput.isVisible()) {
|
||||||
|
await usernameInput.fill(credentials.email);
|
||||||
|
}
|
||||||
|
await page.fill('input[name="password"]', credentials.password);
|
||||||
|
await page.fill('input[name="password_confirmation"]', credentials.password);
|
||||||
|
|
||||||
|
await page.check('input[name="privacy_consent"]');
|
||||||
|
await page.getByRole('button', { name: /Registrieren|Register/i }).last().click();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await expect(page.getByPlaceholder(/Gutscheincode|Coupon/i)).toBeVisible({ timeout: 20_000 });
|
||||||
|
return;
|
||||||
|
} catch {
|
||||||
|
const loginButton = page.getByRole('button', { name: /^Anmelden$|Login/i }).first();
|
||||||
|
if (await loginButton.isVisible()) {
|
||||||
|
await loginButton.click();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await expect(page.locator('input[name="identifier"]')).toBeVisible({ timeout: 10_000 });
|
||||||
|
await page.locator('input[name="identifier"]').fill(credentials.email);
|
||||||
|
await page.locator('input[name="password"]').fill(credentials.password);
|
||||||
|
await page.getByRole('button', { name: /^Anmelden$|Login/i }).last().click();
|
||||||
|
await expect(page.getByPlaceholder(/Gutscheincode|Coupon/i)).toBeVisible({ timeout: 20_000 });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function proceedToAccountStep(page: Page, timeoutMs = 30_000): Promise<void> {
|
||||||
|
const deadline = Date.now() + timeoutMs;
|
||||||
|
|
||||||
|
while (Date.now() < deadline) {
|
||||||
|
const accountForm = page.locator('input[name="first_name"], input[name="identifier"]');
|
||||||
|
if (await accountForm.isVisible()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await dismissConsentBanner(page);
|
||||||
|
|
||||||
|
const continueButton = page.getByRole('button', { name: /Weiter|Continue/i }).first();
|
||||||
|
if (await continueButton.isVisible()) {
|
||||||
|
if (await continueButton.isEnabled()) {
|
||||||
|
await continueButton.click();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await page.waitForTimeout(500);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error('Account step did not load in time.');
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildTenantEmail(): string | null {
|
||||||
|
const rawEmail = process.env.E2E_TENANT_EMAIL ?? process.env.E2E_PAYPAL_EMAIL ?? process.env.E2E_LEMONSQUEEZY_EMAIL ?? process.env.E2E_PADDLE_EMAIL ?? null;
|
||||||
|
if (!rawEmail) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!rawEmail.includes('{timestamp}')) {
|
||||||
|
return rawEmail;
|
||||||
|
}
|
||||||
|
|
||||||
|
const timestamp = Math.floor(Date.now() / 1000).toString();
|
||||||
|
return rawEmail.replaceAll('{timestamp}', timestamp);
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
__paypalOptions?: {
|
||||||
|
createOrder?: () => Promise<string>;
|
||||||
|
onApprove?: (data: { orderID: string }) => Promise<void>;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,15 +2,12 @@ import { test, expectFixture as expect } from '../helpers/test-fixtures';
|
|||||||
|
|
||||||
const shouldRun = process.env.E2E_TESTING_API === '1';
|
const shouldRun = process.env.E2E_TESTING_API === '1';
|
||||||
|
|
||||||
test.describe('Classic package checkout with Lemon Squeezy completion', () => {
|
test.describe('Classic package checkout with PayPal completion', () => {
|
||||||
test.skip(!shouldRun, 'Set E2E_TESTING_API=1 to enable checkout tests that use /api/_testing endpoints.');
|
test.skip(!shouldRun, 'Set E2E_TESTING_API=1 to enable checkout tests that use /api/_testing endpoints.');
|
||||||
test('registers, applies coupon, and reaches confirmation', async ({
|
test('registers, applies coupon, and reaches confirmation', async ({
|
||||||
page,
|
page,
|
||||||
clearTestMailbox,
|
clearTestMailbox,
|
||||||
seedTestCoupons,
|
seedTestCoupons,
|
||||||
getLatestCheckoutSession,
|
|
||||||
simulateLemonSqueezyCompletion,
|
|
||||||
getTestMailbox,
|
|
||||||
}) => {
|
}) => {
|
||||||
await clearTestMailbox();
|
await clearTestMailbox();
|
||||||
await seedTestCoupons();
|
await seedTestCoupons();
|
||||||
@@ -18,44 +15,43 @@ test.describe('Classic package checkout with Lemon Squeezy completion', () => {
|
|||||||
const unique = Date.now();
|
const unique = Date.now();
|
||||||
const email = `checkout+${unique}@example.test`;
|
const email = `checkout+${unique}@example.test`;
|
||||||
const password = 'Password123!';
|
const password = 'Password123!';
|
||||||
await page.addInitScript(() => {
|
|
||||||
window.__openedWindows = [];
|
|
||||||
const originalOpen = window.open;
|
|
||||||
window.open = function (...args) {
|
|
||||||
window.__openedWindows.push(args);
|
|
||||||
return originalOpen?.apply(this, args) ?? null;
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
await page.route('https://app.lemonsqueezy.com/js/lemon.js', async (route) => {
|
await page.route('https://www.paypal.com/sdk/js**', async (route) => {
|
||||||
await route.fulfill({
|
await route.fulfill({
|
||||||
status: 200,
|
status: 200,
|
||||||
contentType: 'application/javascript',
|
contentType: 'application/javascript',
|
||||||
body: `
|
body: `
|
||||||
window.__lemonEventHandler = null;
|
window.paypal = {
|
||||||
window.__lemonOpenedUrl = null;
|
Buttons: function(options) {
|
||||||
window.LemonSqueezy = {
|
window.__paypalOptions = options;
|
||||||
Setup(options) {
|
return {
|
||||||
window.__lemonEventHandler = options?.eventHandler || null;
|
render: function() { return Promise.resolve(); },
|
||||||
},
|
};
|
||||||
Url: {
|
|
||||||
Open(url) {
|
|
||||||
window.__lemonOpenedUrl = url;
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
`,
|
`,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
let lemonsqueezyRequestPayload: Record<string, unknown> | null = null;
|
let paypalRequestPayload: Record<string, unknown> | null = null;
|
||||||
await page.route('**/lemonsqueezy/create-checkout', async (route) => {
|
await page.route('**/paypal/create-order', async (route) => {
|
||||||
lemonsqueezyRequestPayload = route.request().postDataJSON() as Record<string, unknown>;
|
paypalRequestPayload = route.request().postDataJSON() as Record<string, unknown>;
|
||||||
await route.fulfill({
|
await route.fulfill({
|
||||||
status: 200,
|
status: 200,
|
||||||
contentType: 'application/json',
|
contentType: 'application/json',
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
checkout_url: 'https://fotospiel.lemonsqueezy.com/checkout/abc123',
|
order_id: 'order_test_123',
|
||||||
|
checkout_session_id: 'session_test_123',
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.route('**/paypal/capture-order', async (route) => {
|
||||||
|
await route.fulfill({
|
||||||
|
status: 200,
|
||||||
|
contentType: 'application/json',
|
||||||
|
body: JSON.stringify({
|
||||||
|
status: 'completed',
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -113,85 +109,36 @@ test.describe('Classic package checkout with Lemon Squeezy completion', () => {
|
|||||||
await expect(termsCheckbox).toBeVisible();
|
await expect(termsCheckbox).toBeVisible();
|
||||||
await termsCheckbox.click();
|
await termsCheckbox.click();
|
||||||
|
|
||||||
await page.getByRole('button', { name: /Weiter mit Lemon Squeezy|Continue with Lemon Squeezy/i }).first().click();
|
await expect.poll(async () => {
|
||||||
|
return page.evaluate(() => Boolean(window.__paypalOptions));
|
||||||
|
}).toBe(true);
|
||||||
|
|
||||||
let checkoutMode: 'inline' | 'hosted' | null = null;
|
const orderId = await page.evaluate(async () => {
|
||||||
for (let i = 0; i < 8; i++) {
|
return window.__paypalOptions?.createOrder?.();
|
||||||
const state = await page.evaluate(() => ({
|
|
||||||
inline: Boolean(window.__lemonOpenedUrl),
|
|
||||||
opened: window.__openedWindows?.length ?? 0,
|
|
||||||
}));
|
|
||||||
|
|
||||||
if (state.inline) {
|
|
||||||
checkoutMode = 'inline';
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (state.opened > 0) {
|
|
||||||
checkoutMode = 'hosted';
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
await page.waitForTimeout(300);
|
|
||||||
}
|
|
||||||
|
|
||||||
expect(checkoutMode).not.toBeNull();
|
|
||||||
|
|
||||||
if (checkoutMode === 'hosted') {
|
|
||||||
await expect.poll(async () => {
|
|
||||||
return page.evaluate(() => window.__openedWindows?.[0]?.[0] ?? null);
|
|
||||||
}).toContain('https://fotospiel.lemonsqueezy.com/checkout/abc123');
|
|
||||||
}
|
|
||||||
|
|
||||||
await page.evaluate(() => {
|
|
||||||
window.__lemonEventHandler?.({
|
|
||||||
event: 'Checkout.Success',
|
|
||||||
data: { id: 'ord_test', attributes: { checkout_id: 'chk_123' } },
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
let session = null;
|
expect(orderId).toBe('order_test_123');
|
||||||
for (let i = 0; i < 6; i++) {
|
|
||||||
session = await getLatestCheckoutSession({ email });
|
|
||||||
if (session) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
await page.waitForTimeout(500);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (session) {
|
await page.evaluate(async () => {
|
||||||
await simulateLemonSqueezyCompletion(session.id);
|
await window.__paypalOptions?.onApprove?.({ orderID: 'order_test_123' });
|
||||||
|
});
|
||||||
for (let i = 0; i < 6; i++) {
|
|
||||||
const refreshed = await getLatestCheckoutSession({ email });
|
|
||||||
if (refreshed?.status === 'completed') {
|
|
||||||
session = refreshed;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
await page.waitForTimeout(500);
|
|
||||||
}
|
|
||||||
|
|
||||||
expect(session?.status).toBe('completed');
|
|
||||||
}
|
|
||||||
|
|
||||||
await expect(page.getByText(/Marketing-Dashboard/)).toBeVisible();
|
await expect(page.getByText(/Marketing-Dashboard/)).toBeVisible();
|
||||||
await expect(
|
await expect(
|
||||||
page.getByRole('button', { name: /Zum Admin-Bereich|To Admin Area/i })
|
page.getByRole('button', { name: /Zum Admin-Bereich|To Admin Area/i })
|
||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
|
|
||||||
if (lemonsqueezyRequestPayload) {
|
if (paypalRequestPayload) {
|
||||||
expect(lemonsqueezyRequestPayload['coupon_code']).toBe('PERCENT10');
|
expect(paypalRequestPayload['coupon_code']).toBe('PERCENT10');
|
||||||
}
|
}
|
||||||
|
|
||||||
const messages = await getTestMailbox();
|
|
||||||
expect(messages.length).toBeGreaterThan(0);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
__openedWindows?: unknown[];
|
__paypalOptions?: {
|
||||||
__lemonEventHandler?: ((event: { event: string; data?: unknown }) => void) | null;
|
createOrder?: () => Promise<string>;
|
||||||
__lemonOpenedUrl?: string | null;
|
onApprove?: (data: { orderID: string }) => Promise<void>;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user