fix: Persistence of subscription state for existing contacts

This commit is contained in:
Dries Augustyns
2025-12-18 19:43:59 +01:00
parent 23b4ec992d
commit 007a908e83
5 changed files with 589 additions and 6 deletions
@@ -0,0 +1,281 @@
import {beforeEach, describe, expect, it} from 'vitest';
import {factories, getPrismaClient} from '../../../../../test/helpers';
import {ContactService} from '../../services/ContactService.js';
/**
* Tests for Contact Import Processor - Subscription Status Preservation
* Verifies that CSV imports preserve subscription status correctly
*/
describe('Contact Import - Subscription Status Preservation', () => {
let projectId: string;
const prisma = getPrismaClient();
beforeEach(async () => {
const {project} = await factories.createUserWithProject();
projectId = project.id;
});
describe('Existing contacts', () => {
it('should NOT change subscription status when CSV has no subscribed column for subscribed contact', async () => {
// Create a subscribed contact
const contact = await factories.createContact({
projectId,
subscribed: true,
email: '[email protected]',
});
// Verify initial state
expect(contact.subscribed).toBe(true);
// Simulate import without subscribed column (undefined)
await ContactService.upsert(projectId, contact.email, {firstName: 'John'}, undefined);
// Verify subscription status unchanged
const updated = await prisma.contact.findUnique({
where: {id: contact.id},
});
expect(updated?.subscribed).toBe(true);
});
it('should NOT re-subscribe unsubscribed contact when CSV has no subscribed column', async () => {
// Create an unsubscribed contact
const contact = await factories.createContact({
projectId,
subscribed: false,
email: '[email protected]',
});
// Verify initial state
expect(contact.subscribed).toBe(false);
// Simulate import without subscribed column (undefined)
await ContactService.upsert(projectId, contact.email, {firstName: 'Jane'}, undefined);
// Verify subscription status unchanged (should NOT be re-subscribed)
const updated = await prisma.contact.findUnique({
where: {id: contact.id},
});
expect(updated?.subscribed).toBe(false);
});
it('should update subscription status when CSV explicitly has subscribed=true', async () => {
// Create an unsubscribed contact
const contact = await factories.createContact({
projectId,
subscribed: false,
email: '[email protected]',
});
// Verify initial state
expect(contact.subscribed).toBe(false);
// Simulate import with explicit subscribed=true
await ContactService.upsert(projectId, contact.email, {firstName: 'John'}, true);
// Verify subscription status changed
const updated = await prisma.contact.findUnique({
where: {id: contact.id},
});
expect(updated?.subscribed).toBe(true);
});
it('should update subscription status when CSV explicitly has subscribed=false', async () => {
// Create a subscribed contact
const contact = await factories.createContact({
projectId,
subscribed: true,
email: '[email protected]',
});
// Verify initial state
expect(contact.subscribed).toBe(true);
// Simulate import with explicit subscribed=false
await ContactService.upsert(projectId, contact.email, {firstName: 'Jane'}, false);
// Verify subscription status changed
const updated = await prisma.contact.findUnique({
where: {id: contact.id},
});
expect(updated?.subscribed).toBe(false);
});
});
describe('New contacts', () => {
it('should create new contact as subscribed when CSV has no subscribed column', async () => {
const newEmail = '[email protected]';
// Simulate import without subscribed column (undefined)
const contact = await ContactService.upsert(projectId, newEmail, {firstName: 'New'}, undefined);
// New contacts should default to subscribed=true
expect(contact.subscribed).toBe(true);
});
it('should create new contact as subscribed when CSV explicitly has subscribed=true', async () => {
const newEmail = '[email protected]';
// Simulate import with explicit subscribed=true
const contact = await ContactService.upsert(projectId, newEmail, {firstName: 'New'}, true);
expect(contact.subscribed).toBe(true);
});
it('should create new contact as unsubscribed when CSV explicitly has subscribed=false', async () => {
const newEmail = '[email protected]';
// Simulate import with explicit subscribed=false
const contact = await ContactService.upsert(projectId, newEmail, {firstName: 'New'}, false);
expect(contact.subscribed).toBe(false);
});
});
describe('CSV parsing logic', () => {
it('should parse "true" string as boolean true', () => {
const subscribedValue = 'true';
const lowerValue = subscribedValue.toLowerCase().trim();
const subscribed = lowerValue === 'true' || lowerValue === '1' || lowerValue === 'yes';
expect(subscribed).toBe(true);
});
it('should parse "1" string as boolean true', () => {
const subscribedValue = '1';
const lowerValue = subscribedValue.toLowerCase().trim();
const subscribed = lowerValue === 'true' || lowerValue === '1' || lowerValue === 'yes';
expect(subscribed).toBe(true);
});
it('should parse "yes" string as boolean true', () => {
const subscribedValue = 'yes';
const lowerValue = subscribedValue.toLowerCase().trim();
const subscribed = lowerValue === 'true' || lowerValue === '1' || lowerValue === 'yes';
expect(subscribed).toBe(true);
});
it('should parse "false" string as boolean false', () => {
const subscribedValue = 'false';
const lowerValue = subscribedValue.toLowerCase().trim();
const subscribed = lowerValue === 'true' || lowerValue === '1' || lowerValue === 'yes';
expect(subscribed).toBe(false);
});
it('should parse "0" string as boolean false', () => {
const subscribedValue = '0';
const lowerValue = subscribedValue.toLowerCase().trim();
const subscribed = lowerValue === 'true' || lowerValue === '1' || lowerValue === 'yes';
expect(subscribed).toBe(false);
});
it('should parse "no" string as boolean false', () => {
const subscribedValue = 'no';
const lowerValue = subscribedValue.toLowerCase().trim();
const subscribed = lowerValue === 'true' || lowerValue === '1' || lowerValue === 'yes';
expect(subscribed).toBe(false);
});
it('should handle empty string as undefined', () => {
const subscribedValue = '';
let subscribed: boolean | undefined;
if (subscribedValue !== undefined && subscribedValue !== '') {
const lowerValue = subscribedValue.toLowerCase().trim();
subscribed = lowerValue === 'true' || lowerValue === '1' || lowerValue === 'yes';
}
expect(subscribed).toBeUndefined();
});
it('should handle undefined as undefined', () => {
const subscribedValue = undefined;
let subscribed: boolean | undefined;
if (subscribedValue !== undefined && subscribedValue !== '') {
const lowerValue = subscribedValue.toLowerCase().trim();
subscribed = lowerValue === 'true' || lowerValue === '1' || lowerValue === 'yes';
}
expect(subscribed).toBeUndefined();
});
});
describe('Data preservation', () => {
it('should preserve existing contact data while updating subscription', async () => {
// Create contact with existing data
const contact = await factories.createContact({
projectId,
subscribed: false,
email: '[email protected]',
data: {
firstName: 'Original',
lastName: 'Name',
plan: 'pro',
},
});
// Update only subscription via import
await ContactService.upsert(projectId, contact.email, {}, true);
const updated = await prisma.contact.findUnique({
where: {id: contact.id},
});
// Subscription should be updated
expect(updated?.subscribed).toBe(true);
// Original data should be preserved
const data = updated?.data as Record<string, unknown>;
expect(data?.firstName).toBe('Original');
expect(data?.lastName).toBe('Name');
expect(data?.plan).toBe('pro');
});
it('should merge new data while preserving subscription', async () => {
// Create contact with existing data
const contact = await factories.createContact({
projectId,
subscribed: false,
email: '[email protected]',
data: {
firstName: 'John',
plan: 'pro',
},
});
// Import new data without changing subscription
await ContactService.upsert(
projectId,
contact.email,
{
lastName: 'Doe',
company: 'Acme Inc',
},
undefined,
);
const updated = await prisma.contact.findUnique({
where: {id: contact.id},
});
// Subscription should be unchanged
expect(updated?.subscribed).toBe(false);
// Data should be merged
const data = updated?.data as Record<string, unknown>;
expect(data?.firstName).toBe('John'); // Preserved
expect(data?.plan).toBe('pro'); // Preserved
expect(data?.lastName).toBe('Doe'); // New
expect(data?.company).toBe('Acme Inc'); // New
});
});
});
+4 -2
View File
@@ -128,8 +128,10 @@ export function createImportWorker() {
const existingContact = await ContactService.findByEmail(projectId, email);
const isUpdate = !!existingContact;
// Upsert contact with subscribed value from CSV if provided, otherwise default to true
await ContactService.upsert(projectId, email, data, subscribed ?? true);
// Upsert contact with subscribed value from CSV if provided
// For new contacts, ContactService.upsert defaults to true
// For existing contacts, only update if explicitly provided in CSV
await ContactService.upsert(projectId, email, data, subscribed);
result.successCount++;
if (isUpdate) {