Skip to content

Commit f168e9a

Browse files
authored
Campaign creation bug fix (#26)
1 parent bafb4fd commit f168e9a

File tree

10 files changed

+90
-40
lines changed

10 files changed

+90
-40
lines changed

RELEASE_NOTES.md

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,11 @@
11
### ✨ Features
22

3-
- Ability to manually resend messages.
4-
- Filter messages by status.
5-
- Messages are now ordered by last updated time.
6-
- Maintenance only deletes the message content now, not the message entries.
7-
- New features and improvements.
3+
- Message status independence - retry individual messages regardless of campaign status
84

95
### 🐛 Bug Fixes
106

11-
- Various bug fixes and optimizations.
7+
- Fixed campaign creation targeting wrong subscribers (now filters by selected lists only)
8+
- Fixed campaign cancellation
129

1310
### 📚 Docs
1411

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
-- AlterEnum
2+
ALTER TYPE "MessageStatus" ADD VALUE 'CANCELLED';

apps/backend/prisma/schema.prisma

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,7 @@ enum MessageStatus {
198198
CLICKED
199199
FAILED
200200
RETRYING
201+
CANCELLED
201202
}
202203

203204
model Message {

apps/backend/src/campaign/mutation.ts

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,7 @@ export const deleteCampaign = authProcedure
198198
})
199199
}
200200

201+
// On Delete: Cascade delete all messages
201202
await prisma.campaign.delete({
202203
where: { id: input.id },
203204
})
@@ -387,7 +388,7 @@ export const startCampaign = authProcedure
387388
return { campaign: updatedCampaign }
388389
})
389390

390-
export const cancel = authProcedure
391+
export const cancelCampaign = authProcedure
391392
.input(
392393
z.object({
393394
id: z.string(),
@@ -409,21 +410,34 @@ export const cancel = authProcedure
409410
})
410411
}
411412

412-
if (campaign.status !== "SENDING") {
413+
if (!["CREATING", "SENDING", "SCHEDULED"].includes(campaign.status)) {
413414
throw new TRPCError({
414415
code: "BAD_REQUEST",
415-
message: "Campaign is not in sending state",
416+
message: "Campaign cannot be cancelled",
416417
})
417418
}
418419

419-
await prisma.campaign.update({
420-
where: {
421-
id: input.id,
422-
},
423-
data: {
424-
status: "CANCELLED",
425-
},
426-
})
420+
await prisma.$transaction([
421+
prisma.campaign.update({
422+
where: {
423+
id: input.id,
424+
},
425+
data: {
426+
status: "CANCELLED",
427+
},
428+
}),
429+
prisma.message.updateMany({
430+
where: {
431+
campaignId: input.id,
432+
status: {
433+
in: ["QUEUED", "PENDING", "RETRYING"],
434+
},
435+
},
436+
data: {
437+
status: "CANCELLED",
438+
},
439+
}),
440+
])
427441

428442
return { success: true }
429443
})

apps/backend/src/campaign/router.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import {
44
updateCampaign,
55
deleteCampaign,
66
startCampaign,
7-
cancel,
7+
cancelCampaign,
88
sendTestEmail,
99
duplicateCampaign,
1010
} from "./mutation"
@@ -17,7 +17,7 @@ export const campaignRouter = router({
1717
get: getCampaign,
1818
list: listCampaigns,
1919
start: startCampaign,
20-
cancel,
20+
cancel: cancelCampaign,
2121
sendTestEmail,
2222
duplicate: duplicateCampaign,
2323
})

apps/backend/src/cron/processQueuedCampaigns.ts

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,19 @@ import { cronJob } from "./cron.utils"
1313
const BATCH_SIZE = 100
1414

