Skip to content

Commit

Permalink
feat: convertkit sync (#288)
Browse files Browse the repository at this point in the history
* feat: convertkit syncing

* make convertkit work

* let them cook

* add purchase to convertkit props

* add changeset

* sigh

* fix tests after discount changes

* fix builds
  • Loading branch information
joelhooks authored Sep 7, 2024
1 parent f16910b commit cc4cc87
Show file tree
Hide file tree
Showing 16 changed files with 396 additions and 21 deletions.
5 changes: 5 additions & 0 deletions .changeset/big-pillows-listen.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@coursebuilder/core": patch
---

various changes for ProNextJS etc release
1 change: 1 addition & 0 deletions .changeset/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"value-based-design",
"epic-web",
"pro-nextjs",
"astro-party",
"js-visualized",
"egghead"
]
Expand Down
4 changes: 2 additions & 2 deletions apps/astro-party/src/utils/pinecone.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export async function get_or_create_index(
const index: Index = await pc.index(opts.name)
return index
} catch (e) {
console.error('Error getting or creating index: ' + (e as Error).message)
console.debug('Error getting or creating index: ' + (e as Error).message)
}
}

Expand All @@ -38,6 +38,6 @@ export async function get_index(name: string) {
const pc = new Pinecone()
return await pc.index(name)
} catch (e) {
console.error('Error getting index: ' + (e as Error).message)
console.debug('Error getting index: ' + (e as Error).message)
}
}
4 changes: 2 additions & 2 deletions apps/course-builder-web/src/utils/pinecone.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export async function get_or_create_index(
const index: Index = await pc.index(opts.name)
return index
} catch (e) {
console.error('Error getting or creating index: ' + (e as Error).message)
console.debug('Error getting or creating index: ' + (e as Error).message)
}
}

Expand All @@ -38,6 +38,6 @@ export async function get_index(name: string) {
const pc = new Pinecone()
return await pc.index(name)
} catch (e) {
console.error('Error getting index: ' + (e as Error).message)
console.debug('Error getting index: ' + (e as Error).message)
}
}
4 changes: 2 additions & 2 deletions apps/epic-web/src/utils/pinecone.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export async function get_or_create_index(
const index: Index = await pc.index(opts.name)
return index
} catch (e) {
console.error('Error getting or creating index: ' + (e as Error).message)
console.debug('Error getting or creating index: ' + (e as Error).message)
}
}

