[CAL-770] Add payment integration
parent
9ce090f14d
commit
863f02313b
|
@ -15,13 +15,13 @@ import { FormattedNumber, IntlProvider } from "react-intl";
|
|||
import { ReactMultiEmail } from "react-multi-email";
|
||||
import { useMutation } from "react-query";
|
||||
|
||||
import { createPaymentLink } from "@ee/lib/stripe/client";
|
||||
|
||||
import { asStringOrNull } from "@lib/asStringOrNull";
|
||||
import { timeZone } from "@lib/clock";
|
||||
import { ensureArray } from "@lib/ensureArray";
|
||||
import { useLocale } from "@lib/hooks/useLocale";
|
||||
import useTheme from "@lib/hooks/useTheme";
|
||||
import { PaymentLinkDetail } from "@lib/integrations/payment/interfaces/PaymentMethod";
|
||||
import { createPaymentLink } from "@lib/integrations/payment/utils/stripeUtils";
|
||||
import { LocationType } from "@lib/location";
|
||||
import createBooking from "@lib/mutations/bookings/create-booking";
|
||||
import { parseZone } from "@lib/parseZone";
|
||||
|
@ -55,14 +55,14 @@ const BookingPage = (props: BookingPageProps) => {
|
|||
const mutation = useMutation(createBooking, {
|
||||
onSuccess: async ({ attendees, paymentUid, ...responseData }) => {
|
||||
if (paymentUid) {
|
||||
return await router.push(
|
||||
createPaymentLink({
|
||||
const paymentLinkDetail = {
|
||||
paymentUid,
|
||||
date,
|
||||
name: attendees[0].name,
|
||||
absolute: false,
|
||||
})
|
||||
);
|
||||
} as PaymentLinkDetail;
|
||||
|
||||
return await router.push(createPaymentLink(paymentLinkDetail));
|
||||
}
|
||||
|
||||
const location = (function humanReadableLocation(location) {
|
||||
|
|
|
@ -4,10 +4,9 @@ import { useRouter } from "next/router";
|
|||
import { stringify } from "querystring";
|
||||
import React, { SyntheticEvent, useEffect, useState } from "react";
|
||||
|
||||
import { PaymentData } from "@ee/lib/stripe/server";
|
||||
|
||||
import useDarkMode from "@lib/core/browser/useDarkMode";
|
||||
import { useLocale } from "@lib/hooks/useLocale";
|
||||
import { PaymentData } from "@lib/integrations/payment/constants/types";
|
||||
|
||||
import Button from "@components/ui/Button";
|
||||
|
||||
|
|
|
@ -9,11 +9,11 @@ import React, { FC, useEffect, useState } from "react";
|
|||
import { FormattedNumber, IntlProvider } from "react-intl";
|
||||
|
||||
import PaymentComponent from "@ee/components/stripe/Payment";
|
||||
import getStripe from "@ee/lib/stripe/client";
|
||||
import { PaymentPageProps } from "@ee/pages/payment/[uid]";
|
||||
|
||||
import { useLocale } from "@lib/hooks/useLocale";
|
||||
import useTheme from "@lib/hooks/useTheme";
|
||||
import { getStripe } from "@lib/integrations/payment/utils/stripeUtils";
|
||||
|
||||
dayjs.extend(utc);
|
||||
dayjs.extend(toArray);
|
||||
|
@ -108,7 +108,7 @@ const PaymentPage: FC<PaymentPageProps> = (props) => {
|
|||
payment={props.payment}
|
||||
eventType={props.eventType}
|
||||
user={props.user}
|
||||
location={props.booking.location}
|
||||
location={props.booking.location || ""}
|
||||
/>
|
||||
</Elements>
|
||||
)}
|
||||
|
|
|
@ -1,171 +0,0 @@
|
|||
import { PaymentType, Prisma } from "@prisma/client";
|
||||
import Stripe from "stripe";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
|
||||
import { sendAwaitingPaymentEmail, sendOrganizerPaymentRefundFailedEmail } from "@lib/emails/email-manager";
|
||||
import { getErrorFromUnknown } from "@lib/errors";
|
||||
import { CalendarEvent } from "@lib/integrations/calendar/interfaces/Calendar";
|
||||
import prisma from "@lib/prisma";
|
||||
|
||||
import { createPaymentLink } from "./client";
|
||||
|
||||
export type PaymentInfo = {
|
||||
link?: string | null;
|
||||
reason?: string | null;
|
||||
id?: string | null;
|
||||
};
|
||||
|
||||
export type PaymentData = Stripe.Response<Stripe.PaymentIntent> & {
|
||||
stripe_publishable_key: string;
|
||||
stripeAccount: string;
|
||||
};
|
||||
|
||||
export type StripeData = Stripe.OAuthToken & {
|
||||
default_currency: string;
|
||||
};
|
||||
|
||||
const stripePrivateKey = process.env.STRIPE_PRIVATE_KEY!;
|
||||
const paymentFeePercentage = process.env.PAYMENT_FEE_PERCENTAGE!;
|
||||
const paymentFeeFixed = process.env.PAYMENT_FEE_FIXED!;
|
||||
|
||||
const stripe = new Stripe(stripePrivateKey, {
|
||||
apiVersion: "2020-08-27",
|
||||
});
|
||||
|
||||
export async function handlePayment(
|
||||
evt: CalendarEvent,
|
||||
selectedEventType: {
|
||||
price: number;
|
||||
currency: string;
|
||||
},
|
||||
stripeCredential: { key: Prisma.JsonValue },
|
||||
booking: {
|
||||
user: { email: string | null; name: string | null; timeZone: string } | null;
|
||||
id: number;
|
||||
startTime: { toISOString: () => string };
|
||||
uid: string;
|
||||
}
|
||||
) {
|
||||
const paymentFee = Math.round(
|
||||
selectedEventType.price * parseFloat(`${paymentFeePercentage}`) + parseInt(`${paymentFeeFixed}`)
|
||||
);
|
||||
const { stripe_user_id, stripe_publishable_key } = stripeCredential.key as Stripe.OAuthToken;
|
||||
|
||||
const params: Stripe.PaymentIntentCreateParams = {
|
||||
amount: selectedEventType.price,
|
||||
currency: selectedEventType.currency,
|
||||
payment_method_types: ["card"],
|
||||
application_fee_amount: paymentFee,
|
||||
};
|
||||
|
||||
const paymentIntent = await stripe.paymentIntents.create(params, { stripeAccount: stripe_user_id });
|
||||
|
||||
const payment = await prisma.payment.create({
|
||||
data: {
|
||||
type: PaymentType.STRIPE,
|
||||
uid: uuidv4(),
|
||||
booking: {
|
||||
connect: {
|
||||
id: booking.id,
|
||||
},
|
||||
},
|
||||
amount: selectedEventType.price,
|
||||
fee: paymentFee,
|
||||
currency: selectedEventType.currency,
|
||||
success: false,
|
||||
refunded: false,
|
||||
data: Object.assign({}, paymentIntent, {
|
||||
stripe_publishable_key,
|
||||
stripeAccount: stripe_user_id,
|
||||
}) /* We should treat this */ as PaymentData /* but Prisma doesn't know how to handle it, so it we treat it */ as unknown /* and then */ as Prisma.InputJsonValue,
|
||||
externalId: paymentIntent.id,
|
||||
},
|
||||
});
|
||||
|
||||
await sendAwaitingPaymentEmail({
|
||||
...evt,
|
||||
paymentInfo: {
|
||||
link: createPaymentLink({
|
||||
paymentUid: payment.uid,
|
||||
name: booking.user?.name,
|
||||
date: booking.startTime.toISOString(),
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
return payment;
|
||||
}
|
||||
|
||||
export async function refund(
|
||||
booking: {
|
||||
id: number;
|
||||
uid: string;
|
||||
startTime: Date;
|
||||
payment: {
|
||||
id: number;
|
||||
success: boolean;
|
||||
refunded: boolean;
|
||||
externalId: string;
|
||||
data: Prisma.JsonValue;
|
||||
type: PaymentType;
|
||||
}[];
|
||||
},
|
||||
calEvent: CalendarEvent
|
||||
) {
|
||||
try {
|
||||
const payment = booking.payment.find((e) => e.success && !e.refunded);
|
||||
if (!payment) return;
|
||||
|
||||
if (payment.type !== PaymentType.STRIPE) {
|
||||
await handleRefundError({
|
||||
event: calEvent,
|
||||
reason: "cannot refund non Stripe payment",
|
||||
paymentId: "unknown",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const refund = await stripe.refunds.create(
|
||||
{
|
||||
payment_intent: payment.externalId,
|
||||
},
|
||||
{ stripeAccount: (payment.data as unknown as PaymentData)["stripeAccount"] }
|
||||
);
|
||||
|
||||
if (!refund || refund.status === "failed") {
|
||||
await handleRefundError({
|
||||
event: calEvent,
|
||||
reason: refund?.failure_reason || "unknown",
|
||||
paymentId: payment.externalId,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
await prisma.payment.update({
|
||||
where: {
|
||||
id: payment.id,
|
||||
},
|
||||
data: {
|
||||
refunded: true,
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
const err = getErrorFromUnknown(e);
|
||||
console.error(err, "Refund failed");
|
||||
await handleRefundError({
|
||||
event: calEvent,
|
||||
reason: err.message || "unknown",
|
||||
paymentId: "unknown",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function handleRefundError(opts: { event: CalendarEvent; reason: string; paymentId: string }) {
|
||||
console.error(`refund failed: ${opts.reason} for booking '${opts.event.uid}'`);
|
||||
await sendOrganizerPaymentRefundFailedEmail({
|
||||
...opts.event,
|
||||
paymentInfo: { reason: opts.reason, id: opts.paymentId },
|
||||
});
|
||||
}
|
||||
|
||||
export default stripe;
|
|
@ -2,9 +2,10 @@ import { Prisma } from "@prisma/client";
|
|||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import { stringify } from "querystring";
|
||||
|
||||
import stripe, { StripeData } from "@ee/lib/stripe/server";
|
||||
|
||||
import { getSession } from "@lib/auth";
|
||||
import { PAYMENT_INTEGRATIONS_TYPES } from "@lib/integrations/payment/constants/generals";
|
||||
import { STRIPE } from "@lib/integrations/payment/constants/stripeConstats";
|
||||
import { StripeData } from "@lib/integrations/payment/constants/types";
|
||||
import prisma from "@lib/prisma";
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
|
@ -23,20 +24,20 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||
return;
|
||||
}
|
||||
|
||||
const response = await stripe.oauth.token({
|
||||
const response = await STRIPE.oauth.token({
|
||||
grant_type: "authorization_code",
|
||||
code: code.toString(),
|
||||
});
|
||||
|
||||
const data: StripeData = { ...response, default_currency: "" };
|
||||
if (response["stripe_user_id"]) {
|
||||
const account = await stripe.accounts.retrieve(response["stripe_user_id"]);
|
||||
const account = await STRIPE.accounts.retrieve(response["stripe_user_id"]);
|
||||
data["default_currency"] = account.default_currency;
|
||||
}
|
||||
|
||||
await prisma.credential.create({
|
||||
data: {
|
||||
type: "stripe_payment",
|
||||
type: PAYMENT_INTEGRATIONS_TYPES.stripe,
|
||||
key: data as unknown as Prisma.InputJsonObject,
|
||||
userId: session.user.id,
|
||||
},
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
import { Prisma } from "@prisma/client";
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
import stripe from "@ee/lib/stripe/server";
|
||||
|
||||
import { getSession } from "@lib/auth";
|
||||
import { STRIPE } from "@lib/integrations/payment/constants/stripeConstats";
|
||||
import prisma from "@lib/prisma";
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
|
@ -54,7 +53,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||
});
|
||||
|
||||
const return_url = `${process.env.BASE_URL}/settings/billing`;
|
||||
const stripeSession = await stripe.billingPortal.sessions.create({
|
||||
const stripeSession = await STRIPE.billingPortal.sessions.create({
|
||||
customer: customerId,
|
||||
return_url,
|
||||
});
|
||||
|
|
|
@ -2,13 +2,12 @@ import { buffer } from "micro";
|
|||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import Stripe from "stripe";
|
||||
|
||||
import stripe from "@ee/lib/stripe/server";
|
||||
|
||||
import { IS_PRODUCTION } from "@lib/config/constants";
|
||||
import { HttpError as HttpCode } from "@lib/core/http/error";
|
||||
import { getErrorFromUnknown } from "@lib/errors";
|
||||
import EventManager from "@lib/events/EventManager";
|
||||
import { CalendarEvent } from "@lib/integrations/calendar/interfaces/Calendar";
|
||||
import { STRIPE } from "@lib/integrations/payment/constants/stripeConstats";
|
||||
import prisma from "@lib/prisma";
|
||||
import { Ensure } from "@lib/types/utils";
|
||||
|
||||
|
@ -136,7 +135,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||
const requestBuffer = await buffer(req);
|
||||
const payload = requestBuffer.toString();
|
||||
|
||||
const event = stripe.webhooks.constructEvent(payload, sig, process.env.STRIPE_WEBHOOK_SECRET);
|
||||
const event = STRIPE.webhooks.constructEvent(payload, sig, process.env.STRIPE_WEBHOOK_SECRET);
|
||||
|
||||
const handler = webhookHandlers[event.type];
|
||||
if (handler) {
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
import { GetServerSidePropsContext } from "next";
|
||||
|
||||
import { PaymentData } from "@ee/lib/stripe/server";
|
||||
|
||||
import { asStringOrThrow } from "@lib/asStringOrNull";
|
||||
import { PaymentData } from "@lib/integrations/payment/constants/types";
|
||||
import prisma from "@lib/prisma";
|
||||
import { inferSSRProps } from "@lib/types/inferSSRProps";
|
||||
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
import { DestinationCalendar, SelectedCalendar, Credential } from "@prisma/client";
|
||||
import { TFunction } from "next-i18next";
|
||||
|
||||
import { PaymentInfo } from "@ee/lib/stripe/server";
|
||||
|
||||
import { PaymentInfo } from "@lib/integrations/payment/constants/types";
|
||||
import { Ensure } from "@lib/types/utils";
|
||||
import { VideoCallData } from "@lib/videoClient";
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@ import _ from "lodash";
|
|||
* https://github.com/microsoft/playwright/issues/7121
|
||||
*/
|
||||
import { validJson } from "../../lib/jsonUtils";
|
||||
import { PAYMENT_INTEGRATIONS_TYPES } from "../integrations/payment/constants/generals";
|
||||
|
||||
const credentialData = Prisma.validator<Prisma.CredentialArgs>()({
|
||||
select: { id: true, type: true },
|
||||
|
@ -84,7 +85,7 @@ export const ALL_INTEGRATIONS = [
|
|||
process.env.NEXT_PUBLIC_STRIPE_PUBLIC_KEY &&
|
||||
process.env.STRIPE_PRIVATE_KEY
|
||||
),
|
||||
type: "stripe_payment",
|
||||
type: PAYMENT_INTEGRATIONS_TYPES.stripe,
|
||||
title: "Stripe",
|
||||
imageSrc: "integrations/stripe.svg",
|
||||
description: "Collect payments",
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
import { Credential } from "@prisma/client";
|
||||
|
||||
import { PAYMENT_INTEGRATIONS_TYPES } from "@lib/integrations/payment/constants/generals";
|
||||
import { PaymentMethodServiceType } from "@lib/integrations/payment/constants/types";
|
||||
import PaymentService from "@lib/integrations/payment/services/BasePaymentService";
|
||||
import StripePaymentService from "@lib/integrations/payment/services/StripePaymentService";
|
||||
import logger from "@lib/logger";
|
||||
|
||||
const log = logger.getChildLogger({ prefix: ["PaymentManager"] });
|
||||
|
||||
const PAYMENT_METHODS: Record<string, PaymentMethodServiceType> = {
|
||||
[PAYMENT_INTEGRATIONS_TYPES.stripe]: StripePaymentService,
|
||||
};
|
||||
|
||||
export const getPaymentMethod = (credential: Credential): PaymentService | null => {
|
||||
const { type } = credential;
|
||||
|
||||
const paymentMethod = PAYMENT_METHODS[type];
|
||||
if (!paymentMethod) {
|
||||
log.warn(`payment method of type ${type} does not implemented`);
|
||||
return null;
|
||||
}
|
||||
|
||||
return new paymentMethod(credential);
|
||||
};
|
|
@ -0,0 +1 @@
|
|||
export const DEFAULT_PAYMENT_METHOD_INTEGRATION_NAME = "";
|
|
@ -0,0 +1,7 @@
|
|||
export const paymentFeeFixed = process.env.PAYMENT_FEE_FIXED!;
|
||||
|
||||
export const paymentFeePercentage = process.env.PAYMENT_FEE_PERCENTAGE!;
|
||||
|
||||
export const PAYMENT_INTEGRATIONS_TYPES = {
|
||||
stripe: "stripe_payment",
|
||||
};
|
|
@ -0,0 +1,9 @@
|
|||
import Stripe from "stripe";
|
||||
|
||||
const { STRIPE_PRIVATE_KEY } = process.env;
|
||||
|
||||
export const stripePrivateKey = STRIPE_PRIVATE_KEY!;
|
||||
|
||||
export const STRIPE = new Stripe(stripePrivateKey, {
|
||||
apiVersion: "2020-08-27",
|
||||
});
|
|
@ -0,0 +1,20 @@
|
|||
import Stripe from "stripe";
|
||||
|
||||
import StripePaymentService from "@lib/integrations/payment/services/StripePaymentService";
|
||||
|
||||
export type PaymentInfo = {
|
||||
link?: string | null;
|
||||
reason?: string | null;
|
||||
id?: string | null;
|
||||
};
|
||||
|
||||
export type PaymentData = Stripe.Response<Stripe.PaymentIntent> & {
|
||||
stripe_publishable_key: string;
|
||||
stripeAccount: string;
|
||||
};
|
||||
|
||||
export type PaymentMethodServiceType = typeof StripePaymentService;
|
||||
|
||||
export type StripeData = Stripe.OAuthToken & {
|
||||
default_currency: string;
|
||||
};
|
|
@ -0,0 +1,67 @@
|
|||
import { Payment, PaymentType, Prisma } from "@prisma/client";
|
||||
|
||||
import { CalendarEvent } from "@lib/integrations/calendar/interfaces/Calendar";
|
||||
|
||||
import { Maybe } from "@trpc/server";
|
||||
|
||||
export interface PaymentSelectedEventType {
|
||||
price: number;
|
||||
currency: string;
|
||||
}
|
||||
|
||||
export interface BookingDetail {
|
||||
user: {
|
||||
email: string | null;
|
||||
name: string | null;
|
||||
timeZone: string;
|
||||
} | null;
|
||||
id: number;
|
||||
startTime: {
|
||||
toISOString: () => string;
|
||||
};
|
||||
uid: string;
|
||||
}
|
||||
|
||||
export interface BookingRefundDetail {
|
||||
id: number;
|
||||
uid: string;
|
||||
startTime: Date;
|
||||
payment: {
|
||||
id: number;
|
||||
success: boolean;
|
||||
refunded: boolean;
|
||||
externalId: string;
|
||||
data: Prisma.JsonValue;
|
||||
type: PaymentType;
|
||||
}[];
|
||||
}
|
||||
|
||||
export interface bookingRefundError {
|
||||
event: CalendarEvent;
|
||||
reason: string;
|
||||
paymentId: string;
|
||||
}
|
||||
|
||||
export interface PaymentMethodCredential {
|
||||
key: Prisma.JsonValue;
|
||||
}
|
||||
|
||||
export interface PaymentLinkDetail {
|
||||
paymentUid: string;
|
||||
name?: Maybe<string>;
|
||||
date?: Maybe<string>;
|
||||
absolute?: boolean;
|
||||
}
|
||||
|
||||
export interface PaymentMethod {
|
||||
handlePayment(
|
||||
event: CalendarEvent,
|
||||
selectedEventType: PaymentSelectedEventType,
|
||||
credential: PaymentMethodCredential,
|
||||
booking: BookingDetail
|
||||
): Promise<Payment>;
|
||||
|
||||
refund(booking: BookingRefundDetail, event: CalendarEvent): Promise<void>;
|
||||
|
||||
handleRefundError(opts: bookingRefundError): Promise<void>;
|
||||
}
|
|
@ -0,0 +1,70 @@
|
|||
import { Payment, Credential } from "@prisma/client";
|
||||
|
||||
import { sendOrganizerPaymentRefundFailedEmail } from "@lib/emails/email-manager";
|
||||
import { getErrorFromUnknown } from "@lib/errors";
|
||||
import { CalendarEvent } from "@lib/integrations/calendar/interfaces/Calendar";
|
||||
import { DEFAULT_PAYMENT_METHOD_INTEGRATION_NAME } from "@lib/integrations/payment/constants/defaults";
|
||||
import {
|
||||
BookingDetail,
|
||||
BookingRefundDetail,
|
||||
bookingRefundError,
|
||||
PaymentMethod,
|
||||
PaymentMethodCredential,
|
||||
PaymentSelectedEventType,
|
||||
} from "@lib/integrations/payment/interfaces/PaymentMethod";
|
||||
import logger from "@lib/logger";
|
||||
|
||||
export default abstract class BasePaymentService implements PaymentMethod {
|
||||
protected integrationName = DEFAULT_PAYMENT_METHOD_INTEGRATION_NAME;
|
||||
|
||||
log = logger.getChildLogger({ prefix: [`[[lib] ${this.integrationName}`] });
|
||||
|
||||
constructor(_credential: Credential, integrationName: string) {
|
||||
this.integrationName = integrationName;
|
||||
}
|
||||
|
||||
handlePayment(
|
||||
event: CalendarEvent,
|
||||
selectedEventType: PaymentSelectedEventType,
|
||||
_credential: PaymentMethodCredential,
|
||||
booking: BookingDetail
|
||||
): Promise<Payment> {
|
||||
this.log.info(
|
||||
`handle payment for ${JSON.stringify(event)}, payment ${JSON.stringify(
|
||||
selectedEventType
|
||||
)} and booking ${JSON.stringify(booking)}`
|
||||
);
|
||||
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
|
||||
async refund(booking: BookingRefundDetail, event: CalendarEvent): Promise<void> {
|
||||
try {
|
||||
const payment = booking.payment.find((e) => e.success && !e.refunded);
|
||||
if (!payment) return new Promise((resolve) => resolve());
|
||||
|
||||
await this.handleRefundError({
|
||||
event: event,
|
||||
reason: "cannot refund non Stripe payment",
|
||||
paymentId: "unknown",
|
||||
});
|
||||
} catch (e) {
|
||||
const err = getErrorFromUnknown(e);
|
||||
console.error(err, "Refund failed");
|
||||
await this.handleRefundError({
|
||||
event: event,
|
||||
reason: err.message || "unknown",
|
||||
paymentId: "unknown",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async handleRefundError(opts: bookingRefundError): Promise<void> {
|
||||
console.error(`refund failed: ${opts.reason} for booking '${opts.event.uid}'`);
|
||||
|
||||
await sendOrganizerPaymentRefundFailedEmail({
|
||||
...opts.event,
|
||||
paymentInfo: { reason: opts.reason, id: opts.paymentId },
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,125 @@
|
|||
import { PaymentType, Prisma, Credential } from "@prisma/client";
|
||||
import Stripe from "stripe";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
|
||||
import { sendAwaitingPaymentEmail } from "@lib/emails/email-manager";
|
||||
import { getErrorFromUnknown } from "@lib/errors";
|
||||
import { CalendarEvent } from "@lib/integrations/calendar/interfaces/Calendar";
|
||||
import {
|
||||
paymentFeeFixed,
|
||||
paymentFeePercentage,
|
||||
PAYMENT_INTEGRATIONS_TYPES,
|
||||
} from "@lib/integrations/payment/constants/generals";
|
||||
import { STRIPE } from "@lib/integrations/payment/constants/stripeConstats";
|
||||
import { PaymentData } from "@lib/integrations/payment/constants/types";
|
||||
import {
|
||||
BookingDetail,
|
||||
BookingRefundDetail,
|
||||
PaymentMethodCredential,
|
||||
PaymentSelectedEventType,
|
||||
} from "@lib/integrations/payment/interfaces/PaymentMethod";
|
||||
import BasePaymentService from "@lib/integrations/payment/services/BasePaymentService";
|
||||
import { createPaymentLink } from "@lib/integrations/payment/utils/stripeUtils";
|
||||
import prisma from "@lib/prisma";
|
||||
|
||||
export default class StripePaymentService extends BasePaymentService {
|
||||
constructor(credential: Credential) {
|
||||
super(credential, PAYMENT_INTEGRATIONS_TYPES.stripe);
|
||||
}
|
||||
|
||||
async handlePayment(
|
||||
event: CalendarEvent,
|
||||
selectedEventType: PaymentSelectedEventType,
|
||||
credential: PaymentMethodCredential,
|
||||
booking: BookingDetail
|
||||
) {
|
||||
const paymentFee = Math.round(
|
||||
selectedEventType.price * parseFloat(`${paymentFeePercentage}`) + parseInt(`${paymentFeeFixed}`)
|
||||
);
|
||||
const { stripe_user_id, stripe_publishable_key } = credential.key as Stripe.OAuthToken;
|
||||
|
||||
const params: Stripe.PaymentIntentCreateParams = {
|
||||
amount: selectedEventType.price,
|
||||
currency: selectedEventType.currency,
|
||||
payment_method_types: ["card"],
|
||||
application_fee_amount: paymentFee,
|
||||
};
|
||||
|
||||
const paymentIntent = await STRIPE.paymentIntents.create(params, { stripeAccount: stripe_user_id });
|
||||
|
||||
const payment = await prisma.payment.create({
|
||||
data: {
|
||||
type: PaymentType.STRIPE,
|
||||
uid: uuidv4(),
|
||||
booking: {
|
||||
connect: {
|
||||
id: booking.id,
|
||||
},
|
||||
},
|
||||
amount: selectedEventType.price,
|
||||
fee: paymentFee,
|
||||
currency: selectedEventType.currency,
|
||||
success: false,
|
||||
refunded: false,
|
||||
data: Object.assign({}, paymentIntent, {
|
||||
stripe_publishable_key,
|
||||
stripeAccount: stripe_user_id,
|
||||
}) /* We should treat this */ as PaymentData /* but Prisma doesn't know how to handle it, so it we treat it */ as unknown /* and then */ as Prisma.InputJsonValue,
|
||||
externalId: paymentIntent.id,
|
||||
},
|
||||
});
|
||||
|
||||
await sendAwaitingPaymentEmail({
|
||||
...event,
|
||||
paymentInfo: {
|
||||
link: createPaymentLink({
|
||||
paymentUid: payment.uid,
|
||||
name: booking.user?.name,
|
||||
date: booking.startTime.toISOString(),
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
return payment;
|
||||
}
|
||||
|
||||
async refund(booking: BookingRefundDetail, event: CalendarEvent): Promise<void> {
|
||||
try {
|
||||
const payment = booking.payment.find((e) => e.success && !e.refunded);
|
||||
if (!payment) return;
|
||||
|
||||
const refund = await STRIPE.refunds.create(
|
||||
{
|
||||
payment_intent: payment.externalId,
|
||||
},
|
||||
{ stripeAccount: (payment.data as unknown as PaymentData)["stripeAccount"] }
|
||||
);
|
||||
|
||||
if (!refund || refund.status === "failed") {
|
||||
await this.handleRefundError({
|
||||
event: event,
|
||||
reason: refund?.failure_reason || "unknown",
|
||||
paymentId: payment.externalId,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
await prisma.payment.update({
|
||||
where: {
|
||||
id: payment.id,
|
||||
},
|
||||
data: {
|
||||
refunded: true,
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
const err = getErrorFromUnknown(e);
|
||||
console.error(err, "Refund failed");
|
||||
await this.handleRefundError({
|
||||
event: event,
|
||||
reason: err.message || "unknown",
|
||||
paymentId: "unknown",
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,15 +1,14 @@
|
|||
import { loadStripe, Stripe } from "@stripe/stripe-js";
|
||||
import { stringify } from "querystring";
|
||||
|
||||
import { Maybe } from "@trpc/server";
|
||||
import { PaymentLinkDetail } from "@lib/integrations/payment/interfaces/PaymentMethod";
|
||||
|
||||
const stripePublicKey = process.env.NEXT_PUBLIC_STRIPE_PUBLIC_KEY!;
|
||||
const { NEXT_PUBLIC_STRIPE_PUBLIC_KEY } = process.env;
|
||||
|
||||
const stripePublicKey = NEXT_PUBLIC_STRIPE_PUBLIC_KEY!;
|
||||
let stripePromise: Promise<Stripe | null>;
|
||||
|
||||
/**
|
||||
* This is a singleton to ensure we only instantiate Stripe once.
|
||||
*/
|
||||
const getStripe = (userPublicKey?: string) => {
|
||||
export const getStripe = (userPublicKey?: string) => {
|
||||
if (!stripePromise) {
|
||||
stripePromise = loadStripe(
|
||||
userPublicKey || stripePublicKey /* , {
|
||||
|
@ -20,17 +19,10 @@ const getStripe = (userPublicKey?: string) => {
|
|||
return stripePromise;
|
||||
};
|
||||
|
||||
export function createPaymentLink(opts: {
|
||||
paymentUid: string;
|
||||
name?: Maybe<string>;
|
||||
date?: Maybe<string>;
|
||||
absolute?: boolean;
|
||||
}): string {
|
||||
export const createPaymentLink = (opts: PaymentLinkDetail): string => {
|
||||
const { paymentUid, name, date, absolute = true } = opts;
|
||||
let link = "";
|
||||
if (absolute) link = process.env.NEXT_PUBLIC_APP_URL!;
|
||||
const query = stringify({ date, name });
|
||||
return link + `/payment/${paymentUid}?${query}`;
|
||||
}
|
||||
|
||||
export default getStripe;
|
||||
};
|
|
@ -1,13 +1,13 @@
|
|||
import { Prisma, User, Booking, SchedulingType, BookingStatus } from "@prisma/client";
|
||||
import { Prisma, User, Booking, SchedulingType, BookingStatus, Credential } from "@prisma/client";
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
import { refund } from "@ee/lib/stripe/server";
|
||||
|
||||
import { getSession } from "@lib/auth";
|
||||
import { sendDeclinedEmails } from "@lib/emails/email-manager";
|
||||
import { sendScheduledEmails } from "@lib/emails/email-manager";
|
||||
import EventManager from "@lib/events/EventManager";
|
||||
import { CalendarEvent, AdditionInformation } from "@lib/integrations/calendar/interfaces/Calendar";
|
||||
import { getPaymentMethod } from "@lib/integrations/payment/PaymentManager";
|
||||
import { PAYMENT_INTEGRATIONS_TYPES } from "@lib/integrations/payment/constants/generals";
|
||||
import logger from "@lib/logger";
|
||||
import prisma from "@lib/prisma";
|
||||
import { BookingConfirmBody } from "@lib/types/booking";
|
||||
|
@ -167,7 +167,8 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||
|
||||
res.status(204).end();
|
||||
} else {
|
||||
await refund(booking, evt);
|
||||
const paymentMethod = getPaymentMethod({ type: PAYMENT_INTEGRATIONS_TYPES.stripe } as Credential);
|
||||
await paymentMethod?.refund(booking, evt);
|
||||
|
||||
await prisma.booking.update({
|
||||
where: {
|
||||
|
|
|
@ -9,8 +9,6 @@ import type { NextApiRequest, NextApiResponse } from "next";
|
|||
import short from "short-uuid";
|
||||
import { v5 as uuidv5 } from "uuid";
|
||||
|
||||
import { handlePayment } from "@ee/lib/stripe/server";
|
||||
|
||||
import {
|
||||
sendScheduledEmails,
|
||||
sendRescheduledEmails,
|
||||
|
@ -23,6 +21,8 @@ import EventManager, { EventResult, PartialReference } from "@lib/events/EventMa
|
|||
import { getBusyCalendarTimes } from "@lib/integrations/calendar/CalendarManager";
|
||||
import { CalendarEvent, AdditionInformation } from "@lib/integrations/calendar/interfaces/Calendar";
|
||||
import { BufferedBusyTime } from "@lib/integrations/calendar/interfaces/Office365Calendar";
|
||||
import { getPaymentMethod } from "@lib/integrations/payment/PaymentManager";
|
||||
import { PAYMENT_INTEGRATIONS_TYPES } from "@lib/integrations/payment/constants/generals";
|
||||
import logger from "@lib/logger";
|
||||
import notEmpty from "@lib/notEmpty";
|
||||
import prisma from "@lib/prisma";
|
||||
|
@ -520,11 +520,15 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||
|
||||
if (typeof eventType.price === "number" && eventType.price > 0) {
|
||||
try {
|
||||
const [firstStripeCredential] = user.credentials.filter((cred) => cred.type == "stripe_payment");
|
||||
const [firstStripeCredential] = user.credentials.filter(
|
||||
(cred) => cred.type == PAYMENT_INTEGRATIONS_TYPES.stripe
|
||||
);
|
||||
if (!booking.user) booking.user = user;
|
||||
const payment = await handlePayment(evt, eventType, firstStripeCredential, booking);
|
||||
|
||||
res.status(201).json({ ...booking, message: "Payment required", paymentUid: payment.uid });
|
||||
const paymentMethod = getPaymentMethod({ type: PAYMENT_INTEGRATIONS_TYPES.stripe } as Credential);
|
||||
const payment = await paymentMethod?.handlePayment(evt, eventType, firstStripeCredential, booking);
|
||||
|
||||
res.status(201).json({ ...booking, message: "Payment required", paymentUid: payment?.uid });
|
||||
return;
|
||||
} catch (e) {
|
||||
log.error(`Creating payment failed`, e);
|
||||
|
|
|
@ -1,15 +1,15 @@
|
|||
import { BookingStatus } from "@prisma/client";
|
||||
import { BookingStatus, Credential } from "@prisma/client";
|
||||
import async from "async";
|
||||
import { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
import { refund } from "@ee/lib/stripe/server";
|
||||
|
||||
import { asStringOrNull } from "@lib/asStringOrNull";
|
||||
import { getSession } from "@lib/auth";
|
||||
import { sendCancelledEmails } from "@lib/emails/email-manager";
|
||||
import { FAKE_DAILY_CREDENTIAL } from "@lib/integrations/Daily/DailyVideoApiAdapter";
|
||||
import { getCalendar } from "@lib/integrations/calendar/CalendarManager";
|
||||
import { CalendarEvent } from "@lib/integrations/calendar/interfaces/Calendar";
|
||||
import { getPaymentMethod } from "@lib/integrations/payment/PaymentManager";
|
||||
import { PAYMENT_INTEGRATIONS_TYPES } from "@lib/integrations/payment/constants/generals";
|
||||
import prisma from "@lib/prisma";
|
||||
import { deleteMeeting } from "@lib/videoClient";
|
||||
import sendPayload from "@lib/webhooks/sendPayload";
|
||||
|
@ -165,7 +165,9 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||
uid: bookingToDelete.uid ?? "",
|
||||
language: t,
|
||||
};
|
||||
await refund(bookingToDelete, evt);
|
||||
|
||||
const paymentMethod = getPaymentMethod({ type: PAYMENT_INTEGRATIONS_TYPES.stripe } as Credential);
|
||||
await paymentMethod?.refund(bookingToDelete, evt);
|
||||
await prisma.booking.update({
|
||||
where: {
|
||||
id: bookingToDelete.id,
|
||||
|
|
|
@ -26,13 +26,13 @@ import { FormattedNumber, IntlProvider } from "react-intl";
|
|||
import { useMutation } from "react-query";
|
||||
import Select from "react-select";
|
||||
|
||||
import { StripeData } from "@ee/lib/stripe/server";
|
||||
|
||||
import { asNumberOrUndefined, asStringOrThrow, asStringOrUndefined } from "@lib/asStringOrNull";
|
||||
import { getSession } from "@lib/auth";
|
||||
import { HttpError } from "@lib/core/http/error";
|
||||
import { useLocale } from "@lib/hooks/useLocale";
|
||||
import getIntegrations, { hasIntegration } from "@lib/integrations/getIntegrations";
|
||||
import { PAYMENT_INTEGRATIONS_TYPES } from "@lib/integrations/payment/constants/generals";
|
||||
import { StripeData } from "@lib/integrations/payment/constants/types";
|
||||
import { LocationType } from "@lib/location";
|
||||
import deleteEventType from "@lib/mutations/event-types/delete-event-type";
|
||||
import updateEventType from "@lib/mutations/event-types/update-event-type";
|
||||
|
@ -1442,7 +1442,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
|||
if (hasIntegration(integrations, "zoom_video")) {
|
||||
locationOptions.push({ value: LocationType.Zoom, label: "Zoom Video", disabled: true });
|
||||
}
|
||||
const hasPaymentIntegration = hasIntegration(integrations, "stripe_payment");
|
||||
const hasPaymentIntegration = hasIntegration(integrations, PAYMENT_INTEGRATIONS_TYPES.stripe);
|
||||
if (hasIntegration(integrations, "google_calendar")) {
|
||||
locationOptions.push({ value: LocationType.GoogleMeet, label: "Google Meet" });
|
||||
}
|
||||
|
@ -1450,8 +1450,10 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
|||
locationOptions.push({ value: LocationType.Daily, label: "Daily.co Video" });
|
||||
}
|
||||
const currency =
|
||||
(credentials.find((integration) => integration.type === "stripe_payment")?.key as unknown as StripeData)
|
||||
?.default_currency || "usd";
|
||||
(
|
||||
credentials.find((integration) => integration.type === PAYMENT_INTEGRATIONS_TYPES.stripe)
|
||||
?.key as unknown as StripeData
|
||||
)?.default_currency || "usd";
|
||||
|
||||
if (hasIntegration(integrations, "office365_calendar")) {
|
||||
// TODO: Add default meeting option of the office integration.
|
||||
|
|
|
@ -29,55 +29,36 @@ test.describe.serial("Google calendar integration", () => {
|
|||
});
|
||||
|
||||
await Promise.all([
|
||||
page.waitForNavigation(/*{ url: 'https://accounts.google.com/o/oauth2/v2/auth/identifier?access_type=offline&scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fcalendar.readonly%20https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fcalendar.events&prompt=consent&state=%7B%22returnTo%22%3A%22%2Fintegrations%22%7D&response_type=code&client_id=807439123749-1pjdju71hol2tcmr4ce7h99a6p7bh4b4.apps.googleusercontent.com&redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Fapi%2Fintegrations%2Fgooglecalendar%2Fcallback&flowName=GeneralOAuthFlow' }*/),
|
||||
page.click(
|
||||
'text=Google CalendarFor personal and business calendarsConnect >> [data-testid="integration-connection-button"]'
|
||||
),
|
||||
page.waitForNavigation({ url: "https://accounts.google.com/o/oauth2/v2/auth/identifier?*" }),
|
||||
page.click('li:has-text("Google Calendar") >> [data-testid="integration-connection-button"]'),
|
||||
]);
|
||||
|
||||
/** We start the Stripe flow */
|
||||
await page.goto(
|
||||
"https://accounts.google.com/o/oauth2/v2/auth?access_type=offline&scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fcalendar.readonly%20https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fcalendar.events&prompt=consent&state=%7B%22returnTo%22%3A%22%2Fintegrations%22%7D&response_type=code&client_id=807439123749-1pjdju71hol2tcmr4ce7h99a6p7bh4b4.apps.googleusercontent.com&redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Fapi%2Fintegrations%2Fgooglecalendar%2Fcallback"
|
||||
);
|
||||
|
||||
await page.goto(
|
||||
"https://accounts.google.com/o/oauth2/v2/auth/identifier?access_type=offline&scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fcalendar.readonly%20https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fcalendar.events&prompt=consent&state=%7B%22returnTo%22%3A%22%2Fintegrations%22%7D&response_type=code&client_id=807439123749-1pjdju71hol2tcmr4ce7h99a6p7bh4b4.apps.googleusercontent.com&redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Fapi%2Fintegrations%2Fgooglecalendar%2Fcallback&flowName=GeneralOAuthFlow"
|
||||
);
|
||||
|
||||
await page.click('[aria-label="Correo electrónico o teléfono"]');
|
||||
|
||||
await page.fill('[aria-label="Correo electrónico o teléfono"]', "appstest647@gmail.com");
|
||||
await page.fill('[id="identifierId"]', "appstest647@gmail.com");
|
||||
|
||||
await Promise.all([
|
||||
page.waitForNavigation(/*{ url: 'https://accounts.google.com/signin/v2/challenge/pwd?access_type=offline&scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fcalendar.readonly%20https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fcalendar.events&prompt=consent&state=%7B%22returnTo%22%3A%22%2Fintegrations%22%7D&response_type=code&client_id=807439123749-1pjdju71hol2tcmr4ce7h99a6p7bh4b4.apps.googleusercontent.com&redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Fapi%2Fintegrations%2Fgooglecalendar%2Fcallback&flowName=GeneralOAuthFlow&cid=1&navigationDirection=forward&TL=AM3QAYYRpKiZPgQ3OiJXVKpjvb7oI9B9onW6syFiJlBPLS_ADnrNniEXQL0TsTn-' }*/),
|
||||
page.click('button:has-text("Siguiente")'),
|
||||
page.waitForNavigation({ url: "https://accounts.google.com/signin/v2/challenge/pwd?*" }),
|
||||
page.press('[id="identifierId"]', "Enter"),
|
||||
]);
|
||||
|
||||
await page.click('[aria-label="Ingresa tu contraseña"]');
|
||||
|
||||
await page.fill('[aria-label="Ingresa tu contraseña"]', "aW4TDL9GQ55_01");
|
||||
await page.fill('[type="password"]', "aW4TDL9GQ55_01");
|
||||
|
||||
// Press Enter
|
||||
await Promise.all([
|
||||
page.waitForNavigation(/*{ url: 'https://accounts.google.com/signin/oauth/warning?authuser=0&part=AJi8hANmAtMQ-UhmjdCwpNNwGOIshO2BwRJhgAPWhuGd8ehakEoTJTx4KICzosVLmJv1CuUzvvyUHYvuEEKzo865V9i7sxDAf1ApvqopHYJUH4-6cTJhTV2B6pP8U-3v2gRMkEhFYzldGgnDZ3WHyWj3Nfg3TOgiRuS2k4KRoeLr-w__O2jrX8JYaLTMHiTghpWcE9WyPmaMPgLuzbFYat1Sne9BkIze5-AiWF1TsyBLr8uLcBN3pKk7y1eh6L4nlu0XKotZ1wnHgXsi06roHn1KK9yeo48-FwghsX2phheIp654vYc12SjELu0dsjWkXTpfj1X22Xc8fQK31xToqf0tdClmuOSDwEacQmgpZJlncChn7tVAopvTkO5v0wxkrGNy7wKQZMDffpvoCxDyV4ew4zNcT6DOn-oRU0ySiMwuOlZqutn23IcTKEdQiLp33JaacRY6JLQGoA86-InikC75xP9stBby6IZ8HmD_uCPdyaQslfMQWAQ&as=S1091477452%3A1640725683048124&client_id=807439123749-1pjdju71hol2tcmr4ce7h99a6p7bh4b4.apps.googleusercontent.com&rapt=AEjHL4PVP3TevMulM-iUTkKiZSr7x-vNMq4Z7tX2XChPaH99eCoXHrEe7rk2EpBMTKTb9ESEo0QDayW5BRFQLy8KALF3tdQXnw#' }*/),
|
||||
page.press('[aria-label="Ingresa tu contraseña"]', "Enter"),
|
||||
page.waitForNavigation({ url: "https://accounts.google.com/signin/oauth/v2/consentsummary?*" }),
|
||||
page.press('[type="password"]', "Enter"),
|
||||
]);
|
||||
|
||||
// Click button:has-text("Continuar")
|
||||
await Promise.all([
|
||||
page.waitForNavigation(/*{ url: 'https://accounts.google.com/signin/oauth/v2/consentsummary?authuser=0&part=AJi8hANvD4yF7dFpqAflbW06NmSRziM4oeh0SM3zrURKqItAdML4h7BPLE8fQJTMIOkC2OHYFapOIn0YHuhFLSI3cVJ0Wvr1eTmnTNx1iX7XfGHBLCe-HzAfYUIHbAbhwhhUGGPBxryZKB92cLXFLVH-ZjFz2Nm2JALBdupdHsKuGMKw5-6AqV-cQqKK5EWtd46QBo2scRMa0DaV0OOBkWap47J7JbSVei8OqkKkenVaWgnZBKp94T7zL39YkxUsuQaHOr1SHdlZai083fRbD0YV5vBkyJZFfpnb-1J1V7D-uCRHloPubKKTzdLcggik0RvfYgbfY4PRJMmAF7rOjCne2ppYlkmitHbehQ7gSP-AIW1GY5t1b7w37ofvMSqhJn9ImEBCeksZO0iWQG9MyQGwnZZVviNr-5TBu8yZr5OeExBqG-wJpbmffM4RG5iSH1Jz1zK2pxS1iENSHiaGDA-vaqin2zOzzHZX1DTdz84CC7ILe_xLGnApTedaFhIgUdBt06ohnAWaqPY4mncmcPKW52TtqY4vAKAK4oE1tIo0d_cLSbQ9M2vk6CcSOeUtSaGj5_KqYpGGH2oFCyTJuwRhRFN8CzbGKG6bH267DAdhJLjHa6hvK5U3ACtrBm2J9PO4khSxwAVCMd0IQL2tTMdzOH3ZPFAfpTK0p5GTR_pHgtljgBOO1lZPXX8pXuAncTFKThy6hQ5z2lfXqQrTRNsNLMZ9Kyb0PIvF-TOVK6eNP1o76OPSJ1nHP690YJRbDaYFCxWBmtd76ar2mVxs4hn_OLMqFqYq9uzPrq6_dFSKtPXglFxJx85T7AILluOglIOqdfdhjpecxA4WzeFCJS7bOBFnasOHBySn2wvfJv_IFaLJTBv7CGUWq-ZyVROVflfipur6g7zZkBu5hvUJR7ZKTdv9vrvtUQ&hl=es&as=S1091477452%3A1640725683048124&client_id=807439123749-1pjdju71hol2tcmr4ce7h99a6p7bh4b4.apps.googleusercontent.com&rapt=AEjHL4PVP3TevMulM-iUTkKiZSr7x-vNMq4Z7tX2XChPaH99eCoXHrEe7rk2EpBMTKTb9ESEo0QDayW5BRFQLy8KALF3tdQXnw' }*/),
|
||||
page.click('button:has-text("Continuar")'),
|
||||
]);
|
||||
// When connecting an account for the first time, you need to check some chekboxes
|
||||
// await page.check('[type="checkbox"]');
|
||||
|
||||
// Click button:has-text("Continuar")
|
||||
await Promise.all([
|
||||
page.waitForNavigation(/*{ url: 'http://localhost:3000/integrations' }*/),
|
||||
page.click('button:has-text("Continuar")'),
|
||||
page.waitForNavigation({ url: "/integrations" }),
|
||||
page.click('[id="submit_approve_access"]:has(button)'),
|
||||
]);
|
||||
|
||||
page.waitForEvent("load");
|
||||
|
||||
/** If Stripe is added correctly we should see the "Disconnect" button */
|
||||
/** If Google Calendar is added correctly we should see the "Disconnect" button */
|
||||
expect(
|
||||
page.locator(
|
||||
`div:has-text("Calendars") + li:has-text("Google Calendar") >> [data-testid="integration-connection-button"]`
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
import { Browser, chromium } from "@playwright/test";
|
||||
import fs from "fs";
|
||||
|
||||
async function loginAsUser(username: string, browser: Browser) {
|
||||
// Skip is file exists
|
||||
if (fs.existsSync(`playwright/artifacts/${username}StorageState.json`)) return;
|
||||
const page = await browser.newPage();
|
||||
await page.goto(`${process.env.PLAYWRIGHT_TEST_BASE_URL}/auth/login`);
|
||||
// Click input[name="email"]
|
||||
|
|
Loading…
Reference in New Issue