diff --git a/angebotsuebersicht.html b/angebotsuebersicht.html
index 4cf1c87..06d0290 100644
--- a/angebotsuebersicht.html
+++ b/angebotsuebersicht.html
@@ -1,285 +1,34 @@
-
-
-
-
-
- Ernährungs-Pakete – Tri-Hub
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
TRI-HUB · ERNÄHRUNG
-
Unsere Ernährungs-Pakete
-
-
- Wähle das Paket, das zu deinem aktuellen Stand passt. Von den Grundlagen bis zur Wettkampf-Vorbereitung.
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+ Ernährungs-Pakete – Tri-Hub
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/package-lock.json b/package-lock.json
index 7e92738..eb86a80 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -10,6 +10,7 @@
"dependencies": {
"csv-parse": "^7.0.2",
"csv-stringify": "^6.8.3",
+ "libphonenumber-js": "^1.13.14",
"nodemailer": "^10.0.10"
},
"devDependencies": {
@@ -982,6 +983,12 @@
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
+ "node_modules/libphonenumber-js": {
+ "version": "1.13.14",
+ "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.13.14.tgz",
+ "integrity": "sha512-llihgCcx0BFLksecLP+x1J+6JDE1GsXS1RN/LoPF6qcwpeQcnjj0lcvZxY8AzbEpYwyZWPZW/nDuqkqzm3amiw==",
+ "license": "MIT"
+ },
"node_modules/nanoid": {
"version": "3.3.19",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.19.tgz",
diff --git a/package.json b/package.json
index d903bdd..cb17e0c 100644
--- a/package.json
+++ b/package.json
@@ -26,6 +26,7 @@
"dependencies": {
"csv-parse": "^7.0.2",
"csv-stringify": "^6.8.3",
+ "libphonenumber-js": "^1.13.14",
"nodemailer": "^10.0.10"
}
}
diff --git a/paket-details.html b/paket-details.html
index 9ec9e46..e3fabf7 100644
--- a/paket-details.html
+++ b/paket-details.html
@@ -1,372 +1,27 @@
-
-
-
-
-
-
- Paket-Details – Tri-Hub Ernährungsberatung
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+ Paket-Details – Tri-Hub Ernährungsberatung
+
+
+
+
+
+
+ ← Zurück zur Angebotsübersicht
+
+
+
+
+
diff --git a/src/features/angebote/angebote-entry.js b/src/features/angebote/angebote-entry.js
index 43eb523..2cf63e5 100644
--- a/src/features/angebote/angebote-entry.js
+++ b/src/features/angebote/angebote-entry.js
@@ -1,5 +1,3 @@
-import "../../styles/styles.css";
-import "../../styles/packages.css";
import { renderAngebotsuebersicht } from "./angebotsuebersicht.js";
renderAngebotsuebersicht();
diff --git a/src/features/angebote/angebotsuebersicht.js b/src/features/angebote/angebotsuebersicht.js
index c0ca328..57133ac 100644
--- a/src/features/angebote/angebotsuebersicht.js
+++ b/src/features/angebote/angebotsuebersicht.js
@@ -23,7 +23,7 @@ export function renderAngebotsuebersicht() {
${highlightHtml}
- ${pkg.name}
+ ${pkg.name}
${pkg.summary}
@@ -41,7 +41,7 @@ export function renderAngebotsuebersicht() {
- ${pkg.buttonText}
+ ${pkg.buttonText} →
`;
diff --git a/src/features/angebote/paket-details.js b/src/features/angebote/paket-details.js
index 7f7cccd..5c70cc2 100644
--- a/src/features/angebote/paket-details.js
+++ b/src/features/angebote/paket-details.js
@@ -1,199 +1,158 @@
import { packagesDetails } from "../../shared/packagesdetails.js";
import { mountBookingPage } from "../booking/booking-page.js";
+import { formatMoney } from "../../shared/order-summary.js";
+import { paymentNotice } from "../../shared/booking-copy.js";
document.addEventListener("DOMContentLoaded", () => {
const container = document.getElementById("package-detail-container");
if (!container) return;
const params = new URLSearchParams(window.location.search);
- const packageId = params.get("id");
+ const pkg = packagesDetails.find((item) => item.id === params.get("id"));
+ const requestedVariant = params.get("variant");
+ const invalidVariant =
+ requestedVariant &&
+ !pkg?.variants?.some(
+ (variant) => variant.variantId === requestedVariant,
+ );
- const pkg = packagesDetails.find((item) => item.id === packageId);
-
- if (!pkg) {
+ if (!pkg || invalidVariant) {
container.innerHTML = `
-
Paket nicht gefunden
-
Bitte wähle ein gültiges Angebot aus der Übersicht.
-
- Zur Übersicht
-
-
- `;
+ Paket nicht gefunden
+ Bitte wähle ein gültiges Paket und eine passende Variante aus.
+ Zur Übersicht
+ `;
return;
}
+ document.title = `${pkg.type} – Tri-Hub Ernährungsberatung`;
const variantsHtml = pkg.variants?.length
- ? `
-
-
Wähle deine Variante
-
`
: "";
container.innerHTML = `
-
+
-
- ${pkg.type} · ${pkg.durationLabel}
-
- ${
- pkg.badge
- ? `${pkg.badge}`
- : ""
- }
+ ${pkg.type} ·
+ ${pkg.badge ? `${pkg.badge}` : ""}
-
- ${pkg.title}
+ ${pkg.title}
${pkg.subtitle}
-
-
-
- ${pkg.price.toFixed(2)} €
-
-
- inkl. ${pkg.taxRate}% MwSt. (${pkg.billingInterval})
-
-
-
- ${variantsHtml}
-
-
-
-
- Für wen ist dieses Paket ideal?
-
+
+
+
+
+ Für wen ist dieses Paket ideal?
- ${pkg.targetGroup
- .map((item) => `- ${item}
`)
- .join("")}
+ ${pkg.targetGroup.map((item) => `- ${item}
`).join("")}
-
-
-
-
- Das ist im Paket enthalten
-
+
+
+ Das ist im Paket enthalten
- ${pkg.includedFeatures
- .map((feature) => `- ${feature}
`)
- .join("")}
+ ${pkg.includedFeatures.map((feature) => `- ${feature}
`).join("")}
-
-
-
-
- So läuft deine Begleitung ab
-
+
+
+ So läuft deine Begleitung ab
${pkg.processSteps
.map(
(step) => `
-
- ${step.step}:
-
- ${step.text}
-
-
- `,
+
+
${step.step}
+
${step.text}
+
`,
)
.join("")}
-
+
+
+ Dein ${pkg.type.toLowerCase().replace(/^./, (letter) => letter.toUpperCase())}-Paket
+
+
+
inkl. ${pkg.taxRate} % MwSt. · ${pkg.billingInterval}
+
+
+ ${variantsHtml}
+
+
+
+
+
`;
-
-
-
- Dieser Buchungsablauf ist eine Simulation. Es erfolgt keine
- Abbuchung und keine verbindliche Beratungsbuchung.
-
-
-
-
-
- `;
-
- const variantSelect = document.getElementById("variant-select");
- const displayedPrice = document.getElementById("displayed-price");
- const bookingBtn = document.getElementById("proceed-to-booking-btn");
- const bookingRoot = document.getElementById("booking-root");
- const bookingStatus = document.getElementById("booking-status");
-
+ container.querySelector("#booking-notice").textContent = paymentNotice;
+ const variantSelect = container.querySelector("#variant-select");
+ if (variantSelect && requestedVariant)
+ variantSelect.value = requestedVariant;
+ const bookingBtn = container.querySelector("#proceed-to-booking-btn");
+ const bookingRoot = container.querySelector("#booking-root");
let cleanupBooking;
+ let activeDialog;
function prepareBooking() {
cleanupBooking?.();
- bookingStatus.textContent = "";
-
+ activeDialog = null;
+ bookingBtn.disabled = true;
+ bookingBtn.textContent = "Buchungsformular wird geladen …";
+ bookingBtn.setAttribute("aria-expanded", "false");
+ bookingBtn.removeAttribute("aria-controls");
+ const variant = pkg.variants?.find(
+ (item) => item.variantId === variantSelect?.value,
+ );
+ const duration = variant
+ ? `${variant.durationWeeks} Wochen Begleitung`
+ : pkg.durationLabel;
+ container.querySelector("#displayed-price").textContent = formatMoney(
+ variant?.price ?? pkg.price,
+ pkg.currency,
+ );
+ container.querySelector("#package-duration").textContent = duration;
+ container.querySelector("#selected-duration").textContent = duration;
cleanupBooking = mountBookingPage(bookingRoot, {
packageId: pkg.id,
variantId: variantSelect?.value,
});
}
- function updatePrice() {
- if (!variantSelect) return;
-
- const selectedOption = variantSelect.selectedOptions[0];
- const price = Number(selectedOption.dataset.price);
-
- displayedPrice.textContent = `${price.toFixed(2)} €`;
- }
-
- updatePrice();
- prepareBooking();
-
- variantSelect?.addEventListener("change", () => {
- updatePrice();
- prepareBooking();
+ // Booking lädt die Paketvorschau asynchron, auch bei einem erneuten Versuch.
+ const observer = new MutationObserver(() => {
+ const dialog = bookingRoot.querySelector(".booking__dialog");
+ if (!dialog || dialog === activeDialog) return;
+ activeDialog = dialog;
+ bookingBtn.disabled = false;
+ bookingBtn.textContent = "Jetzt buchen";
+ bookingBtn.setAttribute("aria-controls", dialog.id);
+ dialog.addEventListener("close", () => {
+ bookingBtn.setAttribute("aria-expanded", "false");
+ bookingBtn.focus();
+ });
});
+ observer.observe(bookingRoot, { childList: true, subtree: true });
+ prepareBooking();
+ variantSelect?.addEventListener("change", prepareBooking);
bookingBtn.addEventListener("click", () => {
- const dialog = bookingRoot.querySelector(".booking__dialog");
-
- if (!dialog) {
- bookingStatus.textContent =
- "Das Buchungsformular ist noch nicht verfügbar. Bitte beachte den Ladehinweis im Buchungsbereich.";
- return;
- }
-
- if (dialog.open) return;
-
- bookingStatus.textContent = "";
-
- dialog.addEventListener(
- "close",
- () => {
- bookingBtn.focus();
- },
- { once: true },
+ if (!activeDialog || activeDialog.open) return;
+ activeDialog.showModal();
+ bookingBtn.setAttribute("aria-expanded", "true");
+ const result = activeDialog.querySelector(
+ ".booking__result:not([hidden])",
);
-
- dialog.showModal();
- dialog.querySelector('[name="name"]').focus();
+ (result || activeDialog.querySelector('[name="name"]')).focus();
});
});
diff --git a/src/features/booking/booking-confirmation.js b/src/features/booking/booking-confirmation.js
index 5e67a8b..7311709 100644
--- a/src/features/booking/booking-confirmation.js
+++ b/src/features/booking/booking-confirmation.js
@@ -90,7 +90,7 @@ if (
if (body.emailStatus === "pending" && body.packageId) {
const params = new URLSearchParams({ id: body.packageId });
if (order?.variantId) params.set("variant", order.variantId);
- select("confirmation-retry").href = `/booking.html?${params}`;
+ select("confirmation-retry").href = `/paket-details.html?${params}`;
select("confirmation-retry").hidden = false;
}
select("confirmation-empty").hidden = true;
diff --git a/src/features/booking/booking-page.js b/src/features/booking/booking-page.js
index 4d97d52..2366c50 100644
--- a/src/features/booking/booking-page.js
+++ b/src/features/booking/booking-page.js
@@ -1,3 +1,7 @@
+import {
+ isValidBookingPhone,
+ phoneError,
+} from "../../shared/phone-validation.js";
import { previewBooking, submitBooking } from "./booking-api.js";
import "./booking.css";
import { orderSummaryRows } from "../../shared/order-summary.js";
@@ -284,12 +288,8 @@ export function mountBookingPage(
fields.email =
"Bitte eine gültige E-Mail-Adresse eingeben.";
phone.value = phone.value.trim();
- if (
- phone.value.length > 50 ||
- /[\x00-\x1f\x7f]/.test(phone.value)
- )
- fields.phone =
- "Bitte höchstens 50 Zeichen ohne Zeilenumbrüche eingeben.";
+ if (!isValidBookingPhone(phone.value))
+ fields.phone = phoneError;
if (Object.keys(fields).length) {
showError(
"Bitte die markierten Angaben prüfen.",
diff --git a/src/server/services/booking-service.js b/src/server/services/booking-service.js
index f62c6d3..5ecf2e2 100644
--- a/src/server/services/booking-service.js
+++ b/src/server/services/booking-service.js
@@ -1,3 +1,7 @@
+import {
+ isValidBookingPhone,
+ phoneError,
+} from "../../shared/phone-validation.js";
import { createHash } from "node:crypto";
import { packagesDetails } from "../../shared/packagesdetails.js";
@@ -112,16 +116,12 @@ function validateInput(input, key) {
) {
fields.email = "Bitte eine gültige E-Mail-Adresse eingeben.";
}
- if (!customer && input.phone !== undefined) {
- if (
- typeof input.phone !== "string" ||
- input.phone.length > 50 ||
- /[\x00-\x1f\x7f]/.test(input.phone)
- ) {
- fields.phone =
- "Bitte höchstens 50 Zeichen ohne Zeilenumbrüche eingeben.";
- } else if (input.phone.trim()) {
- result.phone = input.phone.trim();
+ const phone = customer ? customer.phone : input.phone;
+ if (phone !== undefined) {
+ if (!isValidBookingPhone(phone)) {
+ fields.phone = phoneError;
+ } else if (!customer && phone.trim()) {
+ result.phone = phone.trim();
}
}
if (Object.keys(fields).length) {
diff --git a/src/server/swagger.json b/src/server/swagger.json
index 409c41d..7ba05eb 100644
--- a/src/server/swagger.json
+++ b/src/server/swagger.json
@@ -553,7 +553,7 @@
"phone": {
"type": "string",
"maxLength": 50,
- "description": "Optionale Telefonnummer bei Übergabe von name und email, ohne Steuerzeichen. Bei customer gilt customer.phone.",
+ "description": "Optionale Telefonnummer: deutsche Nummer mit Vorwahl oder internationale Nummer mit Ländervorwahl. Prüfung des Nummernformats, keine Bestätigung der Erreichbarkeit. Bei customer gilt customer.phone.",
"example": "+49 170 1234567"
},
"customer": {
@@ -575,6 +575,8 @@
},
"phone": {
"type": "string",
+ "maxLength": 50,
+ "description": "Optional. Deutsche Nummer mit Vorwahl oder internationale Nummer mit Ländervorwahl; keine Bestätigung der Erreichbarkeit.",
"example": "+49 170 1234567"
},
"ageGroup": {
diff --git a/src/shared/phone-validation.js b/src/shared/phone-validation.js
new file mode 100644
index 0000000..8958dcf
--- /dev/null
+++ b/src/shared/phone-validation.js
@@ -0,0 +1,14 @@
+import { isValidPhoneNumber } from "libphonenumber-js/max";
+
+export const phoneError =
+ "Bitte eine gültige Telefonnummer mit Vorwahl eingeben, z. B. 0170 1234567 oder +49 170 1234567.";
+
+export function isValidBookingPhone(value) {
+ if (typeof value !== "string" || value.length > 50) return false;
+ if (/[\x00-\x1f\x7f]/.test(value)) return false;
+ const phone = value.trim();
+ if (!phone) return true;
+ if (!/^\+?[0-9 ()/.-]+$/.test(phone)) return false;
+ if (!/^(?:\+|0)/.test(phone)) return false;
+ return isValidPhoneNumber(phone, "DE");
+}
diff --git a/src/styles/packages.css b/src/styles/packages.css
index c40e4f5..dd8c95b 100644
--- a/src/styles/packages.css
+++ b/src/styles/packages.css
@@ -1,10 +1,279 @@
-#booking-root .booking__open {
+.packages-page {
+ margin: 0;
+ min-height: 100vh;
+ color: #f8fafc;
+ background:
+ radial-gradient(circle at 12% 0%, #10b98117, transparent 30%),
+ radial-gradient(circle at 88% 26%, #10b9810e, transparent 27%),
+ linear-gradient(145deg, #0d0d1a, #1a1a2e);
+ font-family: Inter, system-ui, sans-serif;
+ line-height: 1.6;
+ color-scheme: dark;
+}
+.packages-section,
+.packages-section * {
+ box-sizing: border-box;
+}
+.packages-section {
+ width: min(1180px, calc(100% - 3rem));
+ margin: auto;
+ padding-block: 5rem;
+ overflow-wrap: anywhere;
+}
+.packages-header {
+ display: grid;
+ grid-template-columns: minmax(0, 1.2fr) minmax(0, 1fr);
+ gap: 3rem;
+ align-items: end;
+ margin-bottom: 3rem;
+}
+.packages-eyebrow,
+.package-type {
+ color: #10b981;
+ font-size: 0.75rem;
+ font-weight: 600;
+ letter-spacing: 0.14em;
+ text-transform: uppercase;
+}
+.packages-header h1,
+.detail-heading h1 {
+ margin: 0.75rem 0 1rem;
+ font-size: clamp(2rem, 4.5vw, 3.75rem);
+ font-weight: 600;
+ line-height: 1.15;
+ letter-spacing: -0.035em;
+}
+.packages-intro,
+.package-desc,
+.detail-text,
+.detail-list,
+.price-note,
+.booking-notice {
+ color: #94a3b8;
+}
+.packages-intro {
+ margin: 0;
+ max-width: 40rem;
+}
+.packages-container {
+ display: grid;
+ grid-template-columns: repeat(3, minmax(0, 1fr));
+ gap: 1.5rem;
+}
+.package-card,
+.package-checkout {
+ min-width: 0;
+ padding: 1.75rem;
+ border: 1px solid #f8fafc1f;
+ border-radius: 1.4rem;
+ background: #ffffff0b;
+}
+.package-card {
+ display: flex;
+ flex-direction: column;
+}
+.package-type-container {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ gap: 0.75rem;
+ margin-bottom: 1rem;
+}
+.package-highlight {
+ border-radius: 999px;
+ padding: 0.25rem 0.65rem;
+ background: #10b98114;
+ color: #34d399;
+ border: 1px solid #10b98133;
+ font-size: 0.65rem;
+ font-weight: 600;
+ letter-spacing: 0.05em;
+}
+.package-title {
+ margin: 0 0 1rem;
+ color: #fff;
+ font-size: 1.55rem;
+ font-weight: 600;
+ line-height: 1.25;
+ letter-spacing: -0.025em;
+}
+.package-desc {
+ margin: 0 0 1.5rem;
+}
+.package-details-box {
+ padding-block: 1.5rem;
+ border-top: 1px solid #f8fafc1f;
+ flex: 1;
+}
+.detail-item + .detail-item {
+ margin-top: 1.25rem;
+}
+.detail-title {
+ margin: 0 0 0.5rem;
+ color: #f8fafc;
+ font-size: 1rem;
+ font-weight: 600;
+}
+.detail-text {
+ font-size: 0.9rem;
+}
+.package-btn {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 0.75rem;
+ width: 100%;
+ min-height: 3rem;
+ padding: 0.8rem 1.1rem;
+ border: 1px solid #94a3b840;
+ border-radius: 999px;
+ background: #ffffff06;
+ color: #f8fafc;
+ font: inherit;
+ font-size: 0.9rem;
+ font-weight: 600;
+ text-align: center;
+ text-decoration: none;
+ cursor: pointer;
+}
+.package-btn:hover {
+ border-color: #10b981;
+ background: #10b98114;
+}
+.package-btn.primary-cta {
+ background: #10b981;
+ border-color: #10b981;
+ color: #06130f;
+}
+.package-btn.primary-cta:hover {
+ background: #34d399;
+}
+.package-btn:disabled {
+ opacity: 0.65;
+ cursor: wait;
+}
+.packages-section :focus-visible {
+ outline: 3px solid #10b981;
+ outline-offset: 4px;
+}
+.packages-back {
+ display: inline-block;
+ margin-bottom: 2rem;
+ padding-block: 0.5rem;
+ color: #34d399;
+ text-decoration: none;
+}
+.detail-heading {
+ max-width: 52rem;
+ margin-bottom: 3rem;
+}
+.detail-grid {
+ display: grid;
+ grid-template-columns: minmax(0, 1.3fr) minmax(0, 1fr);
+ gap: 2.5rem;
+ align-items: start;
+}
+.detail-content > section + section {
+ margin-top: 2.5rem;
+}
+.detail-content h2,
+.package-checkout h2 {
+ margin: 0 0 1rem;
+ font-size: 1.25rem;
+}
+.detail-list {
+ padding-left: 1.25rem;
+}
+.detail-list li {
+ margin-block: 0.65rem;
+}
+.detail-list li::marker {
+ color: #10b981;
+}
+.process-step {
+ margin-top: 1rem;
+ padding: 1.25rem;
+ border: 1px solid #f8fafc1f;
+ border-radius: 1rem;
+ background: #ffffff06;
+}
+.process-step strong {
+ display: block;
+ margin-bottom: 0.4rem;
+}
+.process-step p {
+ margin: 0;
+}
+.price-box {
+ margin-bottom: 1.5rem;
+}
+.price-amount {
+ display: block;
+ font-size: clamp(2rem, 4vw, 3rem);
+ font-weight: 600;
+ letter-spacing: -0.04em;
+ color: #34d399;
+}
+.price-note,
+.booking-notice {
+ font-size: 0.85rem;
+}
+.detail-box {
+ margin-bottom: 1.5rem;
+}
+.detail-box label {
+ display: block;
+ margin-bottom: 0.5rem;
+}
+.variant-select {
+ width: 100%;
+ max-width: 100%;
+ min-width: 0;
+ min-height: 3rem;
+ padding: 0.65rem;
+ border: 1px solid #94a3b866;
+ border-radius: 0.75rem;
+ color: #f8fafc;
+ background: #0d0d1a;
+ font: inherit;
+}
+/* Die Detailseite zeigt ihre eigene Übersicht; Booking liefert den Dialog. */
+.packages-section #booking-root > .booking > h2,
+.packages-section #booking-root .booking__package,
+.packages-section #booking-root .booking__payment,
+.packages-section #booking-root .booking__open {
display: none;
}
-/* Zusätzliche Booking-Übersicht ausblenden, Dialog erhalten. */
-#booking-root > .booking > h2,
-#booking-root .booking__package,
-#booking-root .booking__payment,
-#booking-root .booking__open {
- display: none;
+.packages-section .booking__dialog {
+ background: #0d0d1a;
+ color: #f8fafc;
+ border-color: #94a3b840;
+}
+.packages-section .booking__dialog .booking__button:not(.booking__cancel) {
+ background: #10b981;
+ border-color: #10b981;
+ color: #06130f;
+}
+.packages-section .booking__dialog :focus-visible {
+ outline-color: #10b981;
+}
+@media (max-width: 60rem) {
+ .packages-container {
+ grid-template-columns: repeat(auto-fit, minmax(min(100%, 18rem), 1fr));
+ }
+ .packages-header,
+ .detail-grid {
+ grid-template-columns: minmax(0, 1fr);
+ gap: 1.5rem;
+ }
+}
+@media (max-width: 30rem) {
+ .packages-section {
+ width: calc(100% - 2rem);
+ padding-block: 2.5rem;
+ }
+ .package-card,
+ .package-checkout {
+ padding: 1.25rem;
+ }
}
diff --git a/src/tests/bookings.test.js b/src/tests/bookings.test.js
index e3b510d..2be862c 100644
--- a/src/tests/bookings.test.js
+++ b/src/tests/bookings.test.js
@@ -619,3 +619,44 @@ test("Swagger-Kundendaten und Zustimmung werden validiert und dauerhaft gespeich
409,
);
});
+
+test("Telefonnummern werden in beiden Anfrageformaten geprüft", async (t) => {
+ const { post, sent } = await setup(t);
+ for (const phone of [
+ "123",
+ "00000000000",
+ "abc",
+ "+999 123456789",
+ "0170 test 1234567",
+ ]) {
+ for (const data of [
+ { ...input, phone },
+ {
+ packageId: input.packageId,
+ acceptedTerms: true,
+ customer: {
+ firstName: "Test",
+ lastName: "Person",
+ email: input.email,
+ phone,
+ },
+ },
+ ]) {
+ const result = await post(data);
+ assert.equal(result.status, 400, phone);
+ assert.ok(result.body.error.fields.phone);
+ }
+ }
+ assert.equal(sent.length, 0);
+ for (const phone of [
+ "",
+ "0170 1234567",
+ "+49 170 1234567",
+ "0049 170 1234567",
+ "030 123456",
+ "+44 20 7946 0958",
+ ]) {
+ const result = await post({ ...input, phone });
+ assert.equal(result.status, 201, phone);
+ }
+});