feat: queue fix and points redeem confirmation email

This commit is contained in:
Artur
2024-11-15 11:01:06 -03:00
parent 7ff2ccab47
commit 0a198a6c90
5 changed files with 183 additions and 81 deletions

View File

@@ -1,3 +1,3 @@
import { ConnectionOptions } from 'bullmq'
export const redisConnection: ConnectionOptions = { host: 'redis' }
export const redisConnection: ConnectionOptions = { host: 'redis', port: 6379, url: 'redis:6379' }

View File

@@ -1,6 +1,8 @@
import { Queue } from 'bullmq'
import { PerkPurchaseWorkerData } from './workers/perk'
import { redisConnection } from '../config/redis'
import './workers/perk'
export const perkPurchaseQueue = new Queue<PerkPurchaseWorkerData>('PerkPurchase', {
connection: { host: 'redis' },
connection: redisConnection,
})

View File

@@ -1,5 +1,6 @@
import { z } from 'zod'
import { protectedProcedure, publicProcedure, router } from '../trpc'
import { QueueEvents } from 'bullmq'
import { fundSlugs } from '../../utils/funds'
import { keycloak, printfulApi, prisma, strapiApi } from '../services'
import {
@@ -22,6 +23,7 @@ import { AxiosResponse } from 'axios'
import { POINTS_REDEEM_PRICE_USD } from '../../config'
import { authenticateKeycloakClient } from '../utils/keycloak'
import { perkPurchaseQueue } from '../queues'
import { redisConnection } from '../../config/redis'
export const perkRouter = router({
getBalance: protectedProcedure.query(async ({ ctx }) => {
@@ -199,7 +201,7 @@ export const perkRouter = router({
throw new TRPCError({ code: 'BAD_REQUEST', message: 'Insufficient balance.' })
}
await perkPurchaseQueue.add('purchase', {
const purchaseJob = await perkPurchaseQueue.add('purchase', {
perk,
perkPrintfulSyncVariantId: input.perkPrintfulSyncVariantId,
shippingAddressLine1: input.shippingAddressLine1,
@@ -214,5 +216,9 @@ export const perkRouter = router({
userEmail: user.email,
userFullname: user?.attributes?.name?.[0],
})
await purchaseJob.waitUntilFinished(
new QueueEvents('PerkPurchase', { connection: redisConnection })
)
}),
})

View File

@@ -113,3 +113,72 @@ ${htmlFromMarkdown}`
html,
})
}
type SendPerkPurchaseConfirmationEmailParams = {
to: string
perkName: string
address?: {
address1: string
address2?: string
city: string
state?: string
country: string
zip: string
}
pointsRedeemed: number
}
export async function sendPerkPurchaseConfirmationEmail({
to,
perkName,
address,
pointsRedeemed,
}: SendPerkPurchaseConfirmationEmailParams) {
const markdown = `You redeemed points from MAGIC Grants!
Redeemed item: ${perkName}
Number of points redeemed: ${pointsFormat.format(pointsRedeemed)}
${
address
? `Mailing address
Address line 1: ${address.address1}
Address line 2: ${address.address2 ? address.address2 : '-'}
City: ${address.city}
State: ${address.state}
Country: ${address.country}
Zip: ${address.zip}`
: ''
}
If you did not make this redemption, please contact [info@magicgrants.org](info@magicgrants.org) immediately, since your account may be compromised. Points are not refundable once redeemed. If you have an issue with your order, please contact us.`
const htmlFromMarkdown = await markdownToHtml(markdown)
const html = `<style>
html {
display: flex;
}
body {
max-width: 700px;
padding: 20px;
margin: 0 auto;
font-family: sans-serif;
background-color: #F1F5FF;
}
a {
color: #3a76f0;
}
</style>
${htmlFromMarkdown}`
return transporter.sendMail({
from: env.SES_VERIFIED_SENDER,
to,
html,
})
}

View File

@@ -1,4 +1,7 @@
import { Worker } from 'bullmq'
import { AxiosResponse } from 'axios'
import { TRPCError } from '@trpc/server'
import { redisConnection } from '../../config/redis'
import { estimatePrintfulOrderCost, getUserPointBalance } from '../utils/perks'
import { POINTS_REDEEM_PRICE_USD } from '../../config'
@@ -10,9 +13,8 @@ import {
StrapiCreatePointBody,
StrapiPerk,
} from '../types'
import { TRPCError } from '@trpc/server'
import { printfulApi, strapiApi } from '../services'
import { AxiosResponse } from 'axios'
import { sendPerkPurchaseConfirmationEmail } from '../utils/mailing'
export type PerkPurchaseWorkerData = {
perk: StrapiPerk
@@ -30,96 +32,119 @@ export type PerkPurchaseWorkerData = {
userFullname: string
}
export const perkPurchaseWorker = new Worker<PerkPurchaseWorkerData>(
'PerkPurchase',
async (job) => {
// Check if user has enough balance
let deductionAmount = 0
const globalForWorker = global as unknown as { hasInitializedWorkers: boolean }
if (job.data.perk.printfulProductId && job.data.perkPrintfulSyncVariantId) {
const printfulCostEstimate = await estimatePrintfulOrderCost({
address1: job.data.shippingAddressLine1!,
address2: job.data.shippingAddressLine2 || '',
city: job.data.shippingCity!,
stateCode: job.data.shippingState!,
countryCode: job.data.shippingCountry!,
zip: job.data.shippingZip!,
phone: job.data.shippingPhone!,
name: job.data.userFullname,
email: job.data.userEmail,
tax_number: job.data.shippingTaxNumber,
printfulSyncVariantId: job.data.perkPrintfulSyncVariantId,
})
if (!globalForWorker.hasInitializedWorkers) console.log('hey')
deductionAmount = Math.ceil(printfulCostEstimate.costs.total / POINTS_REDEEM_PRICE_USD)
} else {
deductionAmount = job.data.perk.price
}
if (!globalForWorker.hasInitializedWorkers)
new Worker<PerkPurchaseWorkerData>(
'PerkPurchase',
async (job) => {
// Check if user has enough balance
let deductionAmount = 0
const currentBalance = await getUserPointBalance(job.data.userId)
const balanceAfterPurchase = currentBalance - deductionAmount
if (balanceAfterPurchase < 0) {
throw new TRPCError({ code: 'BAD_REQUEST', message: 'Insufficient balance.' })
}
// Create printful order (if applicable)
if (job.data.perk.printfulProductId && job.data.perkPrintfulSyncVariantId) {
const result = await printfulApi.post<
{},
AxiosResponse<PrintfulCreateOrderRes>,
PrintfulCreateOrderReq
>(`/orders`, {
recipient: {
if (job.data.perk.printfulProductId && job.data.perkPrintfulSyncVariantId) {
const printfulCostEstimate = await estimatePrintfulOrderCost({
address1: job.data.shippingAddressLine1!,
address2: job.data.shippingAddressLine2 || '',
city: job.data.shippingCity!,
state_code: job.data.shippingState!,
country_code: job.data.shippingCountry!,
stateCode: job.data.shippingState!,
countryCode: job.data.shippingCountry!,
zip: job.data.shippingZip!,
phone: job.data.shippingPhone!,
name: job.data.userFullname,
email: job.data.userEmail,
tax_number: job.data.shippingTaxNumber,
},
items: [{ quantity: 1, sync_variant_id: job.data.perkPrintfulSyncVariantId }],
})
}
printfulSyncVariantId: job.data.perkPrintfulSyncVariantId,
})
// Create strapi order
const {
data: { data: order },
} = await strapiApi.post<StrapiCreateOrderRes, any, StrapiCreateOrderBody>('/orders', {
data: {
perk: job.data.perk.documentId,
userId: job.data.userId,
userEmail: job.data.userEmail,
shippingAddressLine1: job.data.shippingAddressLine1,
shippingAddressLine2: job.data.shippingAddressLine2,
shippingCity: job.data.shippingCity,
shippingState: job.data.shippingState,
shippingCountry: job.data.shippingCountry,
shippingZip: job.data.shippingZip,
shippingPhone: job.data.shippingPhone,
},
})
deductionAmount = Math.ceil(printfulCostEstimate.costs.total / POINTS_REDEEM_PRICE_USD)
} else {
deductionAmount = job.data.perk.price
}
try {
// Deduct points
await strapiApi.post<any, any, StrapiCreatePointBody>('/points', {
const currentBalance = await getUserPointBalance(job.data.userId)
const balanceAfterPurchase = currentBalance - deductionAmount
if (balanceAfterPurchase < 0) {
throw new TRPCError({ code: 'BAD_REQUEST', message: 'Insufficient balance.' })
}
// Create printful order (if applicable)
if (job.data.perk.printfulProductId && job.data.perkPrintfulSyncVariantId) {
const result = await printfulApi.post<
{},
AxiosResponse<PrintfulCreateOrderRes>,
PrintfulCreateOrderReq
>(`/orders`, {
recipient: {
address1: job.data.shippingAddressLine1!,
address2: job.data.shippingAddressLine2 || '',
city: job.data.shippingCity!,
state_code: job.data.shippingState!,
country_code: job.data.shippingCountry!,
zip: job.data.shippingZip!,
phone: job.data.shippingPhone!,
name: job.data.userFullname,
email: job.data.userEmail,
tax_number: job.data.shippingTaxNumber,
},
items: [{ quantity: 1, sync_variant_id: job.data.perkPrintfulSyncVariantId }],
})
}
// Create strapi order
const {
data: { data: order },
} = await strapiApi.post<StrapiCreateOrderRes, any, StrapiCreateOrderBody>('/orders', {
data: {
balanceChange: (-deductionAmount).toString(),
balance: balanceAfterPurchase.toString(),
userId: job.data.userId,
perk: job.data.perk.documentId,
order: order.documentId,
userId: job.data.userId,
userEmail: job.data.userEmail,
shippingAddressLine1: job.data.shippingAddressLine1,
shippingAddressLine2: job.data.shippingAddressLine2,
shippingCity: job.data.shippingCity,
shippingState: job.data.shippingState,
shippingCountry: job.data.shippingCountry,
shippingZip: job.data.shippingZip,
shippingPhone: job.data.shippingPhone,
},
})
} catch (error) {
// If it fails, delete order
await strapiApi.delete(`/orders/${order.documentId}`)
throw error
}
},
{ connection: redisConnection, concurrency: 1 }
)
try {
// Deduct points
await strapiApi.post<any, any, StrapiCreatePointBody>('/points', {
data: {
balanceChange: (-deductionAmount).toString(),
balance: balanceAfterPurchase.toString(),
userId: job.data.userId,
perk: job.data.perk.documentId,
order: order.documentId,
},
})
} catch (error) {
// If it fails, delete order
await strapiApi.delete(`/orders/${order.documentId}`)
throw error
}
sendPerkPurchaseConfirmationEmail({
to: job.data.userEmail,
perkName: job.data.perk.name,
pointsRedeemed: deductionAmount,
address: job.data.shippingAddressLine1
? {
address1: job.data.shippingAddressLine1,
address2: job.data.shippingAddressLine2,
state: job.data.shippingState,
city: job.data.shippingCity!,
country: job.data.shippingCountry!,
zip: job.data.shippingZip!,
}
: undefined,
})
},
{ connection: redisConnection, concurrency: 1 }
)
if (process.env.NODE_ENV !== 'production') globalForWorker.hasInitializedWorkers = true