PPPAIMT-Crest/backend/test/doctorImport.test.js

172 lines
6.0 KiB
JavaScript

import { mkdtemp, rm, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import path from 'node:path';
import { Readable } from 'node:stream';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { db } from '../src/db/knex.js';
import {
formatSummary,
importDoctors,
parseDoctorCsv,
validateDoctors,
} from '../src/services/doctorImport.js';
import { truncateTables } from './truncate.js';
const HEADER =
'Sr NO,TASKFORCE,ZBM Name,RBM Name,ABM Name,HQ,TM ID,TM Name,HCP NAME,LEGACY_CODE,PCODE,SPECIALITY,CLASS,VENDORCODE,VENDORNAME,Email ID,Contact No';
/** One CSV data row in the field team's format. */
function line(srNo, name, email, contact = '9769283034') {
return `${srNo},Respiratory 2,Shaikh Mohseen Ahmed,Satish Dhonde,Adityakumar Tiwari,mumbai western,014262,Salim Khan,${name},CIP833167,011667437,Chest Physician,A+,100064617,${name},${email},${contact}`;
}
/** Parse and validate CSV text with the header row added. */
async function check(lines, header = HEADER) {
return validateDoctors(await parseDoctorCsv(Readable.from([[header, ...lines].join('\n')])));
}
describe('doctor CSV validation', () => {
it('maps every column, ignores Sr NO and lowercases the email', async () => {
const result = await check([line(1, 'Sandeep S Tilve', ' Tilve@Gmail.com ')]);
expect(result).toMatchObject({ missing: [], unexpected: [], invalid: [], duplicates: [] });
expect(result.records[0]).toEqual({
taskforce: 'Respiratory 2',
zbm_name: 'Shaikh Mohseen Ahmed',
rbm_name: 'Satish Dhonde',
abm_name: 'Adityakumar Tiwari',
hq: 'mumbai western',
tm_id: '014262',
tm_name: 'Salim Khan',
hcp_name: 'Sandeep S Tilve',
legacy_code: 'CIP833167',
pcode: '011667437',
speciality: 'Chest Physician',
class: 'A+',
vendor_code: '100064617',
vendor_name: 'Sandeep S Tilve',
email: 'tilve@gmail.com',
contact_no: '9769283034',
});
});
it('reports missing and unexpected headers', async () => {
const result = await check(['1,A,a@x.com,y'], 'Sr NO,HCP NAME,Email,Extra');
expect(result.missing).toContain('Email ID');
expect(result.unexpected).toEqual(['Email', 'Extra']);
});
it('rejects missing and badly formatted emails and short rows', async () => {
const result = await check([
line(1, 'Dr A', 'a@x.com'),
line(2, 'Dr B', 'b@x'),
line(3, 'Dr C', ''),
'4,short',
]);
expect(result.records.map((record) => record.email)).toEqual(['a@x.com']);
expect(result.invalid).toEqual([
{ row: 2, name: 'Dr B', email: 'b@x', reason: '"b@x" is not a valid email' },
{ row: 3, name: 'Dr C', email: '', reason: 'email is missing' },
{ row: 4, name: '', email: '', reason: 'wrong number of columns' },
]);
});
it('groups duplicate emails regardless of case', async () => {
const result = await check([
line(1, 'Dr A', 'a@x.com'),
line(2, 'Dr B', 'b@x.com'),
line(3, 'Dr A2', 'A@X.com'),
]);
expect(result.duplicates).toEqual([
{
email: 'a@x.com',
rows: [
{ row: 1, name: 'Dr A' },
{ row: 3, name: 'Dr A2' },
],
},
]);
});
it('only accepts a plain .csv file name', async () => {
await expect(importDoctors(undefined)).rejects.toThrow(/Pass the CSV file name/);
await expect(importDoctors('../secrets.csv')).rejects.toThrow(/not a path/);
await expect(importDoctors('doctors.txt')).rejects.toThrow(/not a path/);
await expect(importDoctors('missing-file.csv')).rejects.toThrow(/File not found/);
});
});
describe('doctor CSV import', () => {
let directory;
beforeEach(async () => {
directory = await mkdtemp(path.join(tmpdir(), 'doctor-import-'));
});
afterEach(async () => {
await rm(directory, { recursive: true, force: true });
await truncateTables();
});
/** Write a CSV into the temp import folder and return its name. */
async function write(name, lines) {
await writeFile(path.join(directory, name), [HEADER, ...lines].join('\n'));
return name;
}
/** Rows currently in pppaimt_doctors. */
async function doctorCount() {
return Number((await db('pppaimt_doctors').count({ total: '*' }).first())?.total);
}
it('inserts new doctors, then updates them by email on re-import', async () => {
const first = await importDoctors(
await write('first.csv', [line(1, 'Dr A', 'a@x.com'), line(2, 'Dr B', 'b@x.com')]),
{ directory },
);
expect(first).toMatchObject({ stopped: false, rowsRead: 2, inserted: 2, updated: 0, rejected: 0 });
expect(await doctorCount()).toBe(2);
const second = await importDoctors(
await write('second.csv', [
line(1, 'Dr A', 'A@X.com', '9000000000'),
line(3, 'Dr C', 'c@x.com'),
]),
{ directory },
);
expect(second).toMatchObject({ stopped: false, inserted: 1, updated: 1, rejected: 0 });
expect(await db('pppaimt_doctors').where({ email: 'a@x.com' }).first()).toMatchObject({
contact_no: '9000000000',
source_file: 'second.csv',
});
expect(await doctorCount()).toBe(3);
const audits = await db('audit_log').where({ action: 'DOCTORS_IMPORTED' }).count({ total: '*' }).first();
expect(Number(audits?.total)).toBe(2);
});
it('writes nothing when the file has duplicate or invalid emails', async () => {
const summary = await importDoctors(
await write('bad.csv', [
line(1, 'Dr A', 'a@x.com'),
line(2, 'Dr A again', 'a@x.com'),
line(3, 'Dr B', 'b@x'),
line(4, 'Dr C', 'c@x.com'),
]),
{ directory },
);
expect(summary).toMatchObject({ stopped: true, inserted: 0, updated: 0, rejected: 3 });
expect(await doctorCount()).toBe(0);
const report = formatSummary(summary);
expect(report).toContain('Import stopped: bad.csv. Nothing was written.');
expect(report).toContain('a@x.com row 1 (Dr A), row 2 (Dr A again)');
expect(report).toContain('row 3 (Dr B): "b@x" is not a valid email');
});
});