1515
async function getSubscribersForCampaign(
16-
campaignId: string
16+
campaignId: string,
17+
selectedListIds: string[]
1718
): Promise<Map<string, Subscriber & { Metadata: SubscriberMetadata[] }>> {
19+
if (selectedListIds.length === 0) {
20+
return new Map()
21+
}
22+
1823
const subscribers = await prisma.subscriber.findMany({
1924
where: {
2025
Messages: { none: { campaignId } },
2126
ListSubscribers: {
2227
some: {
28+
listId: { in: selectedListIds },
2329
unsubscribedAt: null,
2430
},
2531
},
@@ -71,6 +77,9 @@ export const processQueuedCampaigns = cronJob(
7177
status: "CREATING",
7278
},
7379
include: {
80+
CampaignLists: {
81+
select: { listId: true },
82+
},
7483
Organization: {
7584
include: {
7685
GeneralSettings: true,
@@ -113,7 +122,12 @@ export const processQueuedCampaigns = cronJob(
113122

114123
const generalSettings = campaign.Organization.GeneralSettings
115124

116-
const allSubscribersMap = await getSubscribersForCampaign(campaign.id)
125+
const selectedListIds = campaign.CampaignLists.map((cl) => cl.listId)
126+
127+
const allSubscribersMap = await getSubscribersForCampaign(
128+
campaign.id,
129+
selectedListIds
130+
)
117131
if (allSubscribersMap.size === 0) {
118132
oneTimeLogger(
119133
"noSubscribers",
@@ -234,6 +248,7 @@ export const processQueuedCampaigns = cronJob(
234248
Messages: { none: { campaignId: campaign.id } },
235249
ListSubscribers: {
236250
some: {
251+
listId: { in: selectedListIds },
237252
unsubscribedAt: null,
238253
},
239254
},

apps/backend/src/cron/sendMessages.ts

Lines changed: 15 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -50,26 +50,22 @@ export const sendMessagesCron = cronJob("sendMessages", async () => {
5050
continue
5151
}
5252

53+
// Message status is now independent of campaign status.
54+
// This allows retrying individual messages even for completed campaigns.
55+
// We only filter by QUEUED and RETRYING message statuses.
5356
const messages = await prisma.message.findMany({
5457
where: {
55-
AND: [
58+
Campaign: {
59+
organizationId: organization.id,
60+
},
61+
OR: [
62+
{ status: "QUEUED" },
5663
{
57-
Campaign: {
58-
status: "SENDING",
59-
organizationId: organization.id,
64+
status: "RETRYING",
65+
lastTriedAt: {
66+
lte: subSeconds(new Date(), emailSettings.retryDelay),
6067
},
6168
},
62-
{
63-
OR: [
64-
{ status: "QUEUED" },
65-
{
66-
status: "RETRYING",
67-
lastTriedAt: {
68-
lte: subSeconds(new Date(), emailSettings.retryDelay),
69-
},
70-
},
71-
],
72-
},
7369
],
7470
},
7571
include: {
@@ -90,6 +86,9 @@ export const sendMessagesCron = cronJob("sendMessages", async () => {
9086
const noMoreRetryingMessages = await prisma.message.count({
9187
where: {
9288
status: "RETRYING",
89+
Campaign: {
90+
organizationId: organization.id,
91+
},
9392
},
9493
})
9594

@@ -101,7 +100,7 @@ export const sendMessagesCron = cronJob("sendMessages", async () => {
101100
Messages: {
102101
every: {
103102
status: {
104-
in: ["SENT", "FAILED", "OPENED", "CLICKED"],
103+
in: ["SENT", "FAILED", "OPENED", "CLICKED", "CANCELLED"],
105104
},
106105
},
107106
},

apps/backend/src/index.ts

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,33 @@ export type * from "./types"
44

55
import { app } from "./app"
66
import { initializeCronJobs } from "./cron/cron"
7+
import { prisma } from "./utils/prisma"
78

89
const cronController = initializeCronJobs()
910

1011
const PORT = process.env.PORT || 5000
1112

12-
app.listen(PORT, () => {
13-
console.log(`Server is running on port ${PORT}`)
13+
prisma.$connect().then(async () => {
14+
console.log("Connected to database")
15+
16+
// For backwards compatibility, set all messages that have campaign status === "CANCELLED" to "CANCELLED"
17+
await prisma.message.updateMany({
18+
where: {
19+
Campaign: {
20+
status: "CANCELLED",
21+
},
22+
status: {
23+
in: ["QUEUED", "PENDING", "RETRYING"],
24+
},
25+
},
26+
data: {
27+
status: "CANCELLED",
28+
},
29+
})
30+
31+
app.listen(PORT, () => {
32+
console.log(`Server is running on port ${PORT}`)
33+
})
1434
})
1535

1636
// Handle graceful shutdown

apps/web/src/pages/dashboard/campaigns/[id]/campaign-actions.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,9 @@ export const CampaignActions = () => {
200200
</AlertDialog>
201201
</>
202202
)
203+
case "CREATING":
203204
case "SENDING":
205+
case "SCHEDULED":
204206
return (
205207
<Button
206208
variant="destructive"

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"version": "0.9.0",
2+
"version": "0.9.1",
33
"name": "letterspace",
44
"private": true,
55
"scripts": {

0 commit comments

Comments
 (0)