Added events for subscribe and unsubscribe
This commit is contained in:
@@ -128,7 +128,7 @@ export class ContactService {
|
|||||||
data: {email?: string; data?: Prisma.JsonValue; subscribed?: boolean},
|
data: {email?: string; data?: Prisma.JsonValue; subscribed?: boolean},
|
||||||
): Promise<Contact> {
|
): Promise<Contact> {
|
||||||
// First verify contact exists and belongs to project
|
// First verify contact exists and belongs to project
|
||||||
await this.get(projectId, contactId);
|
const existing = await this.get(projectId, contactId);
|
||||||
|
|
||||||
const updateData: Prisma.ContactUpdateInput = {};
|
const updateData: Prisma.ContactUpdateInput = {};
|
||||||
|
|
||||||
@@ -142,11 +142,28 @@ export class ContactService {
|
|||||||
updateData.subscribed = data.subscribed;
|
updateData.subscribed = data.subscribed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Track subscription status change
|
||||||
|
const isSubscriptionChanging =
|
||||||
|
data.subscribed !== undefined && existing.subscribed !== data.subscribed;
|
||||||
|
const wasSubscribed = existing.subscribed;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return await prisma.contact.update({
|
const updated = await prisma.contact.update({
|
||||||
where: {id: contactId},
|
where: {id: contactId},
|
||||||
data: updateData,
|
data: updateData,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Track subscription event if status changed
|
||||||
|
if (isSubscriptionChanging) {
|
||||||
|
const {EventService} = await import('./EventService.js');
|
||||||
|
if (data.subscribed && !wasSubscribed) {
|
||||||
|
await EventService.trackEvent(projectId, 'contact.subscribed', contactId);
|
||||||
|
} else if (!data.subscribed && wasSubscribed) {
|
||||||
|
await EventService.trackEvent(projectId, 'contact.unsubscribed', contactId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return updated;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Check if this is a unique constraint violation (P2002)
|
// Check if this is a unique constraint violation (P2002)
|
||||||
if (error instanceof Error && 'code' in error && error.code === 'P2002') {
|
if (error instanceof Error && 'code' in error && error.code === 'P2002') {
|
||||||
@@ -231,13 +248,30 @@ export class ContactService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (existing) {
|
if (existing) {
|
||||||
return prisma.contact.update({
|
// Track subscription status change
|
||||||
|
const isSubscriptionChanging =
|
||||||
|
subscribed !== undefined && existing.subscribed !== subscribed;
|
||||||
|
const wasSubscribed = existing.subscribed;
|
||||||
|
|
||||||
|
const updated = await prisma.contact.update({
|
||||||
where: {id: existing.id},
|
where: {id: existing.id},
|
||||||
data: {
|
data: {
|
||||||
data: Object.keys(mergedData).length > 0 ? (mergedData as Prisma.InputJsonValue) : Prisma.JsonNull,
|
data: Object.keys(mergedData).length > 0 ? (mergedData as Prisma.InputJsonValue) : Prisma.JsonNull,
|
||||||
...(subscribed !== undefined ? {subscribed} : {}),
|
...(subscribed !== undefined ? {subscribed} : {}),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Track subscription event if status changed
|
||||||
|
if (isSubscriptionChanging) {
|
||||||
|
const {EventService} = await import('./EventService.js');
|
||||||
|
if (subscribed && !wasSubscribed) {
|
||||||
|
await EventService.trackEvent(projectId, 'contact.subscribed', updated.id);
|
||||||
|
} else if (!subscribed && wasSubscribed) {
|
||||||
|
await EventService.trackEvent(projectId, 'contact.unsubscribed', updated.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return updated;
|
||||||
} else {
|
} else {
|
||||||
return prisma.contact.create({
|
return prisma.contact.create({
|
||||||
data: {
|
data: {
|
||||||
@@ -311,20 +345,32 @@ export class ContactService {
|
|||||||
* PUBLIC: Subscribe a contact
|
* PUBLIC: Subscribe a contact
|
||||||
*/
|
*/
|
||||||
public static async subscribe(contactId: string): Promise<Contact> {
|
public static async subscribe(contactId: string): Promise<Contact> {
|
||||||
return prisma.contact.update({
|
const contact = await prisma.contact.update({
|
||||||
where: {id: contactId},
|
where: {id: contactId},
|
||||||
data: {subscribed: true},
|
data: {subscribed: true},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Track subscription event
|
||||||
|
const {EventService} = await import('./EventService.js');
|
||||||
|
await EventService.trackEvent(contact.projectId, 'contact.subscribed', contactId);
|
||||||
|
|
||||||
|
return contact;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* PUBLIC: Unsubscribe a contact
|
* PUBLIC: Unsubscribe a contact
|
||||||
*/
|
*/
|
||||||
public static async unsubscribe(contactId: string): Promise<Contact> {
|
public static async unsubscribe(contactId: string): Promise<Contact> {
|
||||||
return prisma.contact.update({
|
const contact = await prisma.contact.update({
|
||||||
where: {id: contactId},
|
where: {id: contactId},
|
||||||
data: {subscribed: false},
|
data: {subscribed: false},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Track unsubscription event
|
||||||
|
const {EventService} = await import('./EventService.js');
|
||||||
|
await EventService.trackEvent(contact.projectId, 'contact.unsubscribed', contactId);
|
||||||
|
|
||||||
|
return contact;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -459,17 +459,34 @@ export class EmailService {
|
|||||||
case 'bounced':
|
case 'bounced':
|
||||||
updateData.status = EmailStatus.BOUNCED;
|
updateData.status = EmailStatus.BOUNCED;
|
||||||
updateData.bouncedAt = now;
|
updateData.bouncedAt = now;
|
||||||
|
// Unsubscribe contact on bounce and track event
|
||||||
|
if (email.contactId) {
|
||||||
|
await prisma.contact.update({
|
||||||
|
where: {id: email.contactId},
|
||||||
|
data: {subscribed: false},
|
||||||
|
});
|
||||||
|
// Track unsubscription event
|
||||||
|
const {EventService} = await import('./EventService.js');
|
||||||
|
await EventService.trackEvent(email.projectId, 'contact.unsubscribed', email.contactId, email.id, {
|
||||||
|
reason: 'bounce',
|
||||||
|
});
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'complained':
|
case 'complained':
|
||||||
updateData.status = EmailStatus.COMPLAINED;
|
updateData.status = EmailStatus.COMPLAINED;
|
||||||
updateData.complainedAt = now;
|
updateData.complainedAt = now;
|
||||||
// Unsubscribe contact
|
// Unsubscribe contact and track event
|
||||||
if (email.contactId) {
|
if (email.contactId) {
|
||||||
await prisma.contact.update({
|
await prisma.contact.update({
|
||||||
where: {id: email.contactId},
|
where: {id: email.contactId},
|
||||||
data: {subscribed: false},
|
data: {subscribed: false},
|
||||||
});
|
});
|
||||||
|
// Track unsubscription event
|
||||||
|
const {EventService} = await import('./EventService.js');
|
||||||
|
await EventService.trackEvent(email.projectId, 'contact.unsubscribed', email.contactId, email.id, {
|
||||||
|
reason: 'complaint',
|
||||||
|
});
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -401,10 +401,14 @@ describe('EmailService', () => {
|
|||||||
expect(clicked?.clicks).toBe(1);
|
expect(clicked?.clicks).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should transition to BOUNCED on bounce webhook', async () => {
|
it('should transition to BOUNCED on bounce webhook and unsubscribe contact', async () => {
|
||||||
|
const contact = await factories.createContact({
|
||||||
|
projectId,
|
||||||
|
subscribed: true,
|
||||||
|
});
|
||||||
const email = await factories.createEmail({
|
const email = await factories.createEmail({
|
||||||
projectId,
|
projectId,
|
||||||
contactId,
|
contactId: contact.id,
|
||||||
status: EmailStatus.SENT,
|
status: EmailStatus.SENT,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -416,6 +420,23 @@ describe('EmailService', () => {
|
|||||||
|
|
||||||
expect(bounced?.status).toBe(EmailStatus.BOUNCED);
|
expect(bounced?.status).toBe(EmailStatus.BOUNCED);
|
||||||
expect(bounced?.bouncedAt).not.toBeNull();
|
expect(bounced?.bouncedAt).not.toBeNull();
|
||||||
|
|
||||||
|
// Verify contact was unsubscribed
|
||||||
|
const unsubscribedContact = await prisma.contact.findUnique({
|
||||||
|
where: {id: contact.id},
|
||||||
|
});
|
||||||
|
expect(unsubscribedContact?.subscribed).toBe(false);
|
||||||
|
|
||||||
|
// Verify unsubscription event was tracked
|
||||||
|
const event = await prisma.event.findFirst({
|
||||||
|
where: {
|
||||||
|
projectId,
|
||||||
|
contactId: contact.id,
|
||||||
|
name: 'contact.unsubscribed',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(event).not.toBeNull();
|
||||||
|
expect(event?.data).toMatchObject({reason: 'bounce'});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user