fix: Date filtering not working properly for custom contact data
This commit is contained in:
@@ -458,7 +458,7 @@ export class SegmentService {
|
|||||||
// Handle JSON field paths (e.g., "data.plan")
|
// Handle JSON field paths (e.g., "data.plan")
|
||||||
if (field.startsWith('data.')) {
|
if (field.startsWith('data.')) {
|
||||||
const jsonPath = field.substring(5); // Remove "data." prefix
|
const jsonPath = field.substring(5); // Remove "data." prefix
|
||||||
return this.buildJsonFieldCondition(jsonPath, operator, value);
|
return this.buildJsonFieldCondition(jsonPath, operator, value, unit);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle regular fields
|
// Handle regular fields
|
||||||
@@ -638,13 +638,38 @@ export class SegmentService {
|
|||||||
/**
|
/**
|
||||||
* Build condition for JSON fields (stored in contact.data)
|
* Build condition for JSON fields (stored in contact.data)
|
||||||
*/
|
*/
|
||||||
private static buildJsonFieldCondition(jsonPath: string, operator: string, value: unknown): Prisma.ContactWhereInput {
|
private static buildJsonFieldCondition(
|
||||||
|
jsonPath: string,
|
||||||
|
operator: string,
|
||||||
|
value: unknown,
|
||||||
|
unit?: 'days' | 'hours' | 'minutes',
|
||||||
|
): Prisma.ContactWhereInput {
|
||||||
const path = jsonPath.split('.');
|
const path = jsonPath.split('.');
|
||||||
|
|
||||||
switch (operator) {
|
switch (operator) {
|
||||||
case 'equals':
|
case 'equals':
|
||||||
|
// For date strings, compare only the date portion (ignore time)
|
||||||
|
if (this.isDateString(value)) {
|
||||||
|
const {startOfDay, startOfNextDay} = this.getDateRange(String(value));
|
||||||
|
return {
|
||||||
|
AND: [
|
||||||
|
{data: {path, gte: startOfDay as Prisma.InputJsonValue}},
|
||||||
|
{data: {path, lt: startOfNextDay as Prisma.InputJsonValue}},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
return {data: {path, equals: value as Prisma.InputJsonValue}};
|
return {data: {path, equals: value as Prisma.InputJsonValue}};
|
||||||
case 'notEquals':
|
case 'notEquals':
|
||||||
|
// For date strings, exclude the entire day (not just exact timestamp)
|
||||||
|
if (this.isDateString(value)) {
|
||||||
|
const {startOfDay, startOfNextDay} = this.getDateRange(String(value));
|
||||||
|
return {
|
||||||
|
OR: [
|
||||||
|
{data: {path, lt: startOfDay as Prisma.InputJsonValue}},
|
||||||
|
{data: {path, gte: startOfNextDay as Prisma.InputJsonValue}},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
return {NOT: {data: {path, equals: value as Prisma.InputJsonValue}}};
|
return {NOT: {data: {path, equals: value as Prisma.InputJsonValue}}};
|
||||||
case 'contains':
|
case 'contains':
|
||||||
return {data: {path, string_contains: String(value)}};
|
return {data: {path, string_contains: String(value)}};
|
||||||
@@ -670,6 +695,20 @@ export class SegmentService {
|
|||||||
return {
|
return {
|
||||||
OR: [{data: {path, equals: Prisma.DbNull}}, {data: {path, equals: Prisma.JsonNull}}],
|
OR: [{data: {path, equals: Prisma.DbNull}}, {data: {path, equals: Prisma.JsonNull}}],
|
||||||
};
|
};
|
||||||
|
case 'within': {
|
||||||
|
// Note: Requires JSON date fields in ISO 8601 format for proper comparison
|
||||||
|
if (!unit) {
|
||||||
|
throw new HttpException(400, 'Unit is required for "within" operator');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate the "since" date (X time units ago from now)
|
||||||
|
const now = new Date();
|
||||||
|
const milliseconds = this.getMilliseconds(value as number, unit);
|
||||||
|
const since = new Date(now.getTime() - milliseconds);
|
||||||
|
|
||||||
|
// Use ISO string for lexicographic comparison in JSON
|
||||||
|
return {data: {path, gte: since.toISOString() as Prisma.InputJsonValue}};
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
throw new HttpException(400, `Unsupported operator for JSON field: ${operator}`);
|
throw new HttpException(400, `Unsupported operator for JSON field: ${operator}`);
|
||||||
}
|
}
|
||||||
@@ -721,6 +760,28 @@ export class SegmentService {
|
|||||||
unit?: 'days' | 'hours' | 'minutes',
|
unit?: 'days' | 'hours' | 'minutes',
|
||||||
): Prisma.ContactWhereInput {
|
): Prisma.ContactWhereInput {
|
||||||
switch (operator) {
|
switch (operator) {
|
||||||
|
case 'equals': {
|
||||||
|
// For date fields, compare only the date portion (ignore time)
|
||||||
|
if (this.isDateString(value)) {
|
||||||
|
const {startOfDay, startOfNextDay} = this.getDateRange(String(value));
|
||||||
|
return {
|
||||||
|
AND: [{[field]: {gte: new Date(startOfDay)}}, {[field]: {lt: new Date(startOfNextDay)}}],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// Exact timestamp match if not a date string
|
||||||
|
return {[field]: new Date(value as string | number | Date)};
|
||||||
|
}
|
||||||
|
case 'notEquals': {
|
||||||
|
// For date fields, exclude the entire day (not just exact timestamp)
|
||||||
|
if (this.isDateString(value)) {
|
||||||
|
const {startOfDay, startOfNextDay} = this.getDateRange(String(value));
|
||||||
|
return {
|
||||||
|
OR: [{[field]: {lt: new Date(startOfDay)}}, {[field]: {gte: new Date(startOfNextDay)}}],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// Exclude exact timestamp if not a date string
|
||||||
|
return {NOT: {[field]: new Date(value as string | number | Date)}};
|
||||||
|
}
|
||||||
case 'greaterThan':
|
case 'greaterThan':
|
||||||
return {[field]: {gt: new Date(value as string | number | Date)}};
|
return {[field]: {gt: new Date(value as string | number | Date)}};
|
||||||
case 'lessThan':
|
case 'lessThan':
|
||||||
@@ -762,6 +823,41 @@ export class SegmentService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a value is a date string in YYYY-MM-DD format
|
||||||
|
*/
|
||||||
|
private static isDateString(value: unknown): boolean {
|
||||||
|
if (typeof value !== 'string') return false;
|
||||||
|
// Match YYYY-MM-DD format (with optional time component)
|
||||||
|
const dateRegex = /^\d{4}-\d{2}-\d{2}(T|$)/;
|
||||||
|
if (!dateRegex.test(value)) return false;
|
||||||
|
// Verify it's a valid date
|
||||||
|
const date = new Date(value);
|
||||||
|
return !isNaN(date.getTime());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get date range for a date string (start of day to start of next day in UTC)
|
||||||
|
* @param value - Date string in YYYY-MM-DD format
|
||||||
|
* @returns Object with startOfDay and startOfNextDay as ISO strings
|
||||||
|
*/
|
||||||
|
private static getDateRange(value: string): {startOfDay: string; startOfNextDay: string} {
|
||||||
|
// Extract just the date part (YYYY-MM-DD)
|
||||||
|
const dateStr = value.split('T')[0];
|
||||||
|
const startOfDay = `${dateStr}T00:00:00.000Z`;
|
||||||
|
|
||||||
|
if (!dateStr) {
|
||||||
|
throw new HttpException(400, `Invalid date string: ${value}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate start of next day
|
||||||
|
const nextDay = new Date(dateStr);
|
||||||
|
nextDay.setUTCDate(nextDay.getUTCDate() + 1);
|
||||||
|
const startOfNextDay = nextDay.toISOString().split('T')[0] + 'T00:00:00.000Z';
|
||||||
|
|
||||||
|
return {startOfDay, startOfNextDay};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Build condition for event-based filters
|
* Build condition for event-based filters
|
||||||
* Uses Prisma relations to efficiently query contacts who triggered specific events
|
* Uses Prisma relations to efficiently query contacts who triggered specific events
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import {beforeEach, describe, expect, it} from 'vitest';
|
import {beforeEach, describe, expect, it} from 'vitest';
|
||||||
import {SegmentService} from '../SegmentService';
|
import {SegmentService} from '../SegmentService';
|
||||||
import {factories} from '../../../../../test/helpers';
|
import {factories, getPrismaClient} from '../../../../../test/helpers';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Comprehensive Operator Tests for Segment Filtering
|
* Comprehensive Operator Tests for Segment Filtering
|
||||||
@@ -760,6 +760,226 @@ describe('SegmentService - Comprehensive Operator Tests', () => {
|
|||||||
expect(ids).toContain(justNow.id);
|
expect(ids).toContain(justNow.id);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('within operator for JSON date fields', () => {
|
||||||
|
it('should match contacts with JSON date field within specified days', async () => {
|
||||||
|
// Create contact with recent date in JSON field
|
||||||
|
const recentDate = new Date();
|
||||||
|
const match = await factories.createContact({
|
||||||
|
projectId,
|
||||||
|
data: {
|
||||||
|
subscriptionEndDate: recentDate.toISOString(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create contact with old date
|
||||||
|
const oldDate = new Date();
|
||||||
|
oldDate.setDate(oldDate.getDate() - 10);
|
||||||
|
await factories.createContact({
|
||||||
|
projectId,
|
||||||
|
data: {
|
||||||
|
subscriptionEndDate: oldDate.toISOString(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const segment = await factories.createSegment(projectId, {
|
||||||
|
filters: [
|
||||||
|
{
|
||||||
|
field: 'data.subscriptionEndDate',
|
||||||
|
operator: 'within',
|
||||||
|
value: 3,
|
||||||
|
unit: 'days',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await SegmentService.getContacts(projectId, segment.id);
|
||||||
|
expect(result.contacts).toHaveLength(1);
|
||||||
|
expect(result.contacts[0].id).toBe(match.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should match contacts with JSON date field within specified hours', async () => {
|
||||||
|
const recentDate = new Date();
|
||||||
|
const match = await factories.createContact({
|
||||||
|
projectId,
|
||||||
|
data: {
|
||||||
|
lastLoginAt: recentDate.toISOString(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const segment = await factories.createSegment(projectId, {
|
||||||
|
filters: [
|
||||||
|
{
|
||||||
|
field: 'data.lastLoginAt',
|
||||||
|
operator: 'within',
|
||||||
|
value: 24,
|
||||||
|
unit: 'hours',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await SegmentService.getContacts(projectId, segment.id);
|
||||||
|
expect(result.contacts).toHaveLength(1);
|
||||||
|
expect(result.contacts[0].id).toBe(match.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should match contacts with JSON date field within specified minutes', async () => {
|
||||||
|
const justNow = new Date();
|
||||||
|
const match = await factories.createContact({
|
||||||
|
projectId,
|
||||||
|
data: {
|
||||||
|
verifiedAt: justNow.toISOString(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const segment = await factories.createSegment(projectId, {
|
||||||
|
filters: [
|
||||||
|
{
|
||||||
|
field: 'data.verifiedAt',
|
||||||
|
operator: 'within',
|
||||||
|
value: 60,
|
||||||
|
unit: 'minutes',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await SegmentService.getContacts(projectId, segment.id);
|
||||||
|
expect(result.contacts).toHaveLength(1);
|
||||||
|
expect(result.contacts[0].id).toBe(match.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should NOT match contacts with JSON date field outside the time range', async () => {
|
||||||
|
const oldDate = new Date();
|
||||||
|
oldDate.setDate(oldDate.getDate() - 10);
|
||||||
|
await factories.createContact({
|
||||||
|
projectId,
|
||||||
|
data: {
|
||||||
|
subscriptionEndDate: oldDate.toISOString(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const segment = await factories.createSegment(projectId, {
|
||||||
|
filters: [
|
||||||
|
{
|
||||||
|
field: 'data.subscriptionEndDate',
|
||||||
|
operator: 'within',
|
||||||
|
value: 3,
|
||||||
|
unit: 'days',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await SegmentService.getContacts(projectId, segment.id);
|
||||||
|
expect(result.contacts).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should NOT match contacts where JSON date field does not exist', async () => {
|
||||||
|
await factories.createContact({
|
||||||
|
projectId,
|
||||||
|
data: {
|
||||||
|
otherField: 'value',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const segment = await factories.createSegment(projectId, {
|
||||||
|
filters: [
|
||||||
|
{
|
||||||
|
field: 'data.subscriptionEndDate',
|
||||||
|
operator: 'within',
|
||||||
|
value: 3,
|
||||||
|
unit: 'days',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await SegmentService.getContacts(projectId, segment.id);
|
||||||
|
expect(result.contacts).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle null values in JSON date fields gracefully', async () => {
|
||||||
|
await factories.createContact({
|
||||||
|
projectId,
|
||||||
|
data: {
|
||||||
|
subscriptionEndDate: null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const segment = await factories.createSegment(projectId, {
|
||||||
|
filters: [
|
||||||
|
{
|
||||||
|
field: 'data.subscriptionEndDate',
|
||||||
|
operator: 'within',
|
||||||
|
value: 3,
|
||||||
|
unit: 'days',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await SegmentService.getContacts(projectId, segment.id);
|
||||||
|
expect(result.contacts).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should work correctly with combined filters (AND logic)', async () => {
|
||||||
|
const recentDate = new Date();
|
||||||
|
const match = await factories.createContact({
|
||||||
|
projectId,
|
||||||
|
subscribed: true,
|
||||||
|
data: {
|
||||||
|
plan: 'premium',
|
||||||
|
subscriptionEndDate: recentDate.toISOString(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await factories.createContact({
|
||||||
|
projectId,
|
||||||
|
subscribed: false,
|
||||||
|
data: {
|
||||||
|
plan: 'premium',
|
||||||
|
subscriptionEndDate: recentDate.toISOString(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const segment = await factories.createSegment(projectId, {
|
||||||
|
filters: [
|
||||||
|
{field: 'subscribed', operator: 'equals', value: true},
|
||||||
|
{field: 'data.plan', operator: 'equals', value: 'premium'},
|
||||||
|
{
|
||||||
|
field: 'data.subscriptionEndDate',
|
||||||
|
operator: 'within',
|
||||||
|
value: 3,
|
||||||
|
unit: 'days',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await SegmentService.getContacts(projectId, segment.id);
|
||||||
|
expect(result.contacts).toHaveLength(1);
|
||||||
|
expect(result.contacts[0].id).toBe(match.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should require unit parameter for within operator', async () => {
|
||||||
|
await expect(
|
||||||
|
SegmentService.create(projectId, {
|
||||||
|
name: 'Invalid Segment',
|
||||||
|
condition: {
|
||||||
|
logic: 'AND',
|
||||||
|
groups: [
|
||||||
|
{
|
||||||
|
filters: [
|
||||||
|
{
|
||||||
|
field: 'data.subscriptionEndDate',
|
||||||
|
operator: 'within',
|
||||||
|
value: 3,
|
||||||
|
// unit intentionally omitted
|
||||||
|
} as any,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
).rejects.toThrow(/operator requires a unit/i);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// ========================================
|
// ========================================
|
||||||
@@ -812,6 +1032,229 @@ describe('SegmentService - Comprehensive Operator Tests', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// DATE-ONLY COMPARISON (equals/notEquals)
|
||||||
|
// ========================================
|
||||||
|
describe('Date-Only Comparison for equals/notEquals', () => {
|
||||||
|
describe('JSON date fields', () => {
|
||||||
|
it('should match contacts with date on the same day (equals) - ignoring time', async () => {
|
||||||
|
const targetDate = '2025-01-15';
|
||||||
|
const match1 = await factories.createContact({
|
||||||
|
projectId,
|
||||||
|
data: {
|
||||||
|
subscriptionEndDate: '2025-01-15T08:30:00.000Z', // Morning
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const match2 = await factories.createContact({
|
||||||
|
projectId,
|
||||||
|
data: {
|
||||||
|
subscriptionEndDate: '2025-01-15T18:45:30.000Z', // Evening
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const noMatch = await factories.createContact({
|
||||||
|
projectId,
|
||||||
|
data: {
|
||||||
|
subscriptionEndDate: '2025-01-16T00:00:00.000Z', // Next day
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const segment = await factories.createSegment(projectId, {
|
||||||
|
filters: [
|
||||||
|
{
|
||||||
|
field: 'data.subscriptionEndDate',
|
||||||
|
operator: 'equals',
|
||||||
|
value: targetDate,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await SegmentService.getContacts(projectId, segment.id);
|
||||||
|
const ids = result.contacts.map(c => c.id);
|
||||||
|
expect(ids).toContain(match1.id);
|
||||||
|
expect(ids).toContain(match2.id);
|
||||||
|
expect(ids).not.toContain(noMatch.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should exclude contacts with date on the specified day (notEquals)', async () => {
|
||||||
|
const targetDate = '2025-01-15';
|
||||||
|
const match1 = await factories.createContact({
|
||||||
|
projectId,
|
||||||
|
data: {
|
||||||
|
subscriptionEndDate: '2025-01-14T23:59:59.999Z', // Day before
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const match2 = await factories.createContact({
|
||||||
|
projectId,
|
||||||
|
data: {
|
||||||
|
subscriptionEndDate: '2025-01-16T00:00:00.000Z', // Day after
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const noMatch = await factories.createContact({
|
||||||
|
projectId,
|
||||||
|
data: {
|
||||||
|
subscriptionEndDate: '2025-01-15T12:00:00.000Z', // Same day
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const segment = await factories.createSegment(projectId, {
|
||||||
|
filters: [
|
||||||
|
{
|
||||||
|
field: 'data.subscriptionEndDate',
|
||||||
|
operator: 'notEquals',
|
||||||
|
value: targetDate,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await SegmentService.getContacts(projectId, segment.id);
|
||||||
|
const ids = result.contacts.map(c => c.id);
|
||||||
|
expect(ids).toContain(match1.id);
|
||||||
|
expect(ids).toContain(match2.id);
|
||||||
|
expect(ids).not.toContain(noMatch.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle date string with time component (extracts date only)', async () => {
|
||||||
|
const targetDate = '2025-01-15T00:00:00.000Z'; // Has time component
|
||||||
|
const match = await factories.createContact({
|
||||||
|
projectId,
|
||||||
|
data: {
|
||||||
|
subscriptionEndDate: '2025-01-15T23:59:59.999Z', // End of day
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const segment = await factories.createSegment(projectId, {
|
||||||
|
filters: [
|
||||||
|
{
|
||||||
|
field: 'data.subscriptionEndDate',
|
||||||
|
operator: 'equals',
|
||||||
|
value: targetDate,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await SegmentService.getContacts(projectId, segment.id);
|
||||||
|
const ids = result.contacts.map(c => c.id);
|
||||||
|
expect(ids).toContain(match.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should still do exact match for non-date strings', async () => {
|
||||||
|
const match = await factories.createContact({
|
||||||
|
projectId,
|
||||||
|
data: {
|
||||||
|
plan: 'premium',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await factories.createContact({
|
||||||
|
projectId,
|
||||||
|
data: {
|
||||||
|
plan: 'basic',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const segment = await factories.createSegment(projectId, {
|
||||||
|
filters: [
|
||||||
|
{
|
||||||
|
field: 'data.plan',
|
||||||
|
operator: 'equals',
|
||||||
|
value: 'premium',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await SegmentService.getContacts(projectId, segment.id);
|
||||||
|
expect(result.contacts).toHaveLength(1);
|
||||||
|
expect(result.contacts[0].id).toBe(match.id);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Native date fields (createdAt)', () => {
|
||||||
|
it('should match contacts created on the same day (equals) - ignoring time', async () => {
|
||||||
|
// Create contacts on specific dates
|
||||||
|
const jan15Morning = new Date('2025-01-15T08:30:00.000Z');
|
||||||
|
const jan15Evening = new Date('2025-01-15T18:45:30.000Z');
|
||||||
|
const jan16 = new Date('2025-01-16T00:00:00.000Z');
|
||||||
|
|
||||||
|
const prisma = getPrismaClient();
|
||||||
|
|
||||||
|
const match1 = await factories.createContact({projectId});
|
||||||
|
await prisma.contact.update({
|
||||||
|
where: {id: match1.id},
|
||||||
|
data: {createdAt: jan15Morning},
|
||||||
|
});
|
||||||
|
|
||||||
|
const match2 = await factories.createContact({projectId});
|
||||||
|
await prisma.contact.update({
|
||||||
|
where: {id: match2.id},
|
||||||
|
data: {createdAt: jan15Evening},
|
||||||
|
});
|
||||||
|
|
||||||
|
const noMatch = await factories.createContact({projectId});
|
||||||
|
await prisma.contact.update({
|
||||||
|
where: {id: noMatch.id},
|
||||||
|
data: {createdAt: jan16},
|
||||||
|
});
|
||||||
|
|
||||||
|
const segment = await factories.createSegment(projectId, {
|
||||||
|
filters: [
|
||||||
|
{
|
||||||
|
field: 'createdAt',
|
||||||
|
operator: 'equals',
|
||||||
|
value: '2025-01-15',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await SegmentService.getContacts(projectId, segment.id);
|
||||||
|
const ids = result.contacts.map(c => c.id);
|
||||||
|
expect(ids).toContain(match1.id);
|
||||||
|
expect(ids).toContain(match2.id);
|
||||||
|
expect(ids).not.toContain(noMatch.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should exclude contacts created on the specified day (notEquals)', async () => {
|
||||||
|
const jan14 = new Date('2025-01-14T23:59:59.999Z');
|
||||||
|
const jan15 = new Date('2025-01-15T12:00:00.000Z');
|
||||||
|
const jan16 = new Date('2025-01-16T00:00:00.000Z');
|
||||||
|
|
||||||
|
const prisma = getPrismaClient();
|
||||||
|
|
||||||
|
const match1 = await factories.createContact({projectId});
|
||||||
|
await prisma.contact.update({
|
||||||
|
where: {id: match1.id},
|
||||||
|
data: {createdAt: jan14},
|
||||||
|
});
|
||||||
|
|
||||||
|
const match2 = await factories.createContact({projectId});
|
||||||
|
await prisma.contact.update({
|
||||||
|
where: {id: match2.id},
|
||||||
|
data: {createdAt: jan16},
|
||||||
|
});
|
||||||
|
|
||||||
|
const noMatch = await factories.createContact({projectId});
|
||||||
|
await prisma.contact.update({
|
||||||
|
where: {id: noMatch.id},
|
||||||
|
data: {createdAt: jan15},
|
||||||
|
});
|
||||||
|
|
||||||
|
const segment = await factories.createSegment(projectId, {
|
||||||
|
filters: [
|
||||||
|
{
|
||||||
|
field: 'createdAt',
|
||||||
|
operator: 'notEquals',
|
||||||
|
value: '2025-01-15',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await SegmentService.getContacts(projectId, segment.id);
|
||||||
|
const ids = result.contacts.map(c => c.id);
|
||||||
|
expect(ids).toContain(match1.id);
|
||||||
|
expect(ids).toContain(match2.id);
|
||||||
|
expect(ids).not.toContain(noMatch.id);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// ========================================
|
// ========================================
|
||||||
// COMBINED OPERATORS (AND logic)
|
// COMBINED OPERATORS (AND logic)
|
||||||
// ========================================
|
// ========================================
|
||||||
|
|||||||
Reference in New Issue
Block a user