fix: saving credential id for payment apps (#12251)

## What does this PR do?

<!-- Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. List any dependencies that are required for this change. -->

This PR adds the `credentialId` to payment app data. This fixes a bug where team installed payment apps were not working with team events.

Fixes # (issue)

<!-- Please provide a loom video for visual changes to speed up reviews
 Loom Video: https://www.loom.com/
-->

## Requirement/Documentation

<!-- Please provide all documents that are important to understand the reason of that PR. -->

- If there is a requirement document, please, share it here.
- If there is ab UI/UX design document, please, share it here.

## Type of change

<!-- Please delete bullets that are not relevant. -->

- [x] Bug fix (non-breaking change which fixes an issue)

## How should this be tested?

<!-- Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration. Write details that help to start the tests -->

- Install Stripe for the individual user
- Enable it for the individual's event type
- The `credentialId` should be saved to the metadata
- Install Stripe to the user's team
- Enable it in the team's event type
- The `credentialId` should be saved to the metadata

## Mandatory Tasks

- [ ] Make sure you have self-reviewed the code. A decent size PR without self-review might be rejected.

## Checklist

<!-- Remove bullet points below that don't apply to you -->

- I haven't checked if new and existing unit tests pass locally with my changes
This commit is contained in:
Joe Au-Yeung 2023-11-15 21:59:43 -05:00 committed by GitHub
parent 6a8726f5f8
commit ea0a64624c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 101 additions and 5 deletions

View File

@ -47,7 +47,7 @@ export const EventAppsTab = ({ eventType }: { eventType: EventType }) => {
};
};
const getAppDataSetter = (appId: EventTypeAppsList): SetAppData => {
const getAppDataSetter = (appId: EventTypeAppsList, credentialId?: number): SetAppData => {
return function (key, value) {
// Always get latest data available in Form because consequent calls to setData would update the Form but not allAppsData(it would update during next render)
const allAppsDataFromForm = methods.getValues("metadata")?.apps || {};
@ -57,6 +57,7 @@ export const EventAppsTab = ({ eventType }: { eventType: EventType }) => {
[appId]: {
...appData,
[key]: value,
credentialId,
},
});
};
@ -76,7 +77,7 @@ export const EventAppsTab = ({ eventType }: { eventType: EventType }) => {
appCards.push(
<EventTypeAppCard
getAppData={getAppDataGetter(app.slug as EventTypeAppsList)}
setAppData={getAppDataSetter(app.slug as EventTypeAppsList)}
setAppData={getAppDataSetter(app.slug as EventTypeAppsList, app.userCredentialIds[0])}
key={app.slug}
app={app}
eventType={eventType}
@ -90,7 +91,7 @@ export const EventAppsTab = ({ eventType }: { eventType: EventType }) => {
appCards.push(
<EventTypeAppCard
getAppData={getAppDataGetter(app.slug as EventTypeAppsList)}
setAppData={getAppDataSetter(app.slug as EventTypeAppsList)}
setAppData={getAppDataSetter(app.slug as EventTypeAppsList, team.credentialId)}
key={app.slug + team?.credentialId}
app={{
...app,
@ -147,7 +148,7 @@ export const EventAppsTab = ({ eventType }: { eventType: EventType }) => {
return (
<EventTypeAppCard
getAppData={getAppDataGetter(app.slug as EventTypeAppsList)}
setAppData={getAppDataSetter(app.slug as EventTypeAppsList)}
setAppData={getAppDataSetter(app.slug as EventTypeAppsList, app.userCredentialIds[0])}
key={app.slug}
app={app}
eventType={eventType}

View File

@ -651,7 +651,7 @@ export async function apiLogin(
export async function setupEventWithPrice(eventType: Pick<Prisma.EventType, "id">, page: Page) {
await page.goto(`/event-types/${eventType?.id}?tabName=apps`);
await page.locator("div > .ml-auto").first().click();
await page.locator("[data-testid='app-switch']").first().click();
await page.getByPlaceholder("Price").fill("100");
await page.getByTestId("update-eventtype").click();
}

View File

@ -1,6 +1,10 @@
import { expect } from "@playwright/test";
import type Prisma from "@prisma/client";
import prisma from "@calcom/prisma";
import { SchedulingType } from "@calcom/prisma/enums";
import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
import { test } from "./lib/fixtures";
import type { Fixtures } from "./lib/fixtures";
import { todo, selectFirstAvailableTimeSlotNextMonth } from "./lib/testUtils";
@ -34,6 +38,95 @@ test.describe("Stripe integration", () => {
});
});
test("when enabling Stripe, credentialId is included", async ({ page, users }) => {
const user = await users.create();
await user.apiLogin();
await page.goto("/apps/installed");
await user.getPaymentCredential();
const eventType = user.eventTypes.find((e) => e.slug === "paid") as Prisma.EventType;
await user.setupEventWithPrice(eventType);
// Need to wait for the DB to be updated with the metadata
await page.waitForResponse((res) => res.url().includes("update") && res.status() === 200);
// Check event type metadata to see if credentialId is included
const eventTypeMetadata = await prisma.eventType.findFirst({
where: {
id: eventType.id,
},
select: {
metadata: true,
},
});
const metadata = EventTypeMetaDataSchema.parse(eventTypeMetadata?.metadata);
const stripeAppMetadata = metadata?.apps?.stripe;
expect(stripeAppMetadata).toHaveProperty("credentialId");
expect(typeof stripeAppMetadata?.credentialId).toBe("number");
});
test("when enabling Stripe, team credentialId is included", async ({ page, users }) => {
const ownerObj = { username: "pro-user", name: "pro-user" };
const teamMatesObj = [
{ name: "teammate-1" },
{ name: "teammate-2" },
{ name: "teammate-3" },
{ name: "teammate-4" },
];
const owner = await users.create(ownerObj, {
hasTeam: true,
teammates: teamMatesObj,
schedulingType: SchedulingType.COLLECTIVE,
});
await owner.apiLogin();
const { team } = await owner.getFirstTeam();
const { title: teamEventTitle, slug: teamEventSlug } = await owner.getFirstTeamEvent(team.id);
const teamEvent = await owner.getFirstTeamEvent(team.id);
await page.goto("/apps/stripe");
/** We start the Stripe flow */
await Promise.all([
page.waitForURL("https://connect.stripe.com/oauth/v2/authorize?*"),
page.click('[data-testid="install-app-button"]'),
page.click('[data-testid="anything else"]'),
]);
await Promise.all([
page.waitForURL("/apps/installed/payment?hl=stripe"),
/** We skip filling Stripe forms (testing mode only) */
page.click('[id="skip-account-app"]'),
]);
await owner.setupEventWithPrice(teamEvent);
// Need to wait for the DB to be updated with the metadata
await page.waitForResponse((res) => res.url().includes("update") && res.status() === 200);
// Check event type metadata to see if credentialId is included
const eventTypeMetadata = await prisma.eventType.findFirst({
where: {
id: teamEvent.id,
},
select: {
metadata: true,
},
});
const metadata = EventTypeMetaDataSchema.parse(eventTypeMetadata?.metadata);
const stripeAppMetadata = metadata?.apps?.stripe;
expect(stripeAppMetadata).toHaveProperty("credentialId");
expect(typeof stripeAppMetadata?.credentialId).toBe("number");
});
test("Can book a paid booking", async ({ page, users }) => {
const user = await users.create();
const eventType = user.eventTypes.find((e) => e.slug === "paid") as Prisma.EventType;

View File

@ -90,6 +90,7 @@ export default function AppCard({
{app?.isInstalled || app.credentialOwner ? (
<div className="ml-auto flex items-center">
<Switch
data-testid="app-switch"
disabled={!app.enabled || disabled}
onCheckedChange={(enabled) => {
if (switchOnClick) {

View File

@ -29,6 +29,7 @@ export const appDataSchema = eventTypeAppCardZod.merge(
currency: z.string(),
paymentOption: z.string().optional(),
enabled: z.boolean().optional(),
credentialId: z.number().optional(),
})
);
export const appKeysSchema = z.object({