Skip to content

Commit

Permalink
Merge branch 'main' into remove-convertKit-usage
Browse files Browse the repository at this point in the history
  • Loading branch information
Shpigford authored Jan 12, 2024
2 parents d61bd3b + 44c751a commit 5a92fb7
Show file tree
Hide file tree
Showing 14 changed files with 29 additions and 597 deletions.
3 changes: 0 additions & 3 deletions apps/client/pages/onboarding.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import {
OtherAccounts,
Profile,
OnboardingNavbar,
Terms,
Welcome,
YourMaybe,
OnboardingBackground,
Expand All @@ -29,8 +28,6 @@ function getStepComponent(stepKey?: string): (props: StepProps) => JSX.Element {
return AddFirstAccount
case 'accountSelection':
return OtherAccounts
case 'terms':
return Terms
case 'maybe':
return YourMaybe
case 'welcome':
Expand Down
44 changes: 2 additions & 42 deletions apps/client/pages/settings.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { ReactElement } from 'react'
import { useUserApi, useQueryParam, BrowserUtil } from '@maybe-finance/client/shared'
import { useQueryParam } from '@maybe-finance/client/shared'
import {
AccountSidebar,
BillingPreferences,
Expand All @@ -8,15 +8,11 @@ import {
UserDetails,
WithSidebarLayout,
} from '@maybe-finance/client/features'
import { Button, Tab } from '@maybe-finance/design-system'
import { RiAttachmentLine } from 'react-icons/ri'
import { Tab } from '@maybe-finance/design-system'
import { useRouter } from 'next/router'
import Script from 'next/script'

export default function SettingsPage() {
const { useNewestAgreements } = useUserApi()
const signedAgreements = useNewestAgreements('user')

const router = useRouter()

const tabs = ['details', 'notifications', 'security', 'documents', 'billing']
Expand All @@ -41,7 +37,6 @@ export default function SettingsPage() {
<Tab>Details</Tab>
<Tab>Notifications</Tab>
<Tab>Security</Tab>
<Tab>Documents</Tab>
<Tab>Billing</Tab>
</Tab.List>
<Tab.Panels>
Expand All @@ -58,41 +53,6 @@ export default function SettingsPage() {
<SecurityPreferences />
</div>
</Tab.Panel>

<Tab.Panel>
{signedAgreements.data ? (
<div className="max-w-lg border border-gray-600 px-4 py-3 rounded text-base">
<ul>
{signedAgreements.data.map((agreement) => (
<li
key={agreement.id}
className="flex items-center justify-between"
>
<div className="flex w-0 flex-1 items-center">
<RiAttachmentLine
className="h-4 w-4 shrink-0 text-gray-100"
aria-hidden="true"
/>
<span className="ml-2 w-0 flex-1 truncate text-gray-25">
{BrowserUtil.agreementName(agreement.type)}
</span>
</div>
<Button
as="a"
variant="link"
target="_blank"
href={agreement.url}
>
View
</Button>
</li>
))}
</ul>
</div>
) : (
<p className="text-gray-50">No documents found</p>
)}
</Tab.Panel>
<Tab.Panel>
<BillingPreferences />
</Tab.Panel>
Expand Down
149 changes: 0 additions & 149 deletions apps/server/src/app/routes/users.router.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,11 @@
import { Router } from 'express'
import axios from 'axios'
import type { UnlinkAccountsParamsProvider } from 'auth0'
import { keyBy, mapValues, uniqBy } from 'lodash'
import { subject } from '@casl/ability'
import { z } from 'zod'
import { DateUtil, type SharedType } from '@maybe-finance/shared'
import endpoint from '../lib/endpoint'
import env from '../../env'
import crypto from 'crypto'
import { DateTime } from 'luxon'
import {
type OnboardingState,
type RegisteredStep,
Expand Down Expand Up @@ -462,152 +459,6 @@ router.post(
})
)

/**
* Fetches the latest public version of each agreement
*/
router.get(
'/agreements/newest',
endpoint.create({
input: z.object({ type: z.enum(['public', 'user']) }),
resolve: async ({ ctx, input }) => {
const agreements = await (input.type === 'user'
? ctx.userService.getSignedAgreements(ctx.user!.id)
: ctx.userService.getNewestAgreements())

return agreements.map((agreement) => ({
...agreement,
url: `${env.NX_CDN_URL}/${agreement.src}`,
}))
},
})
)

router.post(
'/agreements/sign',
endpoint.create({
input: z.object({ agreementIds: z.number().array().length(5) }),
resolve: async ({ ctx, input }) => {
return ctx.userService.signAgreements(ctx.user!.id, input.agreementIds, ctx.s3)
},
})
)

/**
* Idempotent, admin-only route that should be run each time we update a legal agreement
*
* - Sends email notifications to users when agreements are updated
* - Records acknowledgement in S3
* - Bumps all successful users to latest agreement set in DB
*/
router.post(
'/agreements/notify-email',
endpoint.create({
resolve: async ({ ctx }) => {
ctx.ability.throwUnlessCan('manage', 'User')

const outdatedAgreements = await ctx.prisma.$queryRaw<
{
email: string
first_name: string
user_id: number
current_agreement_id: number
newest_agreement_id: number
}[]
>`
WITH signed_agreements AS (
SELECT DISTINCT ON (sa.user_id, a.type)
u.email,
u.first_name,
sa.user_id,
sa.agreement_id AS current_agreement_id,
na.id AS newest_agreement_id
FROM signed_agreement sa
LEFT JOIN "user" u ON u.id = sa.user_id
LEFT JOIN agreement a ON a.id = sa.agreement_id
LEFT JOIN LATERAL (
SELECT DISTINCT ON (a.type)
a.id, a.type
FROM agreement a
WHERE a.active
ORDER BY a.type, a.revision DESC
) na ON na.type = a.type
ORDER BY sa.user_id, a.type, a.revision DESC
)
SELECT *
FROM signed_agreements
WHERE current_agreement_id <> newest_agreement_id;
`

if (!outdatedAgreements.length) {
ctx.logger.info('All users have signed latest agreements, skipping email')
return {
updatedAgreementCount: 0,
}
}

ctx.logger.info(`Updating ${outdatedAgreements.length} outdated agreements`)

const newestAgreements = (await ctx.userService.getNewestAgreements()).map((a) => ({
...a,
url: `${env.NX_CDN_URL}/${a.src}`,
}))

// Only send 1 email per user that will cover all 4 agreements
const uniqueAgreements = uniqBy(outdatedAgreements, 'user_id')

// Send users a templated update email notifying them of document change
const batchResponse = await ctx.emailService.sendTemplate(
uniqueAgreements.map((agreement) => ({
to: agreement.email,
template: {
alias: 'agreements-update',
model: {
name: agreement.first_name ?? '',
urls: mapValues(keyBy(newestAgreements, 'type'), (a) => a.url),
},
},
}))
)

// Save audit records of email sends
ctx.logger.info(`Agreement update emails sent`, batchResponse)

const Body = Buffer.from(JSON.stringify(batchResponse))
const Key = `private/agreements/email-receipts/${DateTime.now().toISO()}-agreements-update-email-receipt.txt`
await ctx.s3.upload({
bucketKey: 'private',
Key,
Body,
ContentMD5: crypto.createHash('md5').update(Body).digest('base64'),
})

ctx.logger.info(`Agreement email receipt uploaded to S3 key=${Key}`)

// Find all successful emails and create new agreement signatures for each
const successfulUpdateEmails = batchResponse
.filter((result) => result.ErrorCode === 0 && result.To)
.map((v) => v.To!)

const agreementsToAcknowledge = outdatedAgreements.filter(
(oa) => successfulUpdateEmails.find((email) => email === oa.email) != null
)

// Our signature record pointer in DB
await ctx.prisma.signedAgreement.createMany({
data: agreementsToAcknowledge.map((oa) => ({
userId: oa.user_id,
agreementId: oa.newest_agreement_id,
src: Key,
})),
})

return {
updatedAgreementCount: successfulUpdateEmails.length,
}
},
})
)

router.delete(
'/',
endpoint.create({
Expand Down
2 changes: 1 addition & 1 deletion aws/maybe-app/lib/stacks/shared-stack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ export class SharedStack extends Stack {
removalPolicy: RemovalPolicy.RETAIN,
})

// WORM compliant bucket to store CDN assets such as client agreements, AMA uploads
// WORM compliant bucket to store CDN assets such as AMA uploads
const privateBucket = new Bucket(this, 'Assets', {
versioned: true,
removalPolicy: RemovalPolicy.RETAIN,
Expand Down
Loading

0 comments on commit 5a92fb7

Please sign in to comment.