Zomars/cal 748 paid bookings are failing (#1335)
* E2E video adjustments * Adds test to add Stripe integration * Type fix * WIP: Payment troubleshooting * Paid bookings shouldn't be confirmed by default * Runs stripe test only if installed * BookingListItem Adjustments * Pending paid bookings should be unconfirmed * Attempt to fix paid bookings * Type fixes * Type fixes * Tests fixes * Adds paid booking to seeder * Moves stripe tests to own file * Matches app locale to Stripe's * Fixes minimun price for testing * Stripe test fixes * Fixes stripe frame test * Added some Stripe TODOspull/1335/merge
parent
ca405743fb
commit
21103580f7
|
@ -12,7 +12,13 @@ jobs:
|
|||
GOOGLE_API_CREDENTIALS: "{}"
|
||||
# GOOGLE_API_CREDENTIALS: ${{ secrets.CI_GOOGLE_API_CREDENTIALS }}
|
||||
# CRON_API_KEY: xxx
|
||||
# CALENDSO_ENCRYPTION_KEY: xxx
|
||||
CALENDSO_ENCRYPTION_KEY: ${{ secrets.CI_CALENDSO_ENCRYPTION_KEY }}
|
||||
NEXT_PUBLIC_STRIPE_PUBLIC_KEY: ${{ secrets.CI_NEXT_PUBLIC_STRIPE_PUBLIC_KEY }}
|
||||
STRIPE_PRIVATE_KEY: ${{ secrets.CI_STRIPE_PRIVATE_KEY }}
|
||||
STRIPE_CLIENT_ID: ${{ secrets.CI_STRIPE_CLIENT_ID }}
|
||||
STRIPE_WEBHOOK_SECRET: ${{ secrets.CI_STRIPE_WEBHOOK_SECRET }}
|
||||
PAYMENT_FEE_PERCENTAGE: 0.005
|
||||
PAYMENT_FEE_FIXED: 10
|
||||
# NEXTAUTH_URL: xxx
|
||||
# EMAIL_FROM: xxx
|
||||
# EMAIL_SERVER_HOST: xxx
|
||||
|
@ -61,8 +67,6 @@ jobs:
|
|||
- run: yarn db-seed
|
||||
- run: yarn test
|
||||
- run: yarn build
|
||||
- run: yarn start &
|
||||
- run: npx wait-port 3000 --timeout 10000
|
||||
|
||||
- name: Cache playwright binaries
|
||||
uses: actions/cache@v2
|
||||
|
|
|
@ -37,6 +37,7 @@ yarn-error.log*
|
|||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
.env.*
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
|
|
@ -9,7 +9,7 @@ import { inferQueryOutput, trpc } from "@lib/trpc";
|
|||
|
||||
import TableActions, { ActionType } from "@components/ui/TableActions";
|
||||
|
||||
type BookingItem = inferQueryOutput<"viewer.bookings">[number];
|
||||
type BookingItem = inferQueryOutput<"viewer.bookings">["bookings"][number];
|
||||
|
||||
function BookingListItem(booking: BookingItem) {
|
||||
const { t, i18n } = useLocale();
|
||||
|
@ -73,20 +73,17 @@ function BookingListItem(booking: BookingItem) {
|
|||
const startTime = dayjs(booking.startTime).format(isUpcoming ? "ddd, D MMM" : "D MMMM YYYY");
|
||||
|
||||
return (
|
||||
<tr>
|
||||
<tr className="flex">
|
||||
<td className="hidden px-6 py-4 align-top sm:table-cell whitespace-nowrap">
|
||||
<div className="text-sm leading-6 text-gray-900">{startTime}</div>
|
||||
<div className="text-sm text-gray-500">
|
||||
{dayjs(booking.startTime).format("HH:mm")} - {dayjs(booking.endTime).format("HH:mm")}
|
||||
</div>
|
||||
</td>
|
||||
<td className={"px-6 py-4" + (booking.rejected ? " line-through" : "")}>
|
||||
<td className={"px-6 py-4 flex-1" + (booking.rejected ? " line-through" : "")}>
|
||||
<div className="sm:hidden">
|
||||
{!booking.confirmed && !booking.rejected && (
|
||||
<span className="mb-2 inline-flex items-center px-1.5 py-0.5 rounded-sm text-xs font-medium bg-yellow-100 text-yellow-800">
|
||||
{t("unconfirmed")}
|
||||
</span>
|
||||
)}
|
||||
{!booking.confirmed && !booking.rejected && <Tag className="mb-2 mr-2">{t("unconfirmed")}</Tag>}
|
||||
{!!booking?.eventType?.price && !booking.paid && <Tag className="mb-2 mr-2">Pending payment</Tag>}
|
||||
<div className="text-sm font-medium text-gray-900">
|
||||
{startTime}:{" "}
|
||||
<small className="text-sm text-gray-500">
|
||||
|
@ -94,13 +91,14 @@ function BookingListItem(booking: BookingItem) {
|
|||
</small>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-sm font-medium leading-6 truncate text-neutral-900 max-w-52 md:max-w-96">
|
||||
<div className="text-sm font-medium leading-6 truncate text-neutral-900 max-w-52 md:max-w-max">
|
||||
{booking.eventType?.team && <strong>{booking.eventType.team.name}: </strong>}
|
||||
{booking.title}
|
||||
{!!booking?.eventType?.price && !booking.paid && (
|
||||
<Tag className="hidden ml-2 sm:inline-flex">Pending payment</Tag>
|
||||
)}
|
||||
{!booking.confirmed && !booking.rejected && (
|
||||
<span className="ml-2 hidden sm:inline-flex items-center px-1.5 py-0.5 rounded-sm text-xs font-medium bg-yellow-100 text-yellow-800">
|
||||
{t("unconfirmed")}
|
||||
</span>
|
||||
<Tag className="hidden ml-2 sm:inline-flex">{t("unconfirmed")}</Tag>
|
||||
)}
|
||||
</div>
|
||||
{booking.description && (
|
||||
|
@ -130,4 +128,13 @@ function BookingListItem(booking: BookingItem) {
|
|||
);
|
||||
}
|
||||
|
||||
const Tag = ({ children, className = "" }: React.PropsWithChildren<{ className?: string }>) => {
|
||||
return (
|
||||
<span
|
||||
className={`inline-flex items-center px-1.5 py-0.5 rounded-sm text-xs font-medium bg-yellow-100 text-yellow-800 ${className}`}>
|
||||
{children}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
export default BookingListItem;
|
||||
|
|
|
@ -115,7 +115,7 @@ function ConnectedCalendarsList(props: Props) {
|
|||
<DisconnectIntegration
|
||||
id={item.credentialId}
|
||||
render={(btnProps) => (
|
||||
<Button {...btnProps} color="warn">
|
||||
<Button {...btnProps} color="warn" data-testid="integration-connection-button">
|
||||
{t("disconnect")}
|
||||
</Button>
|
||||
)}
|
||||
|
@ -143,7 +143,7 @@ function ConnectedCalendarsList(props: Props) {
|
|||
<DisconnectIntegration
|
||||
id={item.credentialId}
|
||||
render={(btnProps) => (
|
||||
<Button {...btnProps} color="warn">
|
||||
<Button {...btnProps} color="warn" data-testid="integration-connection-button">
|
||||
Disconnect
|
||||
</Button>
|
||||
)}
|
||||
|
@ -248,7 +248,7 @@ function CalendarList(props: Props) {
|
|||
<ConnectIntegration
|
||||
type={item.type}
|
||||
render={(btnProps) => (
|
||||
<Button color="secondary" {...btnProps}>
|
||||
<Button color="secondary" {...btnProps} data-testid="integration-connection-button">
|
||||
{t("connect")}
|
||||
</Button>
|
||||
)}
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
import { CardElement, useStripe, useElements } from "@stripe/react-stripe-js";
|
||||
import { StripeCardElementChangeEvent } from "@stripe/stripe-js";
|
||||
import { CardElement, useElements, useStripe } from "@stripe/react-stripe-js";
|
||||
import stripejs, { StripeCardElementChangeEvent, StripeElementLocale } from "@stripe/stripe-js";
|
||||
import { useRouter } from "next/router";
|
||||
import { stringify } from "querystring";
|
||||
import React, { useState } from "react";
|
||||
import { SyntheticEvent } from "react";
|
||||
import React, { SyntheticEvent, useEffect, useState } from "react";
|
||||
|
||||
import { PaymentData } from "@ee/lib/stripe/server";
|
||||
|
||||
|
@ -12,7 +11,7 @@ import { useLocale } from "@lib/hooks/useLocale";
|
|||
|
||||
import Button from "@components/ui/Button";
|
||||
|
||||
const CARD_OPTIONS = {
|
||||
const CARD_OPTIONS: stripejs.StripeCardElementOptions = {
|
||||
iconStyle: "solid" as const,
|
||||
classes: {
|
||||
base: "block p-2 w-full border-solid border-2 border-gray-300 rounded-md shadow-sm dark:bg-black dark:text-white dark:border-black focus-within:ring-black focus-within:border-black sm:text-sm",
|
||||
|
@ -29,7 +28,7 @@ const CARD_OPTIONS = {
|
|||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
} as const;
|
||||
|
||||
type Props = {
|
||||
payment: {
|
||||
|
@ -47,18 +46,23 @@ type States =
|
|||
| { status: "ok" };
|
||||
|
||||
export default function PaymentComponent(props: Props) {
|
||||
const { t } = useLocale();
|
||||
const { t, i18n } = useLocale();
|
||||
const router = useRouter();
|
||||
const { name, date } = router.query;
|
||||
const [state, setState] = useState<States>({ status: "idle" });
|
||||
const stripe = useStripe();
|
||||
const elements = useElements();
|
||||
|
||||
const { isDarkMode } = useDarkMode();
|
||||
|
||||
useEffect(() => {
|
||||
elements?.update({ locale: i18n.language as StripeElementLocale });
|
||||
}, [elements, i18n.language]);
|
||||
|
||||
if (isDarkMode) {
|
||||
CARD_OPTIONS.style.base.color = "#fff";
|
||||
CARD_OPTIONS.style.base.iconColor = "#fff";
|
||||
CARD_OPTIONS.style.base["::placeholder"].color = "#fff";
|
||||
CARD_OPTIONS.style!.base!.color = "#fff";
|
||||
CARD_OPTIONS.style!.base!.iconColor = "#fff";
|
||||
CARD_OPTIONS.style!.base!["::placeholder"]!.color = "#fff";
|
||||
}
|
||||
|
||||
const handleChange = async (event: StripeCardElementChangeEvent) => {
|
||||
|
|
|
@ -64,7 +64,11 @@ export async function handlePayment(
|
|||
data: {
|
||||
type: PaymentType.STRIPE,
|
||||
uid: uuidv4(),
|
||||
bookingId: booking.id,
|
||||
booking: {
|
||||
connect: {
|
||||
id: booking.id,
|
||||
},
|
||||
},
|
||||
amount: selectedEventType.price,
|
||||
fee: paymentFee,
|
||||
currency: selectedEventType.currency,
|
||||
|
|
|
@ -6,7 +6,7 @@ import stripe from "@ee/lib/stripe/server";
|
|||
|
||||
import { CalendarEvent } from "@lib/calendarClient";
|
||||
import { IS_PRODUCTION } from "@lib/config/constants";
|
||||
import { HttpError } from "@lib/core/http/error";
|
||||
import { HttpError as HttpCode } from "@lib/core/http/error";
|
||||
import { getErrorFromUnknown } from "@lib/errors";
|
||||
import EventManager from "@lib/events/EventManager";
|
||||
import prisma from "@lib/prisma";
|
||||
|
@ -31,6 +31,7 @@ async function handlePaymentSuccess(event: Stripe.Event) {
|
|||
booking: {
|
||||
update: {
|
||||
paid: true,
|
||||
confirmed: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -97,7 +98,7 @@ async function handlePaymentSuccess(event: Stripe.Event) {
|
|||
|
||||
await prisma.booking.update({
|
||||
where: {
|
||||
id: payment.bookingId,
|
||||
id: booking.id,
|
||||
},
|
||||
data: {
|
||||
references: {
|
||||
|
@ -106,6 +107,11 @@ async function handlePaymentSuccess(event: Stripe.Event) {
|
|||
},
|
||||
});
|
||||
}
|
||||
|
||||
throw new HttpCode({
|
||||
statusCode: 200,
|
||||
message: `Booking with id '${booking.id}' was paid and confirmed.`,
|
||||
});
|
||||
}
|
||||
|
||||
type WebhookHandler = (event: Stripe.Event) => Promise<void>;
|
||||
|
@ -117,15 +123,15 @@ const webhookHandlers: Record<string, WebhookHandler | undefined> = {
|
|||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
try {
|
||||
if (req.method !== "POST") {
|
||||
throw new HttpError({ statusCode: 405, message: "Method Not Allowed" });
|
||||
throw new HttpCode({ statusCode: 405, message: "Method Not Allowed" });
|
||||
}
|
||||
const sig = req.headers["stripe-signature"];
|
||||
if (!sig) {
|
||||
throw new HttpError({ statusCode: 400, message: "Missing stripe-signature" });
|
||||
throw new HttpCode({ statusCode: 400, message: "Missing stripe-signature" });
|
||||
}
|
||||
|
||||
if (!process.env.STRIPE_WEBHOOK_SECRET) {
|
||||
throw new HttpError({ statusCode: 500, message: "Missing process.env.STRIPE_WEBHOOK_SECRET" });
|
||||
throw new HttpCode({ statusCode: 500, message: "Missing process.env.STRIPE_WEBHOOK_SECRET" });
|
||||
}
|
||||
const requestBuffer = await buffer(req);
|
||||
const payload = requestBuffer.toString();
|
||||
|
@ -137,7 +143,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||
await handler(event);
|
||||
} else {
|
||||
/** Not really an error, just letting Stripe know that the webhook was received but unhandled */
|
||||
throw new HttpError({
|
||||
throw new HttpCode({
|
||||
statusCode: 202,
|
||||
message: `Unhandled Stripe Webhook event type ${event.type}`,
|
||||
});
|
||||
|
|
|
@ -1,7 +1,11 @@
|
|||
import { Prisma } from "@prisma/client";
|
||||
import _ from "lodash";
|
||||
|
||||
import { validJson } from "@lib/jsonUtils";
|
||||
/**
|
||||
* We can't use aliases in playwright tests (yet)
|
||||
* https://github.com/microsoft/playwright/issues/7121
|
||||
*/
|
||||
import { validJson } from "../../lib/jsonUtils";
|
||||
|
||||
const credentialData = Prisma.validator<Prisma.CredentialArgs>()({
|
||||
select: { id: true, type: true },
|
||||
|
@ -115,5 +119,8 @@ export function hasIntegration(integrations: IntegrationMeta, type: string): boo
|
|||
(i) => i.type === type && !!i.installed && (type === "daily_video" || i.credentials.length > 0)
|
||||
);
|
||||
}
|
||||
export function hasIntegrationInstalled(type: Integration["type"]): boolean {
|
||||
return ALL_INTEGRATIONS.some((i) => i.type === type && !!i.installed);
|
||||
}
|
||||
|
||||
export default getIntegrations;
|
||||
|
|
|
@ -5,8 +5,15 @@ import { getSession } from "@lib/auth";
|
|||
import prisma from "@lib/prisma";
|
||||
import { WorkingHours } from "@lib/types/schedule";
|
||||
|
||||
function handlePeriodType(periodType: string): PeriodType {
|
||||
return PeriodType[periodType.toUpperCase()];
|
||||
function isPeriodType(keyInput: string): keyInput is PeriodType {
|
||||
return Object.keys(PeriodType).includes(keyInput);
|
||||
}
|
||||
|
||||
function handlePeriodType(periodType: string): PeriodType | undefined {
|
||||
if (typeof periodType !== "string") return undefined;
|
||||
const passedPeriodType = periodType.toUpperCase();
|
||||
if (!isPeriodType(passedPeriodType)) return undefined;
|
||||
return PeriodType[passedPeriodType];
|
||||
}
|
||||
|
||||
function handleCustomInputs(customInputs: EventTypeCustomInput[], eventTypeId: number) {
|
||||
|
@ -104,7 +111,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||
}
|
||||
}
|
||||
|
||||
if (req.method == "PATCH" || req.method == "POST") {
|
||||
if (req.method === "PATCH" || req.method === "POST") {
|
||||
const data: Prisma.EventTypeCreateInput | Prisma.EventTypeUpdateInput = {
|
||||
title: req.body.title,
|
||||
slug: req.body.slug.trim(),
|
||||
|
@ -116,7 +123,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||
locations: req.body.locations,
|
||||
eventName: req.body.eventName,
|
||||
customInputs: handleCustomInputs(req.body.customInputs as EventTypeCustomInput[], req.body.id),
|
||||
periodType: req.body.periodType ? handlePeriodType(req.body.periodType) : undefined,
|
||||
periodType: handlePeriodType(req.body.periodType),
|
||||
periodDays: req.body.periodDays,
|
||||
periodStartDate: req.body.periodStartDate,
|
||||
periodEndDate: req.body.periodEndDate,
|
||||
|
@ -179,8 +186,8 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||
}
|
||||
|
||||
const availabilityToCreate = openingHours.map((openingHour) => ({
|
||||
startTime: openingHour.startTime,
|
||||
endTime: openingHour.endTime,
|
||||
startTime: new Date(openingHour.startTime),
|
||||
endTime: new Date(openingHour.endTime),
|
||||
days: openingHour.days,
|
||||
}));
|
||||
|
||||
|
|
|
@ -198,6 +198,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||
}
|
||||
|
||||
const eventType = await prisma.eventType.findUnique({
|
||||
rejectOnNotFound: true,
|
||||
where: {
|
||||
id: eventTypeId,
|
||||
},
|
||||
|
@ -331,7 +332,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||
startTime: dayjs(evt.startTime).toDate(),
|
||||
endTime: dayjs(evt.endTime).toDate(),
|
||||
description: evt.description,
|
||||
confirmed: !eventType?.requiresConfirmation || !!rescheduleUid,
|
||||
confirmed: (!eventType.requiresConfirmation && !eventType.price) || !!rescheduleUid,
|
||||
location: evt.location,
|
||||
eventType: {
|
||||
connect: {
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
import { NextPageContext } from "next";
|
||||
|
||||
import { getSession } from "@lib/auth";
|
||||
|
||||
function RedirectPage() {
|
||||
return null;
|
||||
}
|
||||
|
||||
export async function getServerSideProps(context) {
|
||||
export async function getServerSideProps(context: NextPageContext) {
|
||||
const session = await getSession(context);
|
||||
if (!session?.user?.id) {
|
||||
return { redirect: { permanent: false, destination: "/auth/login" } };
|
||||
|
|
|
@ -461,7 +461,7 @@ function ConnectOrDisconnectIntegrationButton(props: {
|
|||
<DisconnectIntegration
|
||||
id={credentialId}
|
||||
render={(btnProps) => (
|
||||
<Button {...btnProps} color="warn">
|
||||
<Button {...btnProps} color="warn" data-testid="integration-connection-button">
|
||||
{t("disconnect")}
|
||||
</Button>
|
||||
)}
|
||||
|
@ -488,7 +488,7 @@ function ConnectOrDisconnectIntegrationButton(props: {
|
|||
<ConnectIntegration
|
||||
type={props.type}
|
||||
render={(btnProps) => (
|
||||
<Button color="secondary" {...btnProps}>
|
||||
<Button color="secondary" {...btnProps} data-testid="integration-connection-button">
|
||||
{t("connect")}
|
||||
</Button>
|
||||
)}
|
||||
|
|
|
@ -5,12 +5,20 @@ const config: PlaywrightTestConfig = {
|
|||
testDir: "playwright",
|
||||
timeout: 60_000,
|
||||
retries: process.env.CI ? 3 : 0,
|
||||
reporter: "list",
|
||||
globalSetup: require.resolve("./playwright/lib/globalSetup"),
|
||||
webServer: {
|
||||
command: "yarn start",
|
||||
port: 3000,
|
||||
timeout: 60_000,
|
||||
reuseExistingServer: !process.env.CI,
|
||||
},
|
||||
use: {
|
||||
baseURL: "http://localhost:3000",
|
||||
locale: "en-US",
|
||||
trace: "on-first-retry",
|
||||
trace: "retain-on-failure",
|
||||
headless: !!process.env.CI || !!process.env.PLAYWRIGHT_HEADLESS,
|
||||
video: "on-first-retry",
|
||||
contextOptions: {
|
||||
recordVideo: {
|
||||
dir: "playwright/videos",
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { test, expect } from "@playwright/test";
|
||||
import { expect, test } from "@playwright/test";
|
||||
|
||||
import { todo } from "./lib/testUtils";
|
||||
|
||||
|
|
|
@ -0,0 +1,93 @@
|
|||
import { expect, test } from "@playwright/test";
|
||||
|
||||
import { hasIntegrationInstalled } from "../lib/integrations/getIntegrations";
|
||||
import { todo } from "./lib/testUtils";
|
||||
|
||||
test.describe.serial("Stripe integration", () => {
|
||||
test.skip(!hasIntegrationInstalled("stripe_payment"), "It should only run if Stripe is installed");
|
||||
|
||||
test.describe.serial("Stripe integration dashboard", () => {
|
||||
test.use({ storageState: "playwright/artifacts/proStorageState.json" });
|
||||
|
||||
test("Can add Stripe integration", async ({ page }) => {
|
||||
await page.goto("/integrations");
|
||||
/** We should see the "Connect" button for Stripe */
|
||||
expect(page.locator(`li:has-text("Stripe") >> [data-testid="integration-connection-button"]`))
|
||||
.toContainText("Connect")
|
||||
.catch(() => {
|
||||
console.error(
|
||||
`Make sure Stripe it's properly installed and that an integration hasn't been already added.`
|
||||
);
|
||||
});
|
||||
|
||||
/** We start the Stripe flow */
|
||||
await Promise.all([
|
||||
page.waitForNavigation({ url: "https://connect.stripe.com/oauth/v2/authorize?*" }),
|
||||
await page.click('li:has-text("Stripe") >> [data-testid="integration-connection-button"]'),
|
||||
]);
|
||||
|
||||
await Promise.all([
|
||||
page.waitForNavigation({ url: "/integrations" }),
|
||||
/** We skip filling Stripe forms (testing mode only) */
|
||||
await page.click('[id="skip-account-app"]'),
|
||||
]);
|
||||
|
||||
/** If Stripe is added correctly we should see the "Disconnect" button */
|
||||
expect(
|
||||
page.locator(`li:has-text("Stripe") >> [data-testid="integration-connection-button"]`)
|
||||
).toContainText("Disconnect");
|
||||
});
|
||||
});
|
||||
|
||||
test("Can book a paid booking", async ({ page }) => {
|
||||
await page.goto("/pro/paid");
|
||||
// Click [data-testid="incrementMonth"]
|
||||
await page.click('[data-testid="incrementMonth"]');
|
||||
// Click [data-testid="day"]
|
||||
await page.click('[data-testid="day"][data-disabled="false"]');
|
||||
// Click [data-testid="time"]
|
||||
await page.click('[data-testid="time"]');
|
||||
// --- fill form
|
||||
await page.fill('[name="name"]', "Stripe Stripeson");
|
||||
await page.fill('[name="email"]', "test@example.com");
|
||||
|
||||
await Promise.all([
|
||||
page.waitForNavigation({ url: "/payment/*" }),
|
||||
await page.press('[name="email"]', "Enter"),
|
||||
]);
|
||||
|
||||
await page.waitForSelector('iframe[src^="https://js.stripe.com/v3/elements-inner-card-"]');
|
||||
|
||||
// We lookup Stripe's iframe
|
||||
const stripeFrame = page.frame({
|
||||
url: (url) => url.href.startsWith("https://js.stripe.com/v3/elements-inner-card-"),
|
||||
});
|
||||
|
||||
if (!stripeFrame) throw new Error("Stripe frame not found");
|
||||
|
||||
// Fill [placeholder="Card number"]
|
||||
await stripeFrame.fill('[placeholder="Card number"]', "4242 4242 4242 4242");
|
||||
// Fill [placeholder="MM / YY"]
|
||||
await stripeFrame.fill('[placeholder="MM / YY"]', "12 / 24");
|
||||
// Fill [placeholder="CVC"]
|
||||
await stripeFrame.fill('[placeholder="CVC"]', "111");
|
||||
// Fill [placeholder="ZIP"]
|
||||
await stripeFrame.fill('[placeholder="ZIP"]', "111111");
|
||||
// Click button:has-text("Pay now")
|
||||
await page.click('button:has-text("Pay now")');
|
||||
|
||||
// Make sure we're navigated to the success page
|
||||
await page.waitForNavigation({
|
||||
url(url) {
|
||||
return url.pathname.endsWith("/success");
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
todo("Pending payment booking should not be confirmed by default");
|
||||
todo("Payment should confirm pending payment booking");
|
||||
todo("Payment should trigger a BOOKING_PAID webhook");
|
||||
todo("Paid booking should be able to be rescheduled");
|
||||
todo("Paid booking should be able to be cancelled");
|
||||
todo("Cancelled paid booking should be refunded");
|
||||
});
|
|
@ -11,8 +11,6 @@ test.describe("integrations", () => {
|
|||
|
||||
todo("Can add Zoom integration");
|
||||
|
||||
todo("Can add Stripe integration");
|
||||
|
||||
todo("Can add Google Calendar");
|
||||
|
||||
todo("Can add Office 365 Calendar");
|
||||
|
|
|
@ -2,7 +2,7 @@ import { Browser, chromium } from "@playwright/test";
|
|||
|
||||
async function loginAsUser(username: string, browser: Browser) {
|
||||
const page = await browser.newPage();
|
||||
await page.goto("http://localhost:3000/auth/login");
|
||||
await page.goto(`${process.env.PLAYWRIGHT_TEST_BASE_URL}/auth/login`);
|
||||
// Click input[name="email"]
|
||||
await page.click('input[name="email"]');
|
||||
// Fill input[name="email"]
|
||||
|
|
|
@ -223,6 +223,12 @@ async function main() {
|
|||
slug: "60min",
|
||||
length: 60,
|
||||
},
|
||||
{
|
||||
title: "paid",
|
||||
slug: "paid",
|
||||
length: 60,
|
||||
price: 50,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
|
|
|
@ -334,6 +334,7 @@ const loggedInViewerRouter = createProtectedRouter()
|
|||
endTime: true,
|
||||
eventType: {
|
||||
select: {
|
||||
price: true,
|
||||
team: {
|
||||
select: {
|
||||
name: true,
|
||||
|
@ -342,6 +343,7 @@ const loggedInViewerRouter = createProtectedRouter()
|
|||
},
|
||||
},
|
||||
status: true,
|
||||
paid: true,
|
||||
},
|
||||
orderBy,
|
||||
take: take + 1,
|
||||
|
|
Loading…
Reference in New Issue