Expand All @@ -38,6 +38,6 @@ export async function get_index(name: string) {
const pc = new Pinecone()
return await pc.index(name)
} catch (e) {
console.error('Error getting index: ' + (e as Error).message)
console.debug('Error getting index: ' + (e as Error).message)
}
}
17 changes: 17 additions & 0 deletions apps/js-visualized/src/coursebuilder/email-list-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,23 @@ function EmailListProvider(
options,
apiKey: options.apiKey,
apiSecret: options.apiSecret,
getSubscriberByEmail: async (email: string) => {
if (!email) return null

const user = await db.query.users.findFirst({
where: (user, { eq }) => eq(user.email, email),
})

return user
? {
id: user.id,
first_name: user.name,
email_address: user.email,
// TODO: filter the fields?
fields: user.fields || {},
}
: null
},
getSubscriber: async (subscriberId: string | null | CookieOption) => {
if (typeof subscriberId !== 'string') {
return null
Expand Down
4 changes: 2 additions & 2 deletions apps/pro-aws/src/utils/pinecone.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export async function get_or_create_index(
const index: Index = await pc.index(opts.name)
return index
} catch (e) {
console.error('Error getting or creating index: ' + (e as Error).message)
console.debug('Error getting or creating index: ' + (e as Error).message)
}
}

Expand All @@ -38,6 +38,6 @@ export async function get_index(name: string) {
const pc = new Pinecone()
return await pc.index(name)
} catch (e) {
console.error('Error getting index: ' + (e as Error).message)
console.debug('Error getting index: ' + (e as Error).message)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { emailListProvider } from '@/coursebuilder/email-list-provider'
import { db } from '@/db'
import { purchases, users } from '@/db/schema'
import { inngest } from '@/inngest/inngest.server'
import { format } from 'date-fns'
import { eq } from 'drizzle-orm'

import { NEW_PURCHASE_CREATED_EVENT } from '@coursebuilder/core/inngest/commerce/event-new-purchase-created'

export const addPurchasesConvertkit = inngest.createFunction(
{
id: `add-purchase-convertkit`,
name: 'Add Purchase Convertkit',
idempotency: 'event.user.email',
},
{ event: NEW_PURCHASE_CREATED_EVENT },
async ({ event, step }) => {
const user = await step.run('get user', async () => {
return db.query.users.findFirst({
where: eq(users.id, event.user.id),
with: {
accounts: true,
purchases: true,
},
})
})

if (!user) throw new Error('No user found')

const purchase = await step.run('get purchase', async () => {
return db.query.purchases.findFirst({
where: eq(purchases.id, event.data.purchaseId),
})
})

if (!purchase) throw new Error('No purchase found')

const convertkitUser = await step.run('get convertkit user', async () => {
console.log('get ck user', { user })
return emailListProvider.getSubscriberByEmail(user.email)
})

if (convertkitUser && emailListProvider.updateSubscriberFields) {
await step.run('update convertkit user', async () => {
return emailListProvider.updateSubscriberFields?.({
subscriberId: convertkitUser.id,
fields: {
purchased_pronextjs_course_on: format(
new Date(purchase.createdAt),
'yyyy-MM-dd HH:mm:ss z',
),
},
})
})
console.log(`synced convertkit tags for ${purchase.id}`)
} else {
console.log(`no convertkit tags to sync for ${user.email}`)
}

return 'No discord account found for user'
},
)
138 changes: 138 additions & 0 deletions apps/pro-nextjs/src/inngest/functions/sync-purchase-tags.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import { emailListProvider } from '@/coursebuilder/email-list-provider'
import { db } from '@/db'
import {
accounts,
purchases as purchasesTable,
users as usersTable,
} from '@/db/schema'
import { inngest } from '@/inngest/inngest.server'
import { format } from 'date-fns'
import { and, eq, inArray } from 'drizzle-orm'
import { z } from 'zod'

export const SYNC_PURCHASE_TAGS_EVENT = 'purchase/sync-tags'

export type SyncPurchaseTags = {
name: typeof SYNC_PURCHASE_TAGS_EVENT
data: {}
}

export const syncPurchaseTags = inngest.createFunction(
{ id: `sync-purchase-tags`, name: `Sync Purchase Tags` },
{
event: SYNC_PURCHASE_TAGS_EVENT,
},
async ({ event, step }) => {
const validPurchases = await step.run('get purchases', async () => {
return db.query.purchases.findMany({
where: inArray(purchasesTable.status, ['Valid', 'Restricted']),
})
})

for (const purchase of validPurchases) {
const userId = purchase.userId

if (!userId) continue

const user = await step.run('get user', async () => {
return db.query.users.findFirst({
where: eq(usersTable.id, userId),
})
})

if (!user) throw new Error('No user found')

// const discordAccount = await step.run(
// 'check if discord is connected',
// async () => {
// return db.query.accounts.findFirst({
// where: and(
// eq(accounts.userId, user.id),
// eq(accounts.provider, 'discord'),
// ),
// })
// },
// )
//
// if (discordAccount) {
// console.log('discord account is connected')
// const DiscordMemberBasicSchema = z.object({
// user: z.object({
// id: z.string(),
// }),
// roles: z.array(z.string()),
// })
//
// let discordMember = await step.run('get discord member', async () => {
// return DiscordMemberBasicSchema.parse(
// await fetchJsonAsDiscordBot<DiscordMember | DiscordError>(
// `guilds/${env.DISCORD_GUILD_ID}/members/${discordAccount.providerAccountId}`,
// ),
// )
// })
//
// console.log('discord member', discordMember.user.id)
//
// await step.run('update basic discord roles for user', async () => {
// console.log('updating discord roles', 'user' in discordMember)
//
// const roles = Array.from(
// new Set([...discordMember.roles, env.DISCORD_MEMBER_ROLE_ID]),
// )
//
// console.log('roles', { roles })
//
// return await fetchJsonAsDiscordBot(
// `guilds/${env.DISCORD_GUILD_ID}/members/${discordMember.user.id}`,
// {
// method: 'PATCH',
// body: JSON.stringify({
// roles,
// }),
// headers: {
// 'Content-Type': 'application/json',
// },
// },
// )
// })
//
// let relaodedMember = await step.run(
// 'reload discord member',
// async () => {
// return await fetchJsonAsDiscordBot<DiscordMember | DiscordError>(
// `guilds/${env.DISCORD_GUILD_ID}/members/${discordMember.user.id}`,
// )
// },
// )
//
//
//
// console.log({ relaodedMember })
// }

const convertkitUser = await step.run('get convertkit user', async () => {
console.log('get ck user', { user })
return emailListProvider.getSubscriberByEmail(user.email)
})

if (convertkitUser && emailListProvider.updateSubscriberFields) {
await step.run('update convertkit user', async () => {
return emailListProvider.updateSubscriberFields?.({
subscriberId: convertkitUser.id,
fields: {
purchased_pronextjs_course_on: format(
new Date(purchase.createdAt),
'yyyy-MM-dd HH:mm:ss z',
),
},
})
})
console.log(`synced convertkit tags for ${purchase.id}`)
} else {
console.log(`no convertkit tags to sync for ${user.email}`)
}

await step.sleep('sleep for 200ms', '200ms')
}
},
)
4 changes: 4 additions & 0 deletions apps/pro-nextjs/src/inngest/inngest.config.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { imageResourceCreated } from '@/inngest/functions/cloudinary/image-resource-created'
import { addPurchasesConvertkit } from '@/inngest/functions/convertkit/add-purchased-convertkit'
import { addPurchaseRoleDiscord } from '@/inngest/functions/discord/add-purchase-role-discord'
import { discordAccountLinked } from '@/inngest/functions/discord/discord-account-linked'
import { removePurchaseRoleDiscord } from '@/inngest/functions/discord/remove-purchase-role-discord'
Expand All @@ -11,6 +12,7 @@ import { postPurchaseWaitForProgress } from '@/inngest/functions/post-purchase-w
import { postPurchaseWorkflow } from '@/inngest/functions/post-purchase-workflow'
import { postmarkWebhook } from '@/inngest/functions/postmark/postmarks-webhooks-handler'
import { progressWasMade } from '@/inngest/functions/progress-was-made'
import { syncPurchaseTags } from '@/inngest/functions/sync-purchase-tags'
import { userCreated } from '@/inngest/functions/user-created'
import { inngest } from '@/inngest/inngest.server'

Expand Down Expand Up @@ -41,5 +43,7 @@ export const inngestConfig = {
postPurchaseWaitForProgress,
postPurchaseNoProgress,
noProgressContinued,
syncPurchaseTags,
addPurchasesConvertkit,
],
}
5 changes: 5 additions & 0 deletions apps/pro-nextjs/src/inngest/inngest.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ import {
PostmarkWebhook,
} from '@/inngest/events/postmark-webhook'
import { USER_CREATED_EVENT, UserCreated } from '@/inngest/events/user-created'
import {
SYNC_PURCHASE_TAGS_EVENT,
SyncPurchaseTags,
} from '@/inngest/functions/sync-purchase-tags'
import { authOptions } from '@/server/auth'
import { EventSchemas, Inngest } from 'inngest'
import { UTApi } from 'uploadthing/server'
Expand Down Expand Up @@ -71,6 +75,7 @@ export type Events = {
[LESSON_COMPLETED_EVENT]: LessonCompleted
[OAUTH_PROVIDER_ACCOUNT_LINKED_EVENT]: OauthProviderAccountLinked
[NO_PROGRESS_MADE_EVENT]: NoProgressMade
[SYNC_PURCHASE_TAGS_EVENT]: SyncPurchaseTags
}

const callbackBase =
Expand Down
2 changes: 1 addition & 1 deletion apps/pro-nextjs/src/lib/discord-query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ export async function fetchJsonAsDiscordBot<JsonType = unknown>(
...config?.headers,
},
})
return (await res.json()) as JsonType
return (await res.json().catch((e) => e)) as JsonType
}

export async function fetchAsDiscordBot(
Expand Down
4 changes: 2 additions & 2 deletions apps/value-based-design/src/utils/pinecone.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export async function get_or_create_index(
const index: Index = await pc.index(opts.name)
return index
} catch (e) {
console.error('Error getting or creating index: ' + (e as Error).message)
console.debug('Error getting or creating index: ' + (e as Error).message)
}
}

Expand All @@ -38,6 +38,6 @@ export async function get_index(name: string) {
const pc = new Pinecone()
return await pc.index(name)
} catch (e) {
console.error('Error getting index: ' + (e as Error).message)
console.debug('Error getting index: ' + (e as Error).message)
}
}
Loading

0 comments on commit cc4cc87

Please sign in to comment.