Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: remove agreement code #33

Merged
merged 1 commit into from
Jan 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading