diff --git a/apps/web/app/dashboard2/blogs/layout.tsx b/apps/web/app/dashboard2/blogs/layout.tsx index 1950d2e27..a4e679cd6 100644 --- a/apps/web/app/dashboard2/blogs/layout.tsx +++ b/apps/web/app/dashboard2/blogs/layout.tsx @@ -7,9 +7,8 @@ export async function generateMetadata( parent: ResolvingMetadata, ): Promise { return { - title: `${MANAGE_BLOG_PAGE_HEADING} | ${ - (await parent)?.title?.absolute - }`, + title: `${MANAGE_BLOG_PAGE_HEADING} | ${(await parent)?.title + ?.absolute}`, }; } diff --git a/apps/web/app/dashboard2/pages/layout.tsx b/apps/web/app/dashboard2/pages/layout.tsx index ff4e72c99..4d47cf38c 100644 --- a/apps/web/app/dashboard2/pages/layout.tsx +++ b/apps/web/app/dashboard2/pages/layout.tsx @@ -7,9 +7,8 @@ export async function generateMetadata( parent: ResolvingMetadata, ): Promise { return { - title: `${MANAGE_PAGES_PAGE_HEADING} | ${ - (await parent)?.title?.absolute - }`, + title: `${MANAGE_PAGES_PAGE_HEADING} | ${(await parent)?.title + ?.absolute}`, }; } diff --git a/apps/web/app/dashboard2/products/layout.tsx b/apps/web/app/dashboard2/products/layout.tsx index daf605a15..7d96a6ec0 100644 --- a/apps/web/app/dashboard2/products/layout.tsx +++ b/apps/web/app/dashboard2/products/layout.tsx @@ -7,9 +7,8 @@ export async function generateMetadata( parent: ResolvingMetadata, ): Promise { return { - title: `${MANAGE_COURSES_PAGE_HEADING} | ${ - (await parent)?.title?.absolute - }`, + title: `${MANAGE_COURSES_PAGE_HEADING} | ${(await parent)?.title + ?.absolute}`, }; } diff --git a/apps/web/app/dashboard2/settings/layout.tsx b/apps/web/app/dashboard2/settings/layout.tsx index 2bbb4fc2f..b0ff93821 100644 --- a/apps/web/app/dashboard2/settings/layout.tsx +++ b/apps/web/app/dashboard2/settings/layout.tsx @@ -7,9 +7,8 @@ export async function generateMetadata( parent: ResolvingMetadata, ): Promise { return { - title: `${SITE_SETTINGS_PAGE_HEADING} | ${ - (await parent)?.title?.absolute - }`, + title: `${SITE_SETTINGS_PAGE_HEADING} | ${(await parent)?.title + ?.absolute}`, }; } diff --git a/apps/web/app/verify-domain/route.ts b/apps/web/app/verify-domain/route.ts index f71d5d993..02bccd83a 100644 --- a/apps/web/app/verify-domain/route.ts +++ b/apps/web/app/verify-domain/route.ts @@ -57,9 +57,9 @@ export async function GET(req: Request) { if (!domain) { return Response.json( { - message: `${responses.domain_doesnt_exist}: ${ - host?.split(".")[0] - }`, + message: `${responses.domain_doesnt_exist}: ${host?.split( + ".", + )[0]}`, }, { status: 404 }, ); diff --git a/apps/web/components/admin/mails/sequence-editor.tsx b/apps/web/components/admin/mails/sequence-editor.tsx index e0b488cbd..9848a860b 100644 --- a/apps/web/components/admin/mails/sequence-editor.tsx +++ b/apps/web/components/admin/mails/sequence-editor.tsx @@ -633,11 +633,11 @@ const SequenceEditor = ({ value: tag.tag, })) : triggerType === "PRODUCT_PURCHASED" - ? products.map((product) => ({ - label: product.title, - value: product.courseId, - })) - : [] + ? products.map((product) => ({ + label: product.title, + value: product.courseId, + })) + : [] } /> )} diff --git a/apps/web/components/admin/page-editor/index.tsx b/apps/web/components/admin/page-editor/index.tsx index e62f6b78e..25b213762 100644 --- a/apps/web/components/admin/page-editor/index.tsx +++ b/apps/web/components/admin/page-editor/index.tsx @@ -570,8 +570,8 @@ function PageEditor({ typeof page.draftRobotsAllowed === "boolean" ? page.draftRobotsAllowed : typeof page.robotsAllowed === "boolean" - ? page.robotsAllowed - : true + ? page.robotsAllowed + : true } socialImage={page.draftSocialImage || {}} onClose={(e) => setLeftPaneContent("none")} diff --git a/apps/web/config/strings.ts b/apps/web/config/strings.ts index ebebbf546..49fa711cb 100644 --- a/apps/web/config/strings.ts +++ b/apps/web/config/strings.ts @@ -115,6 +115,7 @@ export const responses = { user_already_exists: "The user already exists", unsubscribe_success: "Sorry to see you go. You have been unsubscribed from our mailing list.", + sales_made_subject: `Yay! You have made a sale!`, }; export const internal = { diff --git a/apps/web/graphql/settings/helpers.ts b/apps/web/graphql/settings/helpers.ts index a141964d9..67544063d 100644 --- a/apps/web/graphql/settings/helpers.ts +++ b/apps/web/graphql/settings/helpers.ts @@ -8,8 +8,8 @@ import { UIConstants, } from "@courselit/common-models"; -const currencyISOCodes = currencies.map((currency) => - currency.isoCode?.toLowerCase(), +const currencyISOCodes = currencies.map( + (currency) => currency.isoCode?.toLowerCase(), ); const verifyCurrencyISOCode = (isoCode: string) => { diff --git a/apps/web/lib/finalize-purchase.ts b/apps/web/lib/finalize-purchase.ts index b56ae5031..96a0e32ea 100644 --- a/apps/web/lib/finalize-purchase.ts +++ b/apps/web/lib/finalize-purchase.ts @@ -1,10 +1,19 @@ import CourseModel, { Course } from "../models/Course"; import UserModel, { User } from "../models/User"; +import PurchaseModel, { Purchase } from "@models/Purchase"; +import DomainModel, { Domain } from "@models/Domain"; import { triggerSequences } from "./trigger-sequences"; import { recordActivity } from "./record-activity"; import { Constants, Progress } from "@courselit/common-models"; +import saleEmailTemplate from "@/templates/sale-email"; +import pug from "pug"; +import { send } from "@/services/mail"; +import { formattedLocaleDate } from "@ui-lib/utils"; +import { error } from "@/services/logger"; +import { responses } from "@/config/strings"; +import getSymbolFromCurrency from "currency-symbol-map"; -export default async ( +const finalizePurchase = async ( userId: string, courseId: string, purchaseId?: string, @@ -52,6 +61,69 @@ export default async ( purchaseId, }, }); + + sendSaleNotificationToAdmins({ + user, + course, + purchaseId: purchaseId!, + }); } } }; + +async function sendSaleNotificationToAdmins({ + user, + course, + purchaseId, +}: { + user: User; + course: Course; + purchaseId: string; +}) { + try { + const domain: Domain | null = await DomainModel.findOne({ + _id: user.domain, + }); + + const purchase: Purchase | null = await PurchaseModel.findOne({ + orderId: purchaseId, + }); + + const usersWithManagePermissions = await UserModel.find( + { + domain: domain?._id, + $or: [ + { userId: course.creatorId, permissions: "course:manage" }, + { permissions: "course:manage_any" }, + ], + }, + { email: 1 }, + ).lean(); + + const courseAdminsEmails = usersWithManagePermissions.map( + (x) => x.email, + ); + + const currencySymbol = getSymbolFromCurrency( + domain?.settings?.currencyISOCode, + ); + const emailBody = pug.render(saleEmailTemplate, { + order: purchase?.orderId, + courseName: course.title, + coursePrice: `${currencySymbol}${course.cost}`, + date: formattedLocaleDate(purchase!.purchasedOn), + email: user?.email, + hideCourseLitBranding: domain?.settings.hideCourseLitBranding, + }); + + await send({ + to: courseAdminsEmails, + subject: responses.sales_made_subject, + body: emailBody, + }); + } catch (err) { + error("Failed to send sale notification mail", err); + } +} + +export default finalizePurchase; diff --git a/apps/web/pages/api/payment/initiate.ts b/apps/web/pages/api/payment/initiate.ts index 99ea26eff..3b6d3318e 100644 --- a/apps/web/pages/api/payment/initiate.ts +++ b/apps/web/pages/api/payment/initiate.ts @@ -6,7 +6,7 @@ import { getPaymentMethod } from "../../../payments"; import PurchaseModel from "../../../models/Purchase"; import finalizePurchase from "../../../lib/finalize-purchase"; import { error } from "../../../services/logger"; -import User from "@models/User"; +import UserModel from "@models/User"; import DomainModel, { Domain } from "@models/Domain"; import { auth } from "@/auth"; @@ -32,7 +32,7 @@ export default async function handler( let user; if (session) { - user = await User.findOne({ + user = await UserModel.findOne({ email: session.user!.email, domain: domain._id, active: true, diff --git a/apps/web/services/logger.ts b/apps/web/services/logger.ts index b15b23520..ed40d3a5c 100644 --- a/apps/web/services/logger.ts +++ b/apps/web/services/logger.ts @@ -14,7 +14,7 @@ export const info = async ( metadata, }); } else { - console.error(severityError, message, metadata); + console.error(severityInfo, message, metadata); } }; diff --git a/apps/web/templates/sale-email.ts b/apps/web/templates/sale-email.ts new file mode 100644 index 000000000..dc4bd5850 --- /dev/null +++ b/apps/web/templates/sale-email.ts @@ -0,0 +1,73 @@ +const saleEmailTemplate = ` +doctype html +html + head + style(type='text/css'). + .email-container { + max-width: 960px; + } + .cta-container { + margin: 32px 0px; + text-align: center; + } + .cta { + border: 1px solid #07077b; + border-radius: 4px; + padding: 4px 8px; + text-decoration: none; + color: white; + background-color: #07077b; + font-weight: bold; + } + .cta:hover { + background-color: #060665; + } + .courselit-branding-container { + margin: 40px 0px; + } + .courselit-branding-cta { + text-decoration: none; + color: #000000; + padding: 6px 10px; + background-color: #FFFFFF; + border: 1px solid; + border-radius: 6px; + text-align: center; + } + .sale-notification-heading { + padding-bottom: 5px; + } + .sale-details { + padding-bottom: 10px; + } + .course-name-and-price { + display: flex; + justify-content: space-between; + align-items: center; + font-weight: bold; + padding-bottom: 20px; + } + body + div(class="email-container") + div(class="sale-notification-heading") + p Yay! You have made a sale! + div(class="sale-details") + p Order: #{order} + p Date: #{date} + p Email: #{email} + div(class="course-name-and-price") + span #{courseName} + span #{coursePrice} + p(class="signature") + | Best, + p #[a(href="https://x.com/courselit") @CourseLit] + if !hideCourseLitBranding + div(class="courselit-branding-container") + a( + href="https://courselit.app" + target="_blank" + class="courselit-branding-cta" + ) Powered by CourseLit +`; + +export default saleEmailTemplate; diff --git a/packages/common-widgets/src/banner/widget.tsx b/packages/common-widgets/src/banner/widget.tsx index 7d6bf007f..3e2692b16 100644 --- a/packages/common-widgets/src/banner/widget.tsx +++ b/packages/common-widgets/src/banner/widget.tsx @@ -72,8 +72,8 @@ export default function Widget({ : undefined : description : product.description - ? JSON.parse(product.description as string) - : undefined; + ? JSON.parse(product.description as string) + : undefined; let direction: any; switch (alignment) { diff --git a/packages/common-widgets/src/email-form/widget.tsx b/packages/common-widgets/src/email-form/widget.tsx index 91ad94ba7..a78588a7c 100644 --- a/packages/common-widgets/src/email-form/widget.tsx +++ b/packages/common-widgets/src/email-form/widget.tsx @@ -54,8 +54,8 @@ const Widget = ({ alignment === "center" ? "center" : alignment === "right" - ? "flex-end" - : "flex-start"; + ? "flex-end" + : "flex-start"; useEffect(() => { if (state.config.turnstileSiteKey) { diff --git a/packages/common-widgets/src/header/widget/index.tsx b/packages/common-widgets/src/header/widget/index.tsx index 94495ba10..8006579b4 100644 --- a/packages/common-widgets/src/header/widget/index.tsx +++ b/packages/common-widgets/src/header/widget/index.tsx @@ -77,8 +77,8 @@ export default function Widget({ state, settings }: WidgetProps) { linkAlignment === "right" ? "justify-end" : linkAlignment === "center" - ? "justify-center" - : "justify-start" + ? "justify-center" + : "justify-start" }`} style={{ gap: `${spacingBetweenLinks}px`, diff --git a/packages/common-widgets/src/hero/widget.tsx b/packages/common-widgets/src/hero/widget.tsx index 956d87d99..7c3c14f1f 100644 --- a/packages/common-widgets/src/hero/widget.tsx +++ b/packages/common-widgets/src/hero/widget.tsx @@ -198,15 +198,15 @@ export default function Widget({ descriptionFontSize === 0 ? "text-base" : descriptionFontSize === 1 - ? "text-lg lg:text-xl" - : `text-${ - descriptionFontSize - - 1 === - 1 - ? "" - : descriptionFontSize - - 1 - }xl lg:text-${descriptionFontSize}xl`, + ? "text-lg lg:text-xl" + : `text-${ + descriptionFontSize - + 1 === + 1 + ? "" + : descriptionFontSize - + 1 + }xl lg:text-${descriptionFontSize}xl`, buttonAction && buttonCaption ? "mb-8" : "mb-0",