diff --git a/apps/web/modules/ee/contacts/components/upload-contacts-button.tsx b/apps/web/modules/ee/contacts/components/upload-contacts-button.tsx index 57367ee37b..68203f976c 100644 --- a/apps/web/modules/ee/contacts/components/upload-contacts-button.tsx +++ b/apps/web/modules/ee/contacts/components/upload-contacts-button.tsx @@ -167,6 +167,17 @@ export const UploadContactsCSVButton = ({ const transformedCsvData = csvResponse.map((record) => { const newRecord: Record = {}; Object.entries(record).forEach(([key, value]) => { + // Normalize default attribute keys to their canonical form for case-insensitive matching + const defaultAttributeKeysMap: Record = { + userid: "userId", + firstname: "firstName", + lastname: "lastName", + email: "email", + language: "language", + }; + const keyLower = key.toLowerCase(); + const normalizedKey = defaultAttributeKeysMap[keyLower] || key; + // if the key is in the attribute map, we wanna replace it if (attributeMap[key]) { const attrKeyId = attributeMap[key]; @@ -178,7 +189,7 @@ export const UploadContactsCSVButton = ({ newRecord[attrKeyId] = value; } } else { - newRecord[key] = value; + newRecord[normalizedKey] = value; } }); @@ -244,6 +255,8 @@ export const UploadContactsCSVButton = ({ }, [error]); // Function to download an example CSV + // Note: The example uses canonical casing for default attributes (email, userId, firstName, lastName, language) + // The upload process is case-insensitive for these attributes (e.g., "Language" will be normalized to "language") const handleDownloadExampleCSV = () => { const exampleData = [ { email: "user1@example.com", userId: "1001", firstName: "John", lastName: "Doe" }, diff --git a/apps/web/modules/ee/contacts/lib/contacts.test.ts b/apps/web/modules/ee/contacts/lib/contacts.test.ts index b32374a844..6b9252451f 100644 --- a/apps/web/modules/ee/contacts/lib/contacts.test.ts +++ b/apps/web/modules/ee/contacts/lib/contacts.test.ts @@ -319,6 +319,54 @@ describe("createContactsFromCSV", () => { createContactsFromCSV(csvData, environmentId, "skip", { email: "email", name: "name" }) ).rejects.toThrow(genericError); }); + + test("handles case-insensitive attribute keys (language, userId, firstName, lastName, email)", async () => { + vi.mocked(prisma.contact.findMany).mockResolvedValue([]); + vi.mocked(prisma.contactAttribute.findMany).mockResolvedValue([]); + vi.mocked(prisma.contactAttributeKey.findMany) + .mockResolvedValueOnce([]) + .mockResolvedValueOnce([ + { key: "email", id: "id-email" }, + { key: "userId", id: "id-userId" }, + { key: "firstName", id: "id-firstName" }, + { key: "lastName", id: "id-lastName" }, + { key: "language", id: "id-language" }, + ] as any); + vi.mocked(prisma.contactAttributeKey.createMany).mockResolvedValue({ count: 5 }); + vi.mocked(prisma.contact.create).mockResolvedValue({ + id: "c1", + environmentId, + createdAt: new Date(), + updatedAt: new Date(), + attributes: [ + { attributeKey: { key: "email" }, value: "john@example.com" }, + { attributeKey: { key: "userId" }, value: "user123" }, + { attributeKey: { key: "firstName" }, value: "John" }, + { attributeKey: { key: "lastName" }, value: "Doe" }, + { attributeKey: { key: "language" }, value: "en" }, + ], + } as any); + // CSV data with normalized keys (already handled by client-side component) + const csvData = [ + { + email: "john@example.com", + userId: "user123", + firstName: "John", + lastName: "Doe", + language: "en", + }, + ]; + const result = await createContactsFromCSV(csvData, environmentId, "skip", { + email: "email", + userId: "userId", + firstName: "firstName", + lastName: "lastName", + language: "language", + }); + expect(Array.isArray(result)).toBe(true); + expect(result[0].id).toBe("c1"); + expect(prisma.contactAttributeKey.createMany).toHaveBeenCalled(); + }); }); describe("buildContactWhereClause", () => {