From f168cf9d7378d1de708f901fcbc0ad5d2b84e840 Mon Sep 17 00:00:00 2001 From: Rami Abdou <38056800+ramiAbdou@users.noreply.github.com> Date: Mon, 30 Sep 2024 08:57:00 -0700 Subject: [PATCH] =?UTF-8?q?chore:=20remove=20all=20swag=20up=20code=20?= =?UTF-8?q?=F0=9F=9A=AE=20(#531)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../_dashboard.students.$id.activate.tsx | 4 +- apps/api/.env.example | 2 - apps/api/src/shared/env.ts | 4 - apps/member-profile/.env.example | 2 - .../app/routes/_profile.home.activation.tsx | 13 +- .../_profile.home.claim-swag-pack._index.tsx | 241 ---------------- ...file.home.claim-swag-pack.confirmation.tsx | 27 -- .../routes/_profile.home.claim-swag-pack.tsx | 23 -- .../app/routes/_profile.home.tsx | 4 +- .../app/routes/_public.claim-swag-pack.tsx | 7 - .../app/routes/_public.login.tsx | 46 +--- .../app/shared/constants.server.ts | 4 - apps/member-profile/app/shared/constants.ts | 2 - docs/how-to-enable-integrations.md | 16 -- packages/core/src/api.ts | 2 - .../src/infrastructure/bull/bull.types.ts | 9 - packages/core/src/member-profile.server.ts | 2 - packages/core/src/member-profile.ui.ts | 1 - .../events/activation-step-completed.ts | 2 +- .../modules/member/events/member-activated.ts | 27 +- .../modules/swag-pack/swag-pack.service.ts | 260 ------------------ .../src/modules/swag-pack/swag-pack.types.ts | 15 - .../src/modules/swag-pack/swag-pack.worker.ts | 17 -- .../swag-pack/use-cases/claim-swag-pack.ts | 91 ------ .../use-cases/notify-swag-pack-inventory.ts | 14 - packages/core/src/shared/env.ts | 2 - packages/types/src/domain/student.ts | 1 - 27 files changed, 38 insertions(+), 800 deletions(-) delete mode 100644 apps/member-profile/app/routes/_profile.home.claim-swag-pack._index.tsx delete mode 100644 apps/member-profile/app/routes/_profile.home.claim-swag-pack.confirmation.tsx delete mode 100644 apps/member-profile/app/routes/_profile.home.claim-swag-pack.tsx delete mode 100644 apps/member-profile/app/routes/_public.claim-swag-pack.tsx delete mode 100644 packages/core/src/modules/swag-pack/swag-pack.service.ts delete mode 100644 packages/core/src/modules/swag-pack/swag-pack.types.ts delete mode 100644 packages/core/src/modules/swag-pack/swag-pack.worker.ts delete mode 100644 packages/core/src/modules/swag-pack/use-cases/claim-swag-pack.ts delete mode 100644 packages/core/src/modules/swag-pack/use-cases/notify-swag-pack-inventory.ts diff --git a/apps/admin-dashboard/app/routes/_dashboard.students.$id.activate.tsx b/apps/admin-dashboard/app/routes/_dashboard.students.$id.activate.tsx index ef53b459..c1c22646 100644 --- a/apps/admin-dashboard/app/routes/_dashboard.students.$id.activate.tsx +++ b/apps/admin-dashboard/app/routes/_dashboard.students.$id.activate.tsx @@ -86,8 +86,8 @@ export default function ActivateStudentPage() { Just confirming - do you want to activate {student.firstName}{' '} - {student.lastName}? They will receive an email with the ability to claim - a swag pack in their profile. + {student.lastName}? They will receive an email with a gift card to the + merch store. diff --git a/apps/api/.env.example b/apps/api/.env.example index 384b6e5d..52ee091a 100644 --- a/apps/api/.env.example +++ b/apps/api/.env.example @@ -49,5 +49,3 @@ STUDENT_PROFILE_URL=http://localhost:3000 # SMTP_HOST= # SMTP_PASSWORD= # SMTP_USERNAME= -# SWAG_UP_CLIENT_ID= -# SWAG_UP_CLIENT_SECRET= diff --git a/apps/api/src/shared/env.ts b/apps/api/src/shared/env.ts index 9a875f17..d075fa29 100644 --- a/apps/api/src/shared/env.ts +++ b/apps/api/src/shared/env.ts @@ -56,8 +56,6 @@ const BaseEnvironmentConfig = z.object({ SLACK_INTRODUCTIONS_CHANNEL_ID: EnvironmentVariable, SLACK_SIGNING_SECRET: EnvironmentVariable, STUDENT_PROFILE_URL: EnvironmentVariable, - SWAG_UP_CLIENT_ID: EnvironmentVariable, - SWAG_UP_CLIENT_SECRET: EnvironmentVariable, }); const EnvironmentConfig = z.discriminatedUnion('ENVIRONMENT', [ @@ -98,8 +96,6 @@ const EnvironmentConfig = z.discriminatedUnion('ENVIRONMENT', [ SLACK_FEED_CHANNEL_ID: true, SLACK_INTRODUCTIONS_CHANNEL_ID: true, SLACK_SIGNING_SECRET: true, - SWAG_UP_CLIENT_ID: true, - SWAG_UP_CLIENT_SECRET: true, }).extend({ ENVIRONMENT: z.literal(Environment.DEVELOPMENT), SMTP_HOST: EnvironmentVariable.optional(), diff --git a/apps/member-profile/.env.example b/apps/member-profile/.env.example index b8becb72..bd306ddc 100644 --- a/apps/member-profile/.env.example +++ b/apps/member-profile/.env.example @@ -33,5 +33,3 @@ STUDENT_PROFILE_URL=http://localhost:3000 # SMTP_HOST= # SMTP_PASSWORD= # SMTP_USERNAME= -# SWAG_UP_CLIENT_ID= -# SWAG_UP_CLIENT_SECRET= diff --git a/apps/member-profile/app/routes/_profile.home.activation.tsx b/apps/member-profile/app/routes/_profile.home.activation.tsx index c4e1ae6d..1e5de917 100644 --- a/apps/member-profile/app/routes/_profile.home.activation.tsx +++ b/apps/member-profile/app/routes/_profile.home.activation.tsx @@ -113,14 +113,7 @@ export default function ActivationModal() { function ActivatedState() { return ( <> - - Great news -- you're activated and eligible to{' '} - - claim your FREE swag pack - - ! 🎉 - - + Great news -- you're activated! 🎉 ); @@ -167,8 +160,8 @@ function NotActivatedState() { <> You've completed {requirementsCompleted.length}/6 activation - requirements. Once you hit all 6, you will be eligible to claim your - FREE swag pack! 👀 + requirements. Once you hit all 6, you will get a gift card to claim your + FREE merch! 👀 diff --git a/apps/member-profile/app/routes/_profile.home.claim-swag-pack._index.tsx b/apps/member-profile/app/routes/_profile.home.claim-swag-pack._index.tsx deleted file mode 100644 index 09d92119..00000000 --- a/apps/member-profile/app/routes/_profile.home.claim-swag-pack._index.tsx +++ /dev/null @@ -1,241 +0,0 @@ -import { - type ActionFunctionArgs, - defer, - json, - type LoaderFunctionArgs, - redirect, -} from '@remix-run/node'; -import { - Await, - Form as RemixForm, - useActionData, - useLoaderData, -} from '@remix-run/react'; -import { Suspense } from 'react'; - -import { - claimSwagPack, - getSwagPackInventory, - reportException, -} from '@oyster/core/member-profile/server'; -import { ClaimSwagPackInput } from '@oyster/core/member-profile/ui'; -import { db } from '@oyster/db'; -import { - Address, - Button, - Form, - getErrors, - Modal, - Spinner, - Text, - validateForm, -} from '@oyster/ui'; - -import { Route } from '@/shared/constants'; -import { ensureUserAuthenticated, user } from '@/shared/session.server'; - -export async function loader({ request }: LoaderFunctionArgs) { - const session = await ensureUserAuthenticated(request); - - const student = await db - .selectFrom('students') - .select(['activatedAt', 'claimedSwagPackAt']) - .where('id', '=', user(session)) - .executeTakeFirst(); - - if (!student || student.claimedSwagPackAt || !student.activatedAt) { - throw new Response(null, { status: 404 }); - } - - const inventoryPromise = getSwagPackInventory(); - - return defer({ - inventoryPromise, - }); -} - -export async function action({ request }: ActionFunctionArgs) { - const session = await ensureUserAuthenticated(request); - - const { data, errors, ok } = await validateForm( - request, - ClaimSwagPackInput.omit({ studentId: true }) - ); - - if (!ok) { - return json({ errors }, { status: 400 }); - } - - try { - const result = await claimSwagPack({ - addressCity: data.addressCity, - addressCountry: data.addressCountry, - addressLine1: data.addressLine1, - addressLine2: data.addressLine2, - addressState: data.addressState, - addressZip: data.addressZip, - studentId: user(session), - }); - - if (!result.ok) { - return json({ error: result.error }, { status: result.code }); - } - - return redirect(Route['/home/claim-swag-pack/confirmation']); - } catch (e) { - reportException(e); - - return json({ - error: `Something went wrong. Please double check that you have a valid address. If you are still having trouble, reach out to membership@colorstack.org for further assistance.`, - errors, - }); - } -} - -const keys = ClaimSwagPackInput.keyof().enum; - -export default function ClaimSwagPack() { - const { inventoryPromise } = useLoaderData(); - - return ( - <> - }> - - {(inventory) => { - return inventory > 0 ? ( - <> - - Claim Your Swag Pack 🎁 - - - - - - ) : ( - <> - - - Sit tight, we're sending you a gift card! 🤑 - - - - - - We're changing the way we send out swag. Give us 2 business - days and we'll send you a gift card to our Merch Store! - - - ); - }} - - - - ); -} - -function LoadingState() { - return ( -
- - - Checking our swag pack inventory... - -
- ); -} - -function ClaimSwagPackForm() { - const { error, errors } = getErrors(useActionData()); - - return ( - - Let us know where to send your swag pack! - -
- - - - - - - - - - - - - - - - - - - - - - - - -
- - {error} - - - Claim Swag Pack - -
- ); -} - -export function ErrorBoundary() { - return <>; -} diff --git a/apps/member-profile/app/routes/_profile.home.claim-swag-pack.confirmation.tsx b/apps/member-profile/app/routes/_profile.home.claim-swag-pack.confirmation.tsx deleted file mode 100644 index 63c4acc7..00000000 --- a/apps/member-profile/app/routes/_profile.home.claim-swag-pack.confirmation.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import { json, type LoaderFunctionArgs } from '@remix-run/node'; - -import { Modal } from '@oyster/ui'; - -import { ensureUserAuthenticated } from '@/shared/session.server'; - -export async function loader({ request }: LoaderFunctionArgs) { - await ensureUserAuthenticated(request); - - return json({}); -} - -export default function ConfirmationPage() { - return ( - <> - - Swag Pack Ordered! 🎉 - - - - - Congratulations! Your ColorStack swag pack is on its way to you. Please - allow 2-4 weeks for shipping and processing. - - - ); -} diff --git a/apps/member-profile/app/routes/_profile.home.claim-swag-pack.tsx b/apps/member-profile/app/routes/_profile.home.claim-swag-pack.tsx deleted file mode 100644 index ffd7eca9..00000000 --- a/apps/member-profile/app/routes/_profile.home.claim-swag-pack.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import { json, type LoaderFunctionArgs } from '@remix-run/node'; -import { Outlet } from '@remix-run/react'; - -import { Modal } from '@oyster/ui'; - -import { Route } from '@/shared/constants'; -import { ensureUserAuthenticated } from '@/shared/session.server'; - -export async function loader({ request }: LoaderFunctionArgs) { - await ensureUserAuthenticated(request, { - redirectTo: `${Route['/login']}?context=claim-swag-pack`, - }); - - return json({}); -} - -export default function ClaimSwagPackLayout() { - return ( - - - - ); -} diff --git a/apps/member-profile/app/routes/_profile.home.tsx b/apps/member-profile/app/routes/_profile.home.tsx index 35fe05e9..6abe63ff 100644 --- a/apps/member-profile/app/routes/_profile.home.tsx +++ b/apps/member-profile/app/routes/_profile.home.tsx @@ -374,8 +374,8 @@ function ActivationCard() { You've completed {student.activationRequirementsCompleted.length}/6 - activation requirements. Once you hit all 6, you will be eligible to - claim your FREE swag pack! 👀 + activation requirements. Once you hit all 6, you will get a gift card to + claim your FREE merch! 👀 diff --git a/apps/member-profile/app/routes/_public.claim-swag-pack.tsx b/apps/member-profile/app/routes/_public.claim-swag-pack.tsx deleted file mode 100644 index 0e146603..00000000 --- a/apps/member-profile/app/routes/_public.claim-swag-pack.tsx +++ /dev/null @@ -1,7 +0,0 @@ -import { redirect } from '@remix-run/node'; - -import { Route } from '@/shared/constants'; - -export function loader() { - return redirect(Route['/home/claim-swag-pack']); -} diff --git a/apps/member-profile/app/routes/_public.login.tsx b/apps/member-profile/app/routes/_public.login.tsx index 0826b325..e76354a1 100644 --- a/apps/member-profile/app/routes/_public.login.tsx +++ b/apps/member-profile/app/routes/_public.login.tsx @@ -1,50 +1,16 @@ -import { json, type LoaderFunctionArgs } from '@remix-run/node'; -import { Outlet, useLoaderData } from '@remix-run/react'; -import { z } from 'zod'; +import { json } from '@remix-run/node'; +import { Outlet } from '@remix-run/react'; -import { Login, Public, Text } from '@oyster/ui'; +import { Login, Public } from '@oyster/ui'; -import { Route } from '@/shared/constants'; - -export const LoginSearchParams = z.object({ - context: z.enum(['claim-swag-pack']).nullish().catch(null), -}); - -export type LoginSearchParams = z.infer; - -export async function loader({ request }: LoaderFunctionArgs) { - const url = new URL(request.url); - - const { context } = LoginSearchParams.parse( - Object.fromEntries(url.searchParams) - ); - - const title: string = - context === 'claim-swag-pack' ? 'Claim Swag Pack 🎁' : 'ColorStack Profile'; - - // We're only going to show the description for the Claim Swag Pack flow - // in the initial login page, and not any subsequent OTP (or other) pages. - const isFirstLoginPage: boolean = - new URL(request.url).pathname === Route['/login']; - - const description: string | null = - context === 'claim-swag-pack' && isFirstLoginPage - ? "In order to claim your swag pack, we'll just need to authenticate your email by sending you a one-time passcode." - : null; - - return json({ - description, - title, - }); +export async function loader() { + return json({}); } export default function LoginLayout() { - const { description, title } = useLoaderData(); - return ( - {title} - {description && {description}} + ColorStack Profile ); diff --git a/apps/member-profile/app/shared/constants.server.ts b/apps/member-profile/app/shared/constants.server.ts index 5648b72a..ce13c92e 100644 --- a/apps/member-profile/app/shared/constants.server.ts +++ b/apps/member-profile/app/shared/constants.server.ts @@ -31,8 +31,6 @@ const BaseEnvironmentConfig = z.object({ SLACK_CLIENT_ID: EnvironmentVariable, SLACK_TEAM_ID: EnvironmentVariable, STUDENT_PROFILE_URL: EnvironmentVariable, - SWAG_UP_CLIENT_ID: EnvironmentVariable, - SWAG_UP_CLIENT_SECRET: EnvironmentVariable, }); const EnvironmentConfig = z.discriminatedUnion('ENVIRONMENT', [ @@ -56,8 +54,6 @@ const EnvironmentConfig = z.discriminatedUnion('ENVIRONMENT', [ SLACK_ANNOUNCEMENTS_CHANNEL_ID: true, SLACK_CLIENT_ID: true, SLACK_TEAM_ID: true, - SWAG_UP_CLIENT_ID: true, - SWAG_UP_CLIENT_SECRET: true, }).extend({ ENVIRONMENT: z.literal(Environment.DEVELOPMENT), SMTP_HOST: EnvironmentVariable.optional(), diff --git a/apps/member-profile/app/shared/constants.ts b/apps/member-profile/app/shared/constants.ts index a525fbac..833daa1a 100644 --- a/apps/member-profile/app/shared/constants.ts +++ b/apps/member-profile/app/shared/constants.ts @@ -20,8 +20,6 @@ const ROUTES = [ '/events/upcoming/:id/registrations', '/home', '/home/activation', - '/home/claim-swag-pack', - '/home/claim-swag-pack/confirmation', '/login', '/login/otp/send', '/login/otp/verify', diff --git a/docs/how-to-enable-integrations.md b/docs/how-to-enable-integrations.md index ec215b27..e1fc42de 100644 --- a/docs/how-to-enable-integrations.md +++ b/docs/how-to-enable-integrations.md @@ -149,19 +149,3 @@ To enable the **Slack** integration: SLACK_CLIENT_ID SLACK_TEAM_ID ``` - -## SwagUp - -To enable the **SwagUp** integration: - -1. -2. In `/api/.env`, set the following variables: - ``` - SWAG_UP_CLIENT_ID - SWAG_UP_CLIENT_SECRET - ``` -3. In `/member-profile/.env`, set the following variables: - ``` - SWAG_UP_CLIENT_ID - SWAG_UP_CLIENT_SECRET - ``` diff --git a/packages/core/src/api.ts b/packages/core/src/api.ts index f6c09cda..516b957b 100644 --- a/packages/core/src/api.ts +++ b/packages/core/src/api.ts @@ -20,7 +20,6 @@ import { profileWorker } from './modules/member/profile.worker'; import { notificationWorker } from './modules/notification/notification.worker'; import { onboardingSessionWorker } from './modules/onboarding-session/onboarding-session.worker'; import { slackWorker } from './modules/slack/slack.worker'; -import { swagPackWorker } from './modules/swag-pack/swag-pack.worker'; export { job } from './infrastructure/bull/use-cases/job'; export { OAuthCodeState } from './modules/authentication/authentication.types'; @@ -48,5 +47,4 @@ export function startBullWorkers(): void { oneTimeCodeWorker.run(); profileWorker.run(); slackWorker.run(); - swagPackWorker.run(); } diff --git a/packages/core/src/infrastructure/bull/bull.types.ts b/packages/core/src/infrastructure/bull/bull.types.ts index ceac0bec..ed227e34 100644 --- a/packages/core/src/infrastructure/bull/bull.types.ts +++ b/packages/core/src/infrastructure/bull/bull.types.ts @@ -39,7 +39,6 @@ export const BullQueue = { PROFILE: 'profile', SLACK: 'slack', STUDENT: 'student', - SWAG_PACK: 'swag_pack', } as const; export type BullQueue = ExtractValue; @@ -593,13 +592,6 @@ export const StudentBullJob = z.discriminatedUnion('name', [ }), ]); -export const SwagPackBullJob = z.discriminatedUnion('name', [ - z.object({ - name: z.literal('swag_pack.inventory.notify'), - data: z.object({}), - }), -]); - // Combination export const BullJob = z.union([ @@ -616,7 +608,6 @@ export const BullJob = z.union([ ProfileBullJob, SlackBullJob, StudentBullJob, - SwagPackBullJob, ]); // Types diff --git a/packages/core/src/member-profile.server.ts b/packages/core/src/member-profile.server.ts index a719fe9c..9b383a7c 100644 --- a/packages/core/src/member-profile.server.ts +++ b/packages/core/src/member-profile.server.ts @@ -37,5 +37,3 @@ export { updateAllowEmailShare } from './modules/member/use-cases/update-allow-e export { updateMember } from './modules/member/use-cases/update-member'; export { reportException } from './modules/sentry/use-cases/report-exception'; export { countMessagesSent } from './modules/slack/queries/count-messages-sent'; -export { getSwagPackInventory } from './modules/swag-pack/swag-pack.service'; -export { claimSwagPack } from './modules/swag-pack/use-cases/claim-swag-pack'; diff --git a/packages/core/src/member-profile.ui.ts b/packages/core/src/member-profile.ui.ts index af9d16b7..21cdee56 100644 --- a/packages/core/src/member-profile.ui.ts +++ b/packages/core/src/member-profile.ui.ts @@ -39,5 +39,4 @@ export { ListMembersInDirectoryWhere, } from './modules/member/member.types'; export { CreateResumeBookInput } from './modules/resume/resume.types'; -export { ClaimSwagPackInput } from './modules/swag-pack/swag-pack.types'; export { Environment, ListSearchParams } from './shared/types'; diff --git a/packages/core/src/modules/member/events/activation-step-completed.ts b/packages/core/src/modules/member/events/activation-step-completed.ts index 669b8b9a..3f9e4256 100644 --- a/packages/core/src/modules/member/events/activation-step-completed.ts +++ b/packages/core/src/modules/member/events/activation-step-completed.ts @@ -230,7 +230,7 @@ async function sendProgressNotification({ You've completed all of your activation requirements, which means...you are now an *activated* ColorStack member. - You can now claim your free ColorStack merch ! 🎁 + Look out for an email that includes a GIFT CARD to the ! 🎁 `; } else { message = dedent` diff --git a/packages/core/src/modules/member/events/member-activated.ts b/packages/core/src/modules/member/events/member-activated.ts index 21777e3d..a48bb3df 100644 --- a/packages/core/src/modules/member/events/member-activated.ts +++ b/packages/core/src/modules/member/events/member-activated.ts @@ -1,19 +1,40 @@ +import dayjs from 'dayjs'; + import { db } from '@oyster/db'; import { type GetBullJobData } from '@/infrastructure/bull/bull.types'; import { job } from '@/infrastructure/bull/use-cases/job'; +import { createGiftCard } from '@/modules/shopify'; export async function onMemberActivated({ studentId, }: GetBullJobData<'student.activated'>) { - const student = await db + const member = await db .selectFrom('students') - .select(['email', 'firstName', 'id']) + .select(['email', 'firstName', 'id', 'lastName']) .where('id', '=', studentId) .executeTakeFirstOrThrow(); + const giftCardResult = await createGiftCard({ + expiresOn: dayjs().add(1, 'week').format('YYYY-MM-DD'), + initialValue: '50.00', + message: + 'Congratulations on becoming an activated ColorStack member! 🎉 ' + + 'From the team at ColorStack, we hope you enjoy your new merch! 🔥', + note: 'This was awarded for member activation.', + recipient: { + email: member.email, + firstName: member.firstName, + lastName: member.lastName, + }, + }); + + if (!giftCardResult.ok) { + throw new Error(giftCardResult.error); + } + job('gamification.activity.completed', { - studentId: student.id, + studentId: member.id, type: 'get_activated', }); } diff --git a/packages/core/src/modules/swag-pack/swag-pack.service.ts b/packages/core/src/modules/swag-pack/swag-pack.service.ts deleted file mode 100644 index 118732f8..00000000 --- a/packages/core/src/modules/swag-pack/swag-pack.service.ts +++ /dev/null @@ -1,260 +0,0 @@ -import { z } from 'zod'; - -import { type Address } from '@oyster/types'; - -import { redis } from '@/infrastructure/redis'; -import { - OAuthTokenResponse, - type OAuthTokens, -} from '@/modules/authentication/oauth.service'; -import { ENV, IS_PRODUCTION } from '@/shared/env'; -import { ErrorWithContext } from '@/shared/errors'; -import { encodeBasicAuthenticationToken } from '@/shared/utils/auth.utils'; -import { validate } from '@/shared/utils/zod.utils'; - -// Errors - -class SwagUpApiError extends ErrorWithContext { - message = 'There was an issue fetching data from the SwagUp API.'; -} - -// Constants - -const SWAG_UP_API_URL = 'https://api.swagup.com/api/v1'; - -// Core - -type OrderSwagPackInput = { - contact: { - address: Address; - email: string; - firstName: string; - lastName: string; - }; -}; - -type SwagUpSendRequestBody = { - employee: { - first_name: string; - last_name: string; - email: string; - shipping_address1: string; - shipping_address2?: string; - shipping_city: string; - shipping_country: string; - shipping_state: string; - shipping_zip: string; - }; - force_address: boolean; - products: { - product: number; - sizes: { - quantity: number; - size: number; - }[]; - }[]; -}; - -const SwagPackOrder = z.object({ - id: z.number(), -}); - -class SwagPackOrderError extends ErrorWithContext { - message = 'There was an issue ordering a swag pack.'; -} - -export async function orderSwagPack(input: OrderSwagPackInput) { - if (!IS_PRODUCTION) { - return null; - } - - const { accessToken } = await retrieveTokens(); - - const { productId, sizeId } = await getProductInformation(); - - const body: SwagUpSendRequestBody = { - employee: { - email: input.contact.email, - first_name: input.contact.firstName, - last_name: input.contact.lastName, - shipping_address1: input.contact.address.line1, - shipping_address2: input.contact.address.line2, - shipping_city: input.contact.address.city, - shipping_country: input.contact.address.country, - shipping_state: input.contact.address.state, - shipping_zip: input.contact.address.zip, - }, - products: [ - { - product: productId, - sizes: [ - { - quantity: 1, - size: sizeId, - }, - ], - }, - ], - force_address: true, - }; - - const response = await fetch(`${SWAG_UP_API_URL}/employee-orders/`, { - body: JSON.stringify([body]), - method: 'post', - headers: { - Authorization: `Bearer ${accessToken}`, - 'Content-Type': 'application/json', - }, - }); - - const data = await response.json(); - - if (!response.ok) { - throw new SwagPackOrderError().withContext({ - body, - error: data, - }); - } - - const [order] = validate(SwagPackOrder.array(), data); - - console.log({ - code: 'swag_pack_ordered', - message: 'Swag pack was ordered.', - data: { - email: input.contact.email, - firstName: input.contact.firstName, - lastName: input.contact.lastName, - orderId: order.id, - }, - }); - - return order.id.toString(); -} - -const SwagProduct = z.object({ - stock: z.object({ quantity: z.coerce.number() }).array(), -}); - -export async function getSwagPackInventory() { - const { productId } = await getProductInformation(); - - const { accessToken } = await retrieveTokens(); - - const response = await fetch( - `${SWAG_UP_API_URL}/account-products/${productId}/`, - { - headers: { - Authorization: `Bearer ${accessToken}`, - }, - } - ); - - if (!response.ok) { - throw new SwagUpApiError().withContext({ productId }); - } - - const product = validate(SwagProduct, await response.json()); - - const inventory = product.stock[0].quantity || 0; - - return inventory; -} - -const SwagProductInformation = z.object({ - productId: z.coerce.number(), - sizeId: z.coerce.number(), -}); - -async function getProductInformation() { - const [productId, sizeId] = await Promise.all([ - redis.get('swag_up:product_id'), - redis.get('swag_up:size_id'), - ]); - - const result = SwagProductInformation.safeParse({ - productId, - sizeId, - }); - - if (!result.success) { - throw new Error( - 'SwagUp information was either not found or misformatted in Redis.' - ); - } - - return result.data; -} - -// Authentication - -async function retrieveTokens(): Promise { - const [accessToken = '', refreshToken = ''] = await Promise.all([ - redis.get('swag_up:access_token'), - redis.get('swag_up:refresh_token'), - ]); - - if (!accessToken || !refreshToken) { - throw new Error( - 'There was some token(s) not found. Please reauthenticate via the SwagUp OAuth 2.0 flow.' - ); - } - - // This is just hitting a dummy endpoint on the SwagUp API to ensure that - // the access token is working properly (not expired, etc). Ideally, - // SwagUp would have an endpoint like POST /token/test to know if the - // token needed to be refreshed or not, but this is our current - // workaround. - const response = await fetch(`${SWAG_UP_API_URL}/accounts?limit=1`, { - headers: { - Authorization: `Bearer ${accessToken}`, - }, - }); - - if (response.ok) { - return { - accessToken, - refreshToken, - }; - } - - const { access_token: newAccessToken, refresh_token: newRefreshToken } = - await refreshAuthentication(refreshToken); - - await Promise.all([ - redis.set('swag_up:access_token', newAccessToken), - redis.set('swag_up:refresh_token', newRefreshToken), - ]); - - return { - accessToken: newAccessToken, - refreshToken: newRefreshToken, - }; -} - -async function refreshAuthentication( - refreshToken: string -): Promise { - const url = new URL('https://signin.swagup.com/oauth2/default/v1/token'); - - url.searchParams.set('grant_type', 'refresh_token'); - url.searchParams.set('refresh_token', refreshToken); - - const basicToken = encodeBasicAuthenticationToken( - ENV.SWAG_UP_CLIENT_ID, - ENV.SWAG_UP_CLIENT_SECRET - ); - - const response = await fetch(url, { - method: 'post', - headers: { - Accept: 'application/json', - Authorization: `Basic ${basicToken}`, - 'Content-Type': 'application/x-www-form-urlencoded', - }, - }); - - const data = validate(OAuthTokenResponse, await response.json()); - - return data; -} diff --git a/packages/core/src/modules/swag-pack/swag-pack.types.ts b/packages/core/src/modules/swag-pack/swag-pack.types.ts deleted file mode 100644 index 3a3e85ec..00000000 --- a/packages/core/src/modules/swag-pack/swag-pack.types.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { z } from 'zod'; - -import { Address, Student } from '@oyster/types'; - -export const ClaimSwagPackInput = z.object({ - addressCity: Address.shape.city, - addressCountry: Address.shape.country, - addressLine1: Address.shape.line1, - addressLine2: Address.shape.line2, - addressState: Address.shape.state, - addressZip: Address.shape.zip, - studentId: Student.shape.id, -}); - -export type ClaimSwagPackInput = z.infer; diff --git a/packages/core/src/modules/swag-pack/swag-pack.worker.ts b/packages/core/src/modules/swag-pack/swag-pack.worker.ts deleted file mode 100644 index 7c6e2f38..00000000 --- a/packages/core/src/modules/swag-pack/swag-pack.worker.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { match } from 'ts-pattern'; - -import { SwagPackBullJob } from '@/infrastructure/bull/bull.types'; -import { registerWorker } from '@/infrastructure/bull/use-cases/register-worker'; -import { notifySwagPackInventory } from './use-cases/notify-swag-pack-inventory'; - -export const swagPackWorker = registerWorker( - 'swag_pack', - SwagPackBullJob, - async (job) => { - return match(job) - .with({ name: 'swag_pack.inventory.notify' }, ({ data }) => { - return notifySwagPackInventory(data); - }) - .exhaustive(); - } -); diff --git a/packages/core/src/modules/swag-pack/use-cases/claim-swag-pack.ts b/packages/core/src/modules/swag-pack/use-cases/claim-swag-pack.ts deleted file mode 100644 index 6c46827a..00000000 --- a/packages/core/src/modules/swag-pack/use-cases/claim-swag-pack.ts +++ /dev/null @@ -1,91 +0,0 @@ -import dedent from 'dedent'; - -import { db } from '@oyster/db'; - -import { job } from '@/infrastructure/bull/use-cases/job'; -import { fail, type Result, success } from '@/shared/utils/core.utils'; -import { orderSwagPack } from '../swag-pack.service'; -import { type ClaimSwagPackInput } from '../swag-pack.types'; - -export async function claimSwagPack({ - addressCity, - addressCountry, - addressLine1, - addressLine2, - addressState, - addressZip, - studentId, -}: ClaimSwagPackInput): Promise { - // We save the address regardless if the swag pack order failed or not so - // we'll be able to send them something in the future. - const student = await db - .updateTable('students') - .set({ - addressCity, - addressCountry, - addressLine1, - addressLine2, - addressState, - addressZip, - }) - .where('id', '=', studentId) - .returning(['email', 'firstName', 'lastName']) - .executeTakeFirstOrThrow(); - - // Currently, SwagUp only supports the US, but not Puerto Rico. - // See: https://support.swagup.com/en/articles/6952397-international-shipments-restricted-items - const isAddressSupported = addressCountry === 'US' && addressState !== 'PR'; - - // If the address isn't supported, then we'll send a notification to our - // team to create a gift card manually for them. - if (!isAddressSupported) { - const notification = dedent` - ${student.firstName} ${student.lastName} (${student.email}) is attempting to claim a swag pack, but they're either from Puerto Rico or Canada, which is not supported for our product. - - We let them know we'll send them a merch store gift card in the next "few days"! - `; - - job('notification.slack.send', { - message: notification, - workspace: 'internal', - }); - - const error = dedent` - Unfortunately, our swag pack provider, SwagUp, does not support shipments to Puerto Rico and Canada. Instead, we will send you a gift card to our official merch store. - - Our team has been notified, please give us a few days to complete this request! - `; - - return fail({ - code: 400, - error, - }); - } - - const swagPackOrderId = await orderSwagPack({ - contact: { - address: { - city: addressCity, - country: addressCountry, - line1: addressLine1, - line2: addressLine2, - state: addressState, - zip: addressZip, - }, - email: student.email, - firstName: student.firstName, - lastName: student.lastName, - }, - }); - - await db - .updateTable('students') - .set({ - claimedSwagPackAt: new Date(), - swagUpOrderId: swagPackOrderId, - }) - .where('id', '=', studentId) - .execute(); - - return success({}); -} diff --git a/packages/core/src/modules/swag-pack/use-cases/notify-swag-pack-inventory.ts b/packages/core/src/modules/swag-pack/use-cases/notify-swag-pack-inventory.ts deleted file mode 100644 index 1ef82a0e..00000000 --- a/packages/core/src/modules/swag-pack/use-cases/notify-swag-pack-inventory.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { type GetBullJobData } from '@/infrastructure/bull/bull.types'; -import { job } from '@/infrastructure/bull/use-cases/job'; -import { getSwagPackInventory } from '../swag-pack.service'; - -export async function notifySwagPackInventory( - _: GetBullJobData<'swag_pack.inventory.notify'> -) { - const inventory = await getSwagPackInventory(); - - job('notification.slack.send', { - message: `Our current SwagUp inventory is: *${inventory}*`, - workspace: 'internal', - }); -} diff --git a/packages/core/src/shared/env.ts b/packages/core/src/shared/env.ts index 0099114e..bb265c49 100644 --- a/packages/core/src/shared/env.ts +++ b/packages/core/src/shared/env.ts @@ -33,8 +33,6 @@ export const ENV = { .SLACK_INTRODUCTIONS_CHANNEL_ID as string, SLACK_SIGNING_SECRET: process.env.SLACK_SIGNING_SECRET as string, STUDENT_PROFILE_URL: process.env.STUDENT_PROFILE_URL as string, - SWAG_UP_CLIENT_ID: process.env.SWAG_UP_CLIENT_ID as string, - SWAG_UP_CLIENT_SECRET: process.env.SWAG_UP_CLIENT_SECRET as string, }; // TODO: Below are the only variables that we need to process in the core, diff --git a/packages/types/src/domain/student.ts b/packages/types/src/domain/student.ts index a0cdf467..b6438066 100644 --- a/packages/types/src/domain/student.ts +++ b/packages/types/src/domain/student.ts @@ -208,7 +208,6 @@ export const Student = Entity.merge(StudentSocialLinks) }), slackId: z.string().optional(), - swagUpOrderId: z.string().min(1).optional(), type: z.nativeEnum(MemberType), /**