172 lines
6.0 KiB
JavaScript
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');
|
|
});
|
|
});
|