fix: Update Event Type Pricing For Multiple Installed Payment Apps (#12272)
* Prevent two payment apps from being enabled * Find the enabled payment app to update the event type * Add string * Add tests * Type fix * Abstract check for multiple payment app logic * Type check * Address feedback * chore: Enable One Payment App Per Event Type (#12414) Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com> Co-authored-by: Morgan Vernay <morgan@cal.com> * Fix bug * Fix test * Clean up * Fix test * Fix test --------- Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com> Co-authored-by: Peer Richelsen <peeroke@gmail.com> Co-authored-by: Alex van Andel <me@alexvanandel.com> Co-authored-by: Omar López <zomars@me.com> Co-authored-by: Morgan Vernay <morgan@cal.com> Co-authored-by: Keith Williams <keithwillcode@gmail.com>
This commit is contained in:
parent
68d40cabbe
commit
d07e86e4f3
|
@ -47,10 +47,17 @@ export const EventAppsTab = ({ eventType }: { eventType: EventType }) => {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const getAppDataSetter = (appId: EventTypeAppsList, credentialId?: number): SetAppData => {
|
const eventTypeFormMetadata = methods.getValues("metadata");
|
||||||
|
|
||||||
|
const getAppDataSetter = (
|
||||||
|
appId: EventTypeAppsList,
|
||||||
|
appCategories: string[],
|
||||||
|
credentialId?: number
|
||||||
|
): SetAppData => {
|
||||||
return function (key, value) {
|
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)
|
// 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 || {};
|
const allAppsDataFromForm = methods.getValues("metadata")?.apps || {};
|
||||||
|
|
||||||
const appData = allAppsDataFromForm[appId];
|
const appData = allAppsDataFromForm[appId];
|
||||||
setAllAppsData({
|
setAllAppsData({
|
||||||
...allAppsDataFromForm,
|
...allAppsDataFromForm,
|
||||||
|
@ -58,6 +65,7 @@ export const EventAppsTab = ({ eventType }: { eventType: EventType }) => {
|
||||||
...appData,
|
...appData,
|
||||||
[key]: value,
|
[key]: value,
|
||||||
credentialId,
|
credentialId,
|
||||||
|
appCategories,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -77,10 +85,15 @@ export const EventAppsTab = ({ eventType }: { eventType: EventType }) => {
|
||||||
appCards.push(
|
appCards.push(
|
||||||
<EventTypeAppCard
|
<EventTypeAppCard
|
||||||
getAppData={getAppDataGetter(app.slug as EventTypeAppsList)}
|
getAppData={getAppDataGetter(app.slug as EventTypeAppsList)}
|
||||||
setAppData={getAppDataSetter(app.slug as EventTypeAppsList, app.userCredentialIds[0])}
|
setAppData={getAppDataSetter(
|
||||||
|
app.slug as EventTypeAppsList,
|
||||||
|
app.categories,
|
||||||
|
app.userCredentialIds[0]
|
||||||
|
)}
|
||||||
key={app.slug}
|
key={app.slug}
|
||||||
app={app}
|
app={app}
|
||||||
eventType={eventType}
|
eventType={eventType}
|
||||||
|
eventTypeFormMetadata={eventTypeFormMetadata}
|
||||||
{...shouldLockDisableProps("apps")}
|
{...shouldLockDisableProps("apps")}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
@ -91,7 +104,7 @@ export const EventAppsTab = ({ eventType }: { eventType: EventType }) => {
|
||||||
appCards.push(
|
appCards.push(
|
||||||
<EventTypeAppCard
|
<EventTypeAppCard
|
||||||
getAppData={getAppDataGetter(app.slug as EventTypeAppsList)}
|
getAppData={getAppDataGetter(app.slug as EventTypeAppsList)}
|
||||||
setAppData={getAppDataSetter(app.slug as EventTypeAppsList, team.credentialId)}
|
setAppData={getAppDataSetter(app.slug as EventTypeAppsList, app.categories, team.credentialId)}
|
||||||
key={app.slug + team?.credentialId}
|
key={app.slug + team?.credentialId}
|
||||||
app={{
|
app={{
|
||||||
...app,
|
...app,
|
||||||
|
@ -104,6 +117,7 @@ export const EventAppsTab = ({ eventType }: { eventType: EventType }) => {
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
eventType={eventType}
|
eventType={eventType}
|
||||||
|
eventTypeFormMetadata={eventTypeFormMetadata}
|
||||||
{...shouldLockDisableProps("apps")}
|
{...shouldLockDisableProps("apps")}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
@ -148,10 +162,15 @@ export const EventAppsTab = ({ eventType }: { eventType: EventType }) => {
|
||||||
return (
|
return (
|
||||||
<EventTypeAppCard
|
<EventTypeAppCard
|
||||||
getAppData={getAppDataGetter(app.slug as EventTypeAppsList)}
|
getAppData={getAppDataGetter(app.slug as EventTypeAppsList)}
|
||||||
setAppData={getAppDataSetter(app.slug as EventTypeAppsList, app.userCredentialIds[0])}
|
setAppData={getAppDataSetter(
|
||||||
|
app.slug as EventTypeAppsList,
|
||||||
|
app.categories,
|
||||||
|
app.userCredentialIds[0]
|
||||||
|
)}
|
||||||
key={app.slug}
|
key={app.slug}
|
||||||
app={app}
|
app={app}
|
||||||
eventType={eventType}
|
eventType={eventType}
|
||||||
|
eventTypeFormMetadata={eventTypeFormMetadata}
|
||||||
{...shouldLockDisableProps("apps")}
|
{...shouldLockDisableProps("apps")}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
@ -179,10 +198,11 @@ export const EventAppsTab = ({ eventType }: { eventType: EventType }) => {
|
||||||
{notInstalledApps?.map((app) => (
|
{notInstalledApps?.map((app) => (
|
||||||
<EventTypeAppCard
|
<EventTypeAppCard
|
||||||
getAppData={getAppDataGetter(app.slug as EventTypeAppsList)}
|
getAppData={getAppDataGetter(app.slug as EventTypeAppsList)}
|
||||||
setAppData={getAppDataSetter(app.slug as EventTypeAppsList)}
|
setAppData={getAppDataSetter(app.slug as EventTypeAppsList, app.categories)}
|
||||||
key={app.slug}
|
key={app.slug}
|
||||||
app={app}
|
app={app}
|
||||||
eventType={eventType}
|
eventType={eventType}
|
||||||
|
eventTypeFormMetadata={eventTypeFormMetadata}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -8,6 +8,7 @@ import { useEffect, useMemo, useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import checkForMultiplePaymentApps from "@calcom/app-store/_utils/payments/checkForMultiplePaymentApps";
|
||||||
import { getEventLocationType } from "@calcom/app-store/locations";
|
import { getEventLocationType } from "@calcom/app-store/locations";
|
||||||
import { validateCustomEventName } from "@calcom/core/event";
|
import { validateCustomEventName } from "@calcom/core/event";
|
||||||
import type { EventLocationType } from "@calcom/core/location";
|
import type { EventLocationType } from "@calcom/core/location";
|
||||||
|
@ -484,6 +485,11 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Prevent two payment apps to be enabled
|
||||||
|
// Ok to cast type here because this metadata will be updated as the event type metadata
|
||||||
|
if (checkForMultiplePaymentApps(metadata as z.infer<typeof EventTypeMetaDataSchema>))
|
||||||
|
throw new Error(t("event_setup_multiple_payment_apps_error"));
|
||||||
|
|
||||||
if (metadata?.apps?.stripe?.paymentOption === "HOLD" && seatsPerTimeSlot) {
|
if (metadata?.apps?.stripe?.paymentOption === "HOLD" && seatsPerTimeSlot) {
|
||||||
throw new Error(t("seats_and_no_show_fee_error"));
|
throw new Error(t("seats_and_no_show_fee_error"));
|
||||||
}
|
}
|
||||||
|
@ -584,6 +590,12 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Prevent two payment apps to be enabled
|
||||||
|
// Ok to cast type here because this metadata will be updated as the event type metadata
|
||||||
|
if (checkForMultiplePaymentApps(metadata as z.infer<typeof EventTypeMetaDataSchema>))
|
||||||
|
throw new Error(t("event_setup_multiple_payment_apps_error"));
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
const { availability, ...rest } = input;
|
const { availability, ...rest } = input;
|
||||||
updateMutation.mutate({
|
updateMutation.mutate({
|
||||||
|
|
|
@ -508,8 +508,8 @@ const createUserFixture = (user: UserWithIncludes, page: Page) => {
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
getPaymentCredential: async () => getPaymentCredential(store.page),
|
getPaymentCredential: async () => getPaymentCredential(store.page),
|
||||||
setupEventWithPrice: async (eventType: Pick<Prisma.EventType, "id">) =>
|
setupEventWithPrice: async (eventType: Pick<Prisma.EventType, "id">, slug: string) =>
|
||||||
setupEventWithPrice(eventType, store.page),
|
setupEventWithPrice(eventType, slug, store.page),
|
||||||
bookAndPayEvent: async (eventType: Pick<Prisma.EventType, "slug">) =>
|
bookAndPayEvent: async (eventType: Pick<Prisma.EventType, "slug">) =>
|
||||||
bookAndPayEvent(user, eventType, store.page),
|
bookAndPayEvent(user, eventType, store.page),
|
||||||
makePaymentUsingStripe: async () => makePaymentUsingStripe(store.page),
|
makePaymentUsingStripe: async () => makePaymentUsingStripe(store.page),
|
||||||
|
@ -693,9 +693,9 @@ export async function apiLogin(
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function setupEventWithPrice(eventType: Pick<Prisma.EventType, "id">, page: Page) {
|
export async function setupEventWithPrice(eventType: Pick<Prisma.EventType, "id">, slug: string, page: Page) {
|
||||||
await page.goto(`/event-types/${eventType?.id}?tabName=apps`);
|
await page.goto(`/event-types/${eventType?.id}?tabName=apps`);
|
||||||
await page.locator("[data-testid='app-switch']").first().click();
|
await page.locator(`[data-testid='${slug}-app-switch']`).first().click();
|
||||||
await page.getByPlaceholder("Price").fill("100");
|
await page.getByPlaceholder("Price").fill("100");
|
||||||
await page.getByTestId("update-eventtype").click();
|
await page.getByTestId("update-eventtype").click();
|
||||||
}
|
}
|
||||||
|
|
|
@ -46,7 +46,7 @@ test.describe("Stripe integration", () => {
|
||||||
await user.getPaymentCredential();
|
await user.getPaymentCredential();
|
||||||
|
|
||||||
const eventType = user.eventTypes.find((e) => e.slug === "paid") as Prisma.EventType;
|
const eventType = user.eventTypes.find((e) => e.slug === "paid") as Prisma.EventType;
|
||||||
await user.setupEventWithPrice(eventType);
|
await user.setupEventWithPrice(eventType, "stripe");
|
||||||
|
|
||||||
// Need to wait for the DB to be updated with the metadata
|
// Need to wait for the DB to be updated with the metadata
|
||||||
await page.waitForResponse((res) => res.url().includes("update") && res.status() === 200);
|
await page.waitForResponse((res) => res.url().includes("update") && res.status() === 200);
|
||||||
|
@ -104,7 +104,7 @@ test.describe("Stripe integration", () => {
|
||||||
page.click('[id="skip-account-app"]'),
|
page.click('[id="skip-account-app"]'),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
await owner.setupEventWithPrice(teamEvent);
|
await owner.setupEventWithPrice(teamEvent, "stripe");
|
||||||
|
|
||||||
// Need to wait for the DB to be updated with the metadata
|
// Need to wait for the DB to be updated with the metadata
|
||||||
await page.waitForResponse((res) => res.url().includes("update") && res.status() === 200);
|
await page.waitForResponse((res) => res.url().includes("update") && res.status() === 200);
|
||||||
|
@ -134,7 +134,7 @@ test.describe("Stripe integration", () => {
|
||||||
await page.goto("/apps/installed");
|
await page.goto("/apps/installed");
|
||||||
|
|
||||||
await user.getPaymentCredential();
|
await user.getPaymentCredential();
|
||||||
await user.setupEventWithPrice(eventType);
|
await user.setupEventWithPrice(eventType, "stripe");
|
||||||
await user.bookAndPayEvent(eventType);
|
await user.bookAndPayEvent(eventType);
|
||||||
// success
|
// success
|
||||||
await expect(page.locator("[data-testid=success-page]")).toBeVisible();
|
await expect(page.locator("[data-testid=success-page]")).toBeVisible();
|
||||||
|
@ -147,7 +147,7 @@ test.describe("Stripe integration", () => {
|
||||||
await page.goto("/apps/installed");
|
await page.goto("/apps/installed");
|
||||||
|
|
||||||
await user.getPaymentCredential();
|
await user.getPaymentCredential();
|
||||||
await user.setupEventWithPrice(eventType);
|
await user.setupEventWithPrice(eventType, "stripe");
|
||||||
|
|
||||||
// booking process without payment
|
// booking process without payment
|
||||||
await page.goto(`${user.username}/${eventType?.slug}`);
|
await page.goto(`${user.username}/${eventType?.slug}`);
|
||||||
|
@ -171,7 +171,7 @@ test.describe("Stripe integration", () => {
|
||||||
await page.goto("/apps/installed");
|
await page.goto("/apps/installed");
|
||||||
|
|
||||||
await user.getPaymentCredential();
|
await user.getPaymentCredential();
|
||||||
await user.setupEventWithPrice(eventType);
|
await user.setupEventWithPrice(eventType, "stripe");
|
||||||
await user.bookAndPayEvent(eventType);
|
await user.bookAndPayEvent(eventType);
|
||||||
|
|
||||||
// Rescheduling the event
|
// Rescheduling the event
|
||||||
|
@ -194,7 +194,7 @@ test.describe("Stripe integration", () => {
|
||||||
await page.goto("/apps/installed");
|
await page.goto("/apps/installed");
|
||||||
|
|
||||||
await user.getPaymentCredential();
|
await user.getPaymentCredential();
|
||||||
await user.setupEventWithPrice(eventType);
|
await user.setupEventWithPrice(eventType, "stripe");
|
||||||
await user.bookAndPayEvent(eventType);
|
await user.bookAndPayEvent(eventType);
|
||||||
|
|
||||||
await page.click('[data-testid="cancel"]');
|
await page.click('[data-testid="cancel"]');
|
||||||
|
@ -214,7 +214,7 @@ test.describe("Stripe integration", () => {
|
||||||
await page.goto("/apps/installed");
|
await page.goto("/apps/installed");
|
||||||
|
|
||||||
await user.getPaymentCredential();
|
await user.getPaymentCredential();
|
||||||
await user.setupEventWithPrice(eventType);
|
await user.setupEventWithPrice(eventType, "stripe");
|
||||||
await user.bookAndPayEvent(eventType);
|
await user.bookAndPayEvent(eventType);
|
||||||
await user.confirmPendingPayment();
|
await user.confirmPendingPayment();
|
||||||
});
|
});
|
||||||
|
@ -264,7 +264,7 @@ test.describe("Stripe integration", () => {
|
||||||
await page.locator("#event-type-form").getByRole("switch").click();
|
await page.locator("#event-type-form").getByRole("switch").click();
|
||||||
|
|
||||||
// Set price
|
// Set price
|
||||||
await page.getByTestId("price-input-stripe").fill("200");
|
await page.getByTestId("stripe-price-input").fill("200");
|
||||||
|
|
||||||
// Select currency in dropdown
|
// Select currency in dropdown
|
||||||
await page.getByTestId("stripe-currency-select").click();
|
await page.getByTestId("stripe-currency-select").click();
|
||||||
|
|
|
@ -80,8 +80,8 @@ test.describe("Payment app", () => {
|
||||||
await page.getByTestId("stripe-currency-select").click();
|
await page.getByTestId("stripe-currency-select").click();
|
||||||
await page.getByTestId("select-option-usd").click();
|
await page.getByTestId("select-option-usd").click();
|
||||||
|
|
||||||
await page.getByTestId("price-input-stripe").click();
|
await page.getByTestId("stripe-price-input").click();
|
||||||
await page.getByTestId("price-input-stripe").fill("350");
|
await page.getByTestId("stripe-price-input").fill("350");
|
||||||
await page.getByTestId("update-eventtype").click();
|
await page.getByTestId("update-eventtype").click();
|
||||||
|
|
||||||
await page.goto(`${user.username}/${paymentEvent.slug}`);
|
await page.goto(`${user.username}/${paymentEvent.slug}`);
|
||||||
|
@ -234,4 +234,128 @@ test.describe("Payment app", () => {
|
||||||
await page.getByLabel("Tracking ID").fill("demo");
|
await page.getByLabel("Tracking ID").fill("demo");
|
||||||
await page.getByTestId("update-eventtype").click();
|
await page.getByTestId("update-eventtype").click();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("Should only be allowed to enable one payment app", async ({ page, users }) => {
|
||||||
|
const user = await users.create();
|
||||||
|
await user.apiLogin();
|
||||||
|
const paymentEvent = user.eventTypes.find((item) => item.slug === "paid");
|
||||||
|
if (!paymentEvent) {
|
||||||
|
throw new Error("No payment event found");
|
||||||
|
}
|
||||||
|
await prisma.credential.createMany({
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
type: "paypal_payment",
|
||||||
|
userId: user.id,
|
||||||
|
key: {
|
||||||
|
client_id: "randomString",
|
||||||
|
secret_key: "randomString",
|
||||||
|
webhook_id: "randomString",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "stripe_payment",
|
||||||
|
userId: user.id,
|
||||||
|
key: {
|
||||||
|
scope: "read_write",
|
||||||
|
livemode: false,
|
||||||
|
token_type: "bearer",
|
||||||
|
access_token: "sk_test_randomString",
|
||||||
|
refresh_token: "rt_randomString",
|
||||||
|
stripe_user_id: "acct_randomString",
|
||||||
|
default_currency: "usd",
|
||||||
|
stripe_publishable_key: "pk_test_randomString",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.goto(`event-types/${paymentEvent.id}?tabName=apps`);
|
||||||
|
|
||||||
|
await page.locator("[data-testid='paypal-app-switch']").click();
|
||||||
|
await page.locator("[data-testid='stripe-app-switch']").isDisabled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("when more than one payment app is installed the price should be updated when changing settings", async ({
|
||||||
|
page,
|
||||||
|
users,
|
||||||
|
}) => {
|
||||||
|
const user = await users.create();
|
||||||
|
await user.apiLogin();
|
||||||
|
const paymentEvent = user.eventTypes.find((item) => item.slug === "paid");
|
||||||
|
if (!paymentEvent) {
|
||||||
|
throw new Error("No payment event found");
|
||||||
|
}
|
||||||
|
|
||||||
|
await prisma.credential.createMany({
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
type: "paypal_payment",
|
||||||
|
userId: user.id,
|
||||||
|
key: {
|
||||||
|
client_id: "randomString",
|
||||||
|
secret_key: "randomString",
|
||||||
|
webhook_id: "randomString",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "stripe_payment",
|
||||||
|
userId: user.id,
|
||||||
|
key: {
|
||||||
|
scope: "read_write",
|
||||||
|
livemode: false,
|
||||||
|
token_type: "bearer",
|
||||||
|
access_token: "sk_test_randomString",
|
||||||
|
refresh_token: "rt_randomString",
|
||||||
|
stripe_user_id: "acct_randomString",
|
||||||
|
default_currency: "usd",
|
||||||
|
stripe_publishable_key: "pk_test_randomString",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.goto(`event-types/${paymentEvent.id}?tabName=apps`);
|
||||||
|
|
||||||
|
await page.locator("[data-testid='paypal-app-switch']").click();
|
||||||
|
await page.locator("[data-testid='paypal-price-input']").fill("100");
|
||||||
|
await page.locator("[data-testid='paypal-currency-select']").first().click();
|
||||||
|
await page.locator("#react-select-2-option-13").click();
|
||||||
|
// await page.locator(".mb-1 > .bg-default > div > div:nth-child(2)").first().click();
|
||||||
|
// await page.getByText("$MXNCurrencyMexican pesoPayment option").click();
|
||||||
|
await page.locator("[data-testid='update-eventtype']").click();
|
||||||
|
|
||||||
|
// Need to wait for the DB to be updated
|
||||||
|
await page.waitForResponse((res) => res.url().includes("update") && res.status() === 200);
|
||||||
|
|
||||||
|
const paypalPrice = await prisma.eventType.findFirst({
|
||||||
|
where: {
|
||||||
|
id: paymentEvent.id,
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
price: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(paypalPrice?.price).toEqual(10000);
|
||||||
|
|
||||||
|
await page.locator("[data-testid='paypal-app-switch']").click();
|
||||||
|
await page.locator("[data-testid='stripe-app-switch']").click();
|
||||||
|
await page.locator("[data-testid='stripe-price-input']").fill("200");
|
||||||
|
await page.locator("[data-testid='update-eventtype']").click();
|
||||||
|
|
||||||
|
// Need to wait for the DB to be updated
|
||||||
|
await page.waitForResponse((res) => res.url().includes("update") && res.status() === 200);
|
||||||
|
|
||||||
|
const stripePrice = await prisma.eventType.findFirst({
|
||||||
|
where: {
|
||||||
|
id: paymentEvent.id,
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
price: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(stripePrice?.price).toEqual(20000);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -2158,6 +2158,7 @@
|
||||||
"manage_availability_schedules":"Manage availability schedules",
|
"manage_availability_schedules":"Manage availability schedules",
|
||||||
"lock_timezone_toggle_on_booking_page": "Lock timezone on booking page",
|
"lock_timezone_toggle_on_booking_page": "Lock timezone on booking page",
|
||||||
"description_lock_timezone_toggle_on_booking_page" : "To lock the timezone on booking page, useful for in-person events.",
|
"description_lock_timezone_toggle_on_booking_page" : "To lock the timezone on booking page, useful for in-person events.",
|
||||||
|
"event_setup_multiple_payment_apps_error": "You can only have one payment app enabled per event type.",
|
||||||
"number_in_international_format": "Please enter number in international format.",
|
"number_in_international_format": "Please enter number in international format.",
|
||||||
"install_calendar":"Install Calendar",
|
"install_calendar":"Install Calendar",
|
||||||
"branded_subdomain": "Branded Subdomain",
|
"branded_subdomain": "Branded Subdomain",
|
||||||
|
@ -2175,6 +2176,7 @@
|
||||||
"enterprise_description": "Upgrade to Enterprise to create your Organization",
|
"enterprise_description": "Upgrade to Enterprise to create your Organization",
|
||||||
"create_your_org": "Create your Organization",
|
"create_your_org": "Create your Organization",
|
||||||
"create_your_org_description": "Upgrade to Enterprise and receive a subdomain, unified billing, Insights, extensive whitelabeling and more",
|
"create_your_org_description": "Upgrade to Enterprise and receive a subdomain, unified billing, Insights, extensive whitelabeling and more",
|
||||||
|
"other_payment_app_enabled": "You can only enable one payment app per event type",
|
||||||
"admin_delete_organization_description": "<ul><li>Teams that are member of this organization will also be deleted along with their event-types</li><li>Users that were part of the organization will not be deleted and their event-types will also remain intact.</li><li>Usernames would be changed to allow them to exist outside the organization</li></ul>",
|
"admin_delete_organization_description": "<ul><li>Teams that are member of this organization will also be deleted along with their event-types</li><li>Users that were part of the organization will not be deleted and their event-types will also remain intact.</li><li>Usernames would be changed to allow them to exist outside the organization</li></ul>",
|
||||||
"admin_delete_organization_title": "Delete organization?",
|
"admin_delete_organization_title": "Delete organization?",
|
||||||
"published": "Published",
|
"published": "Published",
|
||||||
|
|
|
@ -19,6 +19,8 @@ export default function AppCard({
|
||||||
children,
|
children,
|
||||||
returnTo,
|
returnTo,
|
||||||
teamId,
|
teamId,
|
||||||
|
disableSwitch,
|
||||||
|
switchTooltip,
|
||||||
}: {
|
}: {
|
||||||
app: RouterOutputs["viewer"]["integrations"]["items"][number] & { credentialOwner?: CredentialOwner };
|
app: RouterOutputs["viewer"]["integrations"]["items"][number] & { credentialOwner?: CredentialOwner };
|
||||||
description?: React.ReactNode;
|
description?: React.ReactNode;
|
||||||
|
@ -28,10 +30,12 @@ export default function AppCard({
|
||||||
returnTo?: string;
|
returnTo?: string;
|
||||||
teamId?: number;
|
teamId?: number;
|
||||||
LockedIcon?: React.ReactNode;
|
LockedIcon?: React.ReactNode;
|
||||||
|
disableSwitch?: boolean;
|
||||||
|
switchTooltip?: string;
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [animationRef] = useAutoAnimate<HTMLDivElement>();
|
const [animationRef] = useAutoAnimate<HTMLDivElement>();
|
||||||
const { setAppData, LockedIcon, disabled } = useAppContextWithSchema();
|
const { setAppData, LockedIcon, disabled: managedDisabled } = useAppContextWithSchema();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
@ -91,7 +95,7 @@ export default function AppCard({
|
||||||
<div className="ml-auto flex items-center">
|
<div className="ml-auto flex items-center">
|
||||||
<Switch
|
<Switch
|
||||||
data-testid="app-switch"
|
data-testid="app-switch"
|
||||||
disabled={!app.enabled || disabled}
|
disabled={!app.enabled || managedDisabled || disableSwitch}
|
||||||
onCheckedChange={(enabled) => {
|
onCheckedChange={(enabled) => {
|
||||||
if (switchOnClick) {
|
if (switchOnClick) {
|
||||||
switchOnClick(enabled);
|
switchOnClick(enabled);
|
||||||
|
@ -100,6 +104,8 @@ export default function AppCard({
|
||||||
}}
|
}}
|
||||||
checked={switchChecked}
|
checked={switchChecked}
|
||||||
LockedIcon={LockedIcon}
|
LockedIcon={LockedIcon}
|
||||||
|
data-testId={`${app.slug}-app-switch`}
|
||||||
|
tooltip={switchTooltip}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
|
import type z from "zod";
|
||||||
|
|
||||||
import type { GetAppData, SetAppData } from "@calcom/app-store/EventTypeAppContext";
|
import type { GetAppData, SetAppData } from "@calcom/app-store/EventTypeAppContext";
|
||||||
import EventTypeAppContext from "@calcom/app-store/EventTypeAppContext";
|
import EventTypeAppContext from "@calcom/app-store/EventTypeAppContext";
|
||||||
import { EventTypeAddonMap } from "@calcom/app-store/apps.browser.generated";
|
import { EventTypeAddonMap } from "@calcom/app-store/apps.browser.generated";
|
||||||
|
import type { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
||||||
import type { RouterOutputs } from "@calcom/trpc/react";
|
import type { RouterOutputs } from "@calcom/trpc/react";
|
||||||
import { ErrorBoundary } from "@calcom/ui";
|
import { ErrorBoundary } from "@calcom/ui";
|
||||||
|
|
||||||
|
@ -14,6 +17,7 @@ export const EventTypeAppCard = (props: {
|
||||||
setAppData: SetAppData;
|
setAppData: SetAppData;
|
||||||
// For event type apps, get these props from shouldLockDisableProps
|
// For event type apps, get these props from shouldLockDisableProps
|
||||||
LockedIcon?: JSX.Element | false;
|
LockedIcon?: JSX.Element | false;
|
||||||
|
eventTypeFormMetadata: z.infer<typeof EventTypeMetaDataSchema>;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
const { app, getAppData, setAppData, LockedIcon, disabled } = props;
|
const { app, getAppData, setAppData, LockedIcon, disabled } = props;
|
||||||
|
|
|
@ -0,0 +1,34 @@
|
||||||
|
import type z from "zod";
|
||||||
|
|
||||||
|
import type { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
||||||
|
|
||||||
|
import type { appDataSchemas } from "../../apps.schemas.generated";
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param metadata The event type metadata
|
||||||
|
* @param inclusive Determines if multiple includes the case of 1
|
||||||
|
* @returns boolean
|
||||||
|
*/
|
||||||
|
const checkForMultiplePaymentApps = (
|
||||||
|
metadata: z.infer<typeof EventTypeMetaDataSchema>,
|
||||||
|
inclusive = false
|
||||||
|
) => {
|
||||||
|
let enabledPaymentApps = 0;
|
||||||
|
for (const appKey in metadata?.apps) {
|
||||||
|
const app = metadata?.apps[appKey as keyof typeof appDataSchemas];
|
||||||
|
|
||||||
|
if ("appCategories" in app) {
|
||||||
|
const isPaymentApp = app.appCategories.includes("payment");
|
||||||
|
if (isPaymentApp && app.enabled) {
|
||||||
|
enabledPaymentApps++;
|
||||||
|
}
|
||||||
|
} else if ("price" in app && app.enabled) {
|
||||||
|
enabledPaymentApps++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return inclusive ? enabledPaymentApps >= 1 : enabledPaymentApps > 1;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default checkForMultiplePaymentApps;
|
|
@ -10,12 +10,17 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { Alert, Select, TextField } from "@calcom/ui";
|
import { Alert, Select, TextField } from "@calcom/ui";
|
||||||
import { SatSymbol } from "@calcom/ui/components/icon/SatSymbol";
|
import { SatSymbol } from "@calcom/ui/components/icon/SatSymbol";
|
||||||
|
|
||||||
|
import checkForMultiplePaymentApps from "../../_utils/payments/checkForMultiplePaymentApps";
|
||||||
import type { appDataSchema } from "../zod";
|
import type { appDataSchema } from "../zod";
|
||||||
import { PaypalPaymentOptions as paymentOptions } from "../zod";
|
import { PaypalPaymentOptions as paymentOptions } from "../zod";
|
||||||
|
|
||||||
type Option = { value: string; label: string };
|
type Option = { value: string; label: string };
|
||||||
|
|
||||||
const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({ app, eventType }) {
|
const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({
|
||||||
|
app,
|
||||||
|
eventType,
|
||||||
|
eventTypeFormMetadata,
|
||||||
|
}) {
|
||||||
const { asPath } = useRouter();
|
const { asPath } = useRouter();
|
||||||
const { getAppData, setAppData } = useAppContextWithSchema<typeof appDataSchema>();
|
const { getAppData, setAppData } = useAppContextWithSchema<typeof appDataSchema>();
|
||||||
const price = getAppData("price");
|
const price = getAppData("price");
|
||||||
|
@ -32,6 +37,9 @@ const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({
|
||||||
const [requirePayment, setRequirePayment] = useState(getAppData("enabled"));
|
const [requirePayment, setRequirePayment] = useState(getAppData("enabled"));
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
const recurringEventDefined = eventType.recurringEvent?.count !== undefined;
|
const recurringEventDefined = eventType.recurringEvent?.count !== undefined;
|
||||||
|
const otherPaymentAppEnabled = checkForMultiplePaymentApps(eventTypeFormMetadata);
|
||||||
|
|
||||||
|
const shouldDisableSwitch = !requirePayment && otherPaymentAppEnabled;
|
||||||
|
|
||||||
// make sure a currency is selected
|
// make sure a currency is selected
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -48,7 +56,9 @@ const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({
|
||||||
switchOnClick={(enabled) => {
|
switchOnClick={(enabled) => {
|
||||||
setRequirePayment(enabled);
|
setRequirePayment(enabled);
|
||||||
}}
|
}}
|
||||||
description={<>Add bitcoin lightning payments to your events</>}>
|
description={<>Add bitcoin lightning payments to your events</>}
|
||||||
|
disableSwitch={shouldDisableSwitch}
|
||||||
|
switchTooltip={shouldDisableSwitch ? t("other_payment_app_enabled") : undefined}>
|
||||||
<>
|
<>
|
||||||
{recurringEventDefined ? (
|
{recurringEventDefined ? (
|
||||||
<Alert className="mt-2" severity="warning" title={t("warning_recurring_event_payment")} />
|
<Alert className="mt-2" severity="warning" title={t("warning_recurring_event_payment")} />
|
||||||
|
|
|
@ -4,6 +4,7 @@ import { z } from "zod";
|
||||||
export const eventTypeAppCardZod = z.object({
|
export const eventTypeAppCardZod = z.object({
|
||||||
enabled: z.boolean().optional(),
|
enabled: z.boolean().optional(),
|
||||||
credentialId: z.number().optional(),
|
credentialId: z.number().optional(),
|
||||||
|
appCategories: z.array(z.string()).optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const appKeysSchema = z.object({});
|
export const appKeysSchema = z.object({});
|
||||||
|
|
|
@ -13,12 +13,17 @@ import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { Alert, Select, TextField } from "@calcom/ui";
|
import { Alert, Select, TextField } from "@calcom/ui";
|
||||||
|
|
||||||
|
import checkForMultiplePaymentApps from "../../_utils/payments/checkForMultiplePaymentApps";
|
||||||
import type { appDataSchema } from "../zod";
|
import type { appDataSchema } from "../zod";
|
||||||
import { PaypalPaymentOptions as paymentOptions } from "../zod";
|
import { PaypalPaymentOptions as paymentOptions } from "../zod";
|
||||||
|
|
||||||
type Option = { value: string; label: string };
|
type Option = { value: string; label: string };
|
||||||
|
|
||||||
const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({ app, eventType }) {
|
const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({
|
||||||
|
app,
|
||||||
|
eventType,
|
||||||
|
eventTypeFormMetadata,
|
||||||
|
}) {
|
||||||
const { asPath } = useRouter();
|
const { asPath } = useRouter();
|
||||||
const { getAppData, setAppData } = useAppContextWithSchema<typeof appDataSchema>();
|
const { getAppData, setAppData } = useAppContextWithSchema<typeof appDataSchema>();
|
||||||
const price = getAppData("price");
|
const price = getAppData("price");
|
||||||
|
@ -38,6 +43,9 @@ const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({
|
||||||
const [requirePayment, setRequirePayment] = useState(getAppData("enabled"));
|
const [requirePayment, setRequirePayment] = useState(getAppData("enabled"));
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
const recurringEventDefined = eventType.recurringEvent?.count !== undefined;
|
const recurringEventDefined = eventType.recurringEvent?.count !== undefined;
|
||||||
|
const otherPaymentAppEnabled = checkForMultiplePaymentApps(eventTypeFormMetadata);
|
||||||
|
|
||||||
|
const shouldDisableSwitch = !requirePayment && otherPaymentAppEnabled;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (requirePayment) {
|
if (requirePayment) {
|
||||||
|
@ -58,7 +66,9 @@ const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({
|
||||||
switchOnClick={(enabled) => {
|
switchOnClick={(enabled) => {
|
||||||
setRequirePayment(enabled);
|
setRequirePayment(enabled);
|
||||||
}}
|
}}
|
||||||
description={<>Add Paypal payment to your events</>}>
|
description={<>Add Paypal payment to your events</>}
|
||||||
|
disableSwitch={shouldDisableSwitch}
|
||||||
|
switchTooltip={shouldDisableSwitch ? t("other_payment_app_enabled") : undefined}>
|
||||||
<>
|
<>
|
||||||
{recurringEventDefined ? (
|
{recurringEventDefined ? (
|
||||||
<Alert className="mt-2" severity="warning" title={t("warning_recurring_event_payment")} />
|
<Alert className="mt-2" severity="warning" title={t("warning_recurring_event_payment")} />
|
||||||
|
@ -77,6 +87,7 @@ const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({
|
||||||
required
|
required
|
||||||
className="block w-full rounded-sm pl-2 text-sm"
|
className="block w-full rounded-sm pl-2 text-sm"
|
||||||
placeholder="Price"
|
placeholder="Price"
|
||||||
|
data-testid="paypal-price-input"
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
setAppData("price", Number(e.target.value) * 100);
|
setAppData("price", Number(e.target.value) * 100);
|
||||||
if (selectedCurrency) {
|
if (selectedCurrency) {
|
||||||
|
|
|
@ -8,6 +8,7 @@ import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { Alert, Select, TextField } from "@calcom/ui";
|
import { Alert, Select, TextField } from "@calcom/ui";
|
||||||
|
|
||||||
|
import checkForMultiplePaymentApps from "../../_utils/payments/checkForMultiplePaymentApps";
|
||||||
import { paymentOptions } from "../lib/constants";
|
import { paymentOptions } from "../lib/constants";
|
||||||
import {
|
import {
|
||||||
convertToSmallestCurrencyUnit,
|
convertToSmallestCurrencyUnit,
|
||||||
|
@ -18,7 +19,11 @@ import type { appDataSchema } from "../zod";
|
||||||
|
|
||||||
type Option = { value: string; label: string };
|
type Option = { value: string; label: string };
|
||||||
|
|
||||||
const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({ app, eventType }) {
|
const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({
|
||||||
|
app,
|
||||||
|
eventType,
|
||||||
|
eventTypeFormMetadata,
|
||||||
|
}) {
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
const { getAppData, setAppData, disabled } = useAppContextWithSchema<typeof appDataSchema>();
|
const { getAppData, setAppData, disabled } = useAppContextWithSchema<typeof appDataSchema>();
|
||||||
const price = getAppData("price");
|
const price = getAppData("price");
|
||||||
|
@ -32,6 +37,9 @@ const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({
|
||||||
const paymentOption = getAppData("paymentOption");
|
const paymentOption = getAppData("paymentOption");
|
||||||
const paymentOptionSelectValue = paymentOptions.find((option) => paymentOption === option.value);
|
const paymentOptionSelectValue = paymentOptions.find((option) => paymentOption === option.value);
|
||||||
const [requirePayment, setRequirePayment] = useState(getAppData("enabled"));
|
const [requirePayment, setRequirePayment] = useState(getAppData("enabled"));
|
||||||
|
const otherPaymentAppEnabled = checkForMultiplePaymentApps(eventTypeFormMetadata);
|
||||||
|
|
||||||
|
const shouldDisableSwitch = !requirePayment && otherPaymentAppEnabled;
|
||||||
|
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
const recurringEventDefined = eventType.recurringEvent?.count !== undefined;
|
const recurringEventDefined = eventType.recurringEvent?.count !== undefined;
|
||||||
|
@ -66,7 +74,9 @@ const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({
|
||||||
switchOnClick={(enabled) => {
|
switchOnClick={(enabled) => {
|
||||||
setRequirePayment(enabled);
|
setRequirePayment(enabled);
|
||||||
}}
|
}}
|
||||||
teamId={eventType.team?.id || undefined}>
|
teamId={eventType.team?.id || undefined}
|
||||||
|
disableSwitch={shouldDisableSwitch}
|
||||||
|
switchTooltip={shouldDisableSwitch ? t("other_payment_app_enabled") : undefined}>
|
||||||
<>
|
<>
|
||||||
{recurringEventDefined && (
|
{recurringEventDefined && (
|
||||||
<Alert className="mt-2" severity="warning" title={t("warning_recurring_event_payment")} />
|
<Alert className="mt-2" severity="warning" title={t("warning_recurring_event_payment")} />
|
||||||
|
@ -75,7 +85,7 @@ const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({
|
||||||
<>
|
<>
|
||||||
<div className="mt-4 block items-center justify-start sm:flex sm:space-x-2">
|
<div className="mt-4 block items-center justify-start sm:flex sm:space-x-2">
|
||||||
<TextField
|
<TextField
|
||||||
data-testid="price-input-stripe"
|
data-testid="stripe-price-input"
|
||||||
label={t("price")}
|
label={t("price")}
|
||||||
className="h-[38px]"
|
className="h-[38px]"
|
||||||
addOnLeading={
|
addOnLeading={
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import type React from "react";
|
import type React from "react";
|
||||||
import type { z } from "zod";
|
import type { z } from "zod";
|
||||||
|
|
||||||
|
import type { EventTypeFormMetadataSchema } from "@calcom/prisma/zod-utils";
|
||||||
import type { RouterOutputs } from "@calcom/trpc/react";
|
import type { RouterOutputs } from "@calcom/trpc/react";
|
||||||
import type { ButtonProps } from "@calcom/ui";
|
import type { ButtonProps } from "@calcom/ui";
|
||||||
|
|
||||||
|
@ -51,5 +52,6 @@ export type EventTypeAppCardComponentProps = {
|
||||||
app: EventTypeAppCardApp;
|
app: EventTypeAppCardApp;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
LockedIcon?: JSX.Element | false;
|
LockedIcon?: JSX.Element | false;
|
||||||
|
eventTypeFormMetadata?: z.infer<typeof EventTypeFormMetadataSchema>;
|
||||||
};
|
};
|
||||||
export type EventTypeAppCardComponent = React.FC<EventTypeAppCardComponentProps>;
|
export type EventTypeAppCardComponent = React.FC<EventTypeAppCardComponentProps>;
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { Prisma } from "@prisma/client";
|
import { Prisma } from "@prisma/client";
|
||||||
import type { NextApiResponse, GetServerSidePropsContext } from "next";
|
import type { NextApiResponse, GetServerSidePropsContext } from "next";
|
||||||
|
|
||||||
|
import type { appDataSchemas } from "@calcom/app-store/apps.schemas.generated";
|
||||||
import updateChildrenEventTypes from "@calcom/features/ee/managed-event-types/lib/handleChildrenEventTypes";
|
import updateChildrenEventTypes from "@calcom/features/ee/managed-event-types/lib/handleChildrenEventTypes";
|
||||||
import { validateIntervalLimitOrder } from "@calcom/lib";
|
import { validateIntervalLimitOrder } from "@calcom/lib";
|
||||||
import logger from "@calcom/lib/logger";
|
import logger from "@calcom/lib/logger";
|
||||||
|
@ -240,31 +241,14 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
for (const appKey in input.metadata?.apps) {
|
||||||
* Since you can have multiple payment apps we will honor the first one to save in eventType
|
const app = input.metadata?.apps[appKey as keyof typeof appDataSchemas];
|
||||||
* but the real detail will be inside app metadata, so with this you can have different prices in different apps
|
// There should only be one enabled payment app in the metadata
|
||||||
* So the price and currency inside eventType will be deprecated soon or just keep as reference.
|
if (app.enabled && app.price && app.currency) {
|
||||||
*/
|
data.price = app.price;
|
||||||
if (
|
data.currency = app.currency;
|
||||||
input.metadata?.apps?.alby?.price ||
|
break;
|
||||||
input?.metadata?.apps?.paypal?.price ||
|
}
|
||||||
input?.metadata?.apps?.stripe?.price
|
|
||||||
) {
|
|
||||||
data.price =
|
|
||||||
input.metadata?.apps?.alby?.price ||
|
|
||||||
input.metadata.apps.paypal?.price ||
|
|
||||||
input.metadata.apps.stripe?.price;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
input.metadata?.apps?.alby?.currency ||
|
|
||||||
input?.metadata?.apps?.paypal?.currency ||
|
|
||||||
input?.metadata?.apps?.stripe?.currency
|
|
||||||
) {
|
|
||||||
data.currency =
|
|
||||||
input.metadata?.apps?.alby?.currency ||
|
|
||||||
input.metadata.apps.paypal?.currency ||
|
|
||||||
input.metadata.apps.stripe?.currency;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const connectedLink = await ctx.prisma.hashedLink.findFirst({
|
const connectedLink = await ctx.prisma.hashedLink.findFirst({
|
||||||
|
|
Loading…
Reference in New Issue
Block a user