"use strict";
Object.defineProperty(exports, "__esModule", {
    value: true
});
Object.defineProperty(exports, "Encounter", {
    enumerable: true,
    get: function() {
        return Encounter;
    }
});
const _sequelize = require("sequelize");
const _lodash = require("lodash");
const _constants = require("@tamanu/constants");
const _errors = require("@tamanu/errors");
const _dischargeOutpatientEncounters = require("@tamanu/shared/utils/dischargeOutpatientEncounters");
const _config = /*#__PURE__*/ _interop_require_default(require("config"));
const _dateFormatters = require("@tamanu/utils/dateFormatters");
const _dateTime = require("@tamanu/utils/dateTime");
const _timeZoneCheck = require("@tamanu/shared/utils/timeZoneCheck");
const _Model = require("./Model");
const _model = require("../types/model");
const _onCreateEncounterMarkPatientForSync = require("../utils/onCreateEncounterMarkPatientForSync");
const _recordModelChanges = require("../utils/recordModelChanges");
const _buildEncounterLinkedLookupFilter = require("../sync/buildEncounterLinkedLookupFilter");
function _interop_require_default(obj) {
    return obj && obj.__esModule ? obj : {
        default: obj
    };
}
const encounterTypes = new Set(_constants.ENCOUNTER_TYPE_VALUES);
function isEncounterType(value) {
    return typeof value === 'string' && encounterTypes.has(value);
}
let Encounter = class Encounter extends _Model.Model {
    static initModel({ primaryKey, hackToSkipEncounterValidation, ...options }, models) {
        let validate = {};
        if (!hackToSkipEncounterValidation) {
            validate = {
                mustHaveValidEncounterType () {
                    if (!this.deletedAt && !isEncounterType(this.encounterType)) {
                        throw new _errors.InvalidOperationError('An encounter must have a valid encounter type.');
                    }
                },
                mustHavePatient () {
                    if (!this.deletedAt && !this.patientId) {
                        throw new _errors.InvalidOperationError('An encounter must have a patient.');
                    }
                },
                mustHaveDepartment () {
                    if (!this.deletedAt && !this.departmentId) {
                        throw new _errors.InvalidOperationError('An encounter must have a department.');
                    }
                },
                mustHaveLocation () {
                    if (!this.deletedAt && !this.locationId) {
                        throw new _errors.InvalidOperationError('An encounter must have a location.');
                    }
                },
                mustHaveExaminer () {
                    if (!this.deletedAt && !this.examinerId) {
                        throw new _errors.InvalidOperationError('An encounter must have an examiner.');
                    }
                }
            };
        }
        super.init({
            id: primaryKey,
            encounterType: _sequelize.DataTypes.STRING(31),
            startDate: (0, _model.dateTimeType)('startDate', {
                allowNull: false
            }),
            endDate: (0, _model.dateTimeType)('endDate'),
            estimatedEndDate: (0, _model.dateType)('estimatedEndDate'),
            reasonForEncounter: _sequelize.DataTypes.TEXT,
            deviceId: _sequelize.DataTypes.TEXT,
            plannedLocationStartTime: (0, _model.dateTimeType)('plannedLocationStartTime'),
            dischargeDraft: {
                type: _sequelize.DataTypes.JSONB,
                allowNull: true
            }
        }, {
            ...options,
            validate,
            syncDirection: _constants.SYNC_DIRECTIONS.BIDIRECTIONAL,
            hooks: {
                async afterDestroy (encounter, opts) {
                    const deletionReason = await models.ReferenceData.findByPk(_constants.TASK_DELETE_RECORDED_IN_ERROR_REASON_ID);
                    // update endtime for all parent tasks of this encounter
                    await models.Task.update({
                        endTime: (0, _dateTime.getCurrentDateTimeString)(),
                        deletedReasonForSyncId: deletionReason?.id ?? null
                    }, {
                        where: {
                            encounterId: encounter.id,
                            parentTaskId: null,
                            frequencyValue: {
                                [_sequelize.Op.not]: null
                            },
                            frequencyUnit: {
                                [_sequelize.Op.not]: null
                            }
                        },
                        transaction: opts.transaction
                    });
                    // set deletion info for all tasks of this encounter
                    await models.Task.update({
                        deletedByUserId: _constants.SYSTEM_USER_UUID,
                        deletedReasonId: deletionReason?.id ?? null,
                        deletedTime: (0, _dateTime.getCurrentDateTimeString)()
                    }, {
                        where: {
                            encounterId: encounter.id
                        },
                        transaction: opts.transaction
                    });
                    // delete all tasks of this encounter
                    await models.Task.destroy({
                        where: {
                            encounterId: encounter.id
                        },
                        transaction: opts.transaction,
                        individualHooks: true
                    });
                    /** clean up all notifications */ await models.Notification.destroy({
                        where: {
                            metadata: {
                                [_sequelize.Op.contains]: {
                                    encounterId: encounter.id
                                }
                            }
                        },
                        transaction: opts.transaction,
                        individualHooks: true
                    });
                },
                afterUpdate: async (encounter, opts)=>{
                    if (encounter.endDate && !encounter.previous('endDate')) {
                        await models.Task.onEncounterDischarged(encounter, opts?.transaction ?? undefined);
                        await models.MedicationAdministrationRecord.removeInvalidMedicationAdministrationRecords(opts?.transaction);
                    }
                }
            }
        });
        (0, _onCreateEncounterMarkPatientForSync.onCreateEncounterMarkPatientForSync)(this);
    }
    static getFullReferenceAssociations() {
        return [
            'department',
            'examiner',
            {
                association: 'location',
                include: [
                    'facility',
                    'locationGroup'
                ]
            },
            {
                association: 'plannedLocation',
                include: [
                    'facility',
                    'locationGroup'
                ]
            },
            'referralSource',
            'diets',
            {
                association: 'triages',
                include: [
                    'chiefComplaint',
                    'secondaryComplaint'
                ]
            }
        ];
    }
    static initRelations(models) {
        this.hasOne(models.Discharge, {
            foreignKey: 'encounterId',
            as: 'discharge'
        });
        this.hasOne(models.Invoice, {
            foreignKey: 'encounterId',
            as: 'invoice'
        });
        this.belongsTo(models.Patient, {
            foreignKey: {
                name: 'patientId',
                allowNull: false
            },
            as: 'patient'
        });
        this.belongsTo(models.User, {
            foreignKey: 'examinerId',
            as: 'examiner'
        });
        this.belongsTo(models.Location, {
            foreignKey: 'locationId',
            as: 'location'
        });
        this.belongsTo(models.Location, {
            foreignKey: 'plannedLocationId',
            as: 'plannedLocation'
        });
        this.belongsTo(models.Department, {
            foreignKey: 'departmentId',
            as: 'department'
        });
        this.hasMany(models.SurveyResponse, {
            foreignKey: 'encounterId',
            as: 'surveyResponses'
        });
        this.hasMany(models.Referral, {
            foreignKey: 'initiatingEncounterId',
            as: 'initiatedReferrals'
        });
        this.hasMany(models.Referral, {
            foreignKey: 'completingEncounterId',
            as: 'completedReferrals'
        });
        this.hasMany(models.AdministeredVaccine, {
            foreignKey: 'encounterId',
            as: 'administeredVaccines'
        });
        this.hasMany(models.EncounterDiagnosis, {
            foreignKey: 'encounterId',
            as: 'diagnoses'
        });
        this.belongsToMany(models.Prescription, {
            foreignKey: 'encounterId',
            as: 'medications',
            through: models.EncounterPrescription
        });
        this.hasMany(models.LabRequest, {
            foreignKey: 'encounterId',
            as: 'labRequests'
        });
        this.hasMany(models.ImagingRequest, {
            foreignKey: 'encounterId',
            as: 'imagingRequests'
        });
        this.hasMany(models.Procedure, {
            foreignKey: 'encounterId',
            as: 'procedures'
        });
        this.hasMany(models.Vitals, {
            foreignKey: 'encounterId',
            as: 'vitals'
        });
        this.hasMany(models.Triage, {
            foreignKey: 'encounterId',
            as: 'triages'
        });
        this.hasMany(models.LabTestPanelRequest, {
            foreignKey: 'encounterId',
            as: 'labTestPanelRequests'
        });
        this.hasMany(models.DocumentMetadata, {
            foreignKey: 'encounterId',
            as: 'documents'
        });
        this.hasMany(models.EncounterHistory, {
            foreignKey: 'encounterId',
            as: 'encounterHistories'
        });
        this.belongsTo(models.ReferenceData, {
            foreignKey: 'patientBillingTypeId',
            as: 'patientBillingType'
        });
        this.belongsTo(models.ReferenceData, {
            foreignKey: 'referralSourceId',
            as: 'referralSource'
        });
        this.belongsToMany(models.ReferenceData, {
            through: models.EncounterDiet,
            as: 'diets',
            foreignKey: 'encounterId'
        });
        this.hasMany(models.Note, {
            foreignKey: 'recordId',
            as: 'notes',
            constraints: false,
            scope: {
                recordType: this.name
            }
        });
        this.hasMany(models.EncounterHistory, {
            foreignKey: 'encounterId',
            as: 'encounterHistory'
        });
        this.hasMany(models.Appointment, {
            foreignKey: 'encounterId',
            as: 'appointments'
        });
        this.hasMany(models.Appointment, {
            foreignKey: 'linkEncounterId',
            as: 'linkedAppointments'
        });
    // this.hasMany(models.Procedure);
    // this.hasMany(models.Report);
    }
    static buildPatientSyncFilter(patientCount, markedForSyncPatientsTable, sessionConfig) {
        const { syncAllLabRequests } = sessionConfig;
        const joins = [];
        const encountersToIncludeClauses = [];
        const updatedAtSyncTickClauses = [
            'encounters.updated_at_sync_tick > :since'
        ];
        if (patientCount > 0) {
            encountersToIncludeClauses.push(`encounters.patient_id IN (SELECT patient_id FROM ${markedForSyncPatientsTable})`);
        }
        // add any encounters with a lab request, if syncing all labs is turned on for facility server
        if (syncAllLabRequests) {
            joins.push(`
        LEFT JOIN (
          SELECT e.id, max(lr.updated_at_sync_tick) as lr_updated_at_sync_tick
          FROM encounters e
          INNER JOIN lab_requests lr ON lr.encounter_id = e.id
          WHERE (e.updated_at_sync_tick > :since OR lr.updated_at_sync_tick > :since)
          ${patientCount > 0 ? `AND e.patient_id NOT IN (SELECT patient_id FROM ${markedForSyncPatientsTable}) -- no need to sync if it would be synced anyway` : ''}
          GROUP BY e.id
        ) AS encounters_with_labs ON encounters_with_labs.id = encounters.id
      `);
            encountersToIncludeClauses.push(`
        encounters_with_labs.id IS NOT NULL
      `);
            updatedAtSyncTickClauses.push(`
        encounters_with_labs.lr_updated_at_sync_tick > :since
      `);
        }
        if (encountersToIncludeClauses.length === 0) {
            return null;
        }
        return `
      ${joins.join('\n')}
      WHERE (
        ${encountersToIncludeClauses.join('\nOR')}
      )
      AND (
        ${updatedAtSyncTickClauses.join('\nOR')}
      )
    `;
    }
    static async buildSyncLookupQueryDetails() {
        return {
            select: await (0, _buildEncounterLinkedLookupFilter.buildEncounterLinkedLookupSelect)(this, {
                isLabRequestValue: 'new_labs.encounter_id IS NOT NULL'
            }),
            joins: `
        LEFT JOIN (
          SELECT DISTINCT encounter_id
          FROM lab_requests
          WHERE updated_at_sync_tick > :since -- to only include lab requests that recently got attached to the encounters
        ) AS new_labs ON new_labs.encounter_id = encounters.id
        LEFT JOIN locations ON encounters.location_id = locations.id
        LEFT JOIN facilities ON locations.facility_id = facilities.id
      `,
            where: `
        encounters.updated_at_sync_tick > :since -- to include including normal encounters
        OR
        new_labs.encounter_id IS NOT NULL -- to include encounters that got lab requests recently attached to it
      `
        };
    }
    static async adjustDataPostSyncPush(recordIds) {
        await (0, _dischargeOutpatientEncounters.dischargeOutpatientEncounters)(this.sequelize.models, recordIds);
    }
    static async create(...args) {
        const [data, options] = args;
        const { actorId, ...encounterData } = data;
        const encounter = await super.create(encounterData, options);
        const { EncounterHistory } = this.sequelize.models;
        await EncounterHistory.createSnapshot(encounter, {
            // No change_type (NULL) for initial snapshots as these are treated differently in integration reports
            actorId: actorId || encounter.examinerId,
            submittedTime: encounter.startDate
        }, options);
        return encounter;
    }
    async addSystemNote(content, date, user) {
        return this.createNote({
            noteTypeId: _constants.NOTE_TYPES.SYSTEM,
            date,
            content,
            ...user?.id && {
                authorId: user?.id
            }
        });
    }
    async onDischarge({ endDate, submittedTime, systemNote, discharge }, user) {
        const { Discharge } = this.sequelize.models;
        await Discharge.create({
            ...discharge,
            encounterId: this.id
        });
        await this.addSystemNote(systemNote || 'Discharged patient.', submittedTime, user);
        await this.closeTriage(endDate);
    }
    async closeTriage(endDate) {
        const { Triage } = this.sequelize.models;
        const triage = await Triage.findOne({
            where: {
                encounterId: this.id
            }
        });
        if (!triage) return;
        if (triage.closedTime) return; // already closed
        await triage.update({
            closedTime: endDate
        });
    }
    async update(data, user) {
        const { Department, Location, EncounterHistory, ReferenceData, User } = this.sequelize.models;
        // Track change types for encounter history snapshot
        const changeTypes = [];
        // To collect system note messages describing all changes in this encounter update
        const systemNoteRows = [];
        const { onChangeForeignKey, onChangeTextColumn } = (0, _recordModelChanges.createChangeRecorders)(this, data, systemNoteRows, changeTypes);
        const updateEncounter = async ()=>{
            const additionalChanges = {};
            if (data.endDate && !this.endDate) {
                await this.onDischarge(data, user);
            }
            if (data.patientId && data.patientId !== this.patientId) {
                throw new _errors.InvalidOperationError("An encounter's patient cannot be changed");
            }
            if (data.plannedLocationId && data.plannedLocationId === this.locationId) {
                throw new _errors.InvalidOperationError('Planned location cannot be the same as current location');
            }
            // Handle planned location cancellation (when set to null)
            if (data.plannedLocationId === null) {
                // The automatic timeout doesn't provide a submittedTime, prevents double noting a cancellation
                if (this.plannedLocationId && data.submittedTime) {
                    const currentlyPlannedLocation = await Location.findOne({
                        where: {
                            id: this.plannedLocationId
                        }
                    });
                    systemNoteRows.push(`Cancelled planned move to ‘${currentlyPlannedLocation?.name}’`);
                }
                additionalChanges.plannedLocationStartTime = null;
            }
            // Handle new planned location assignment
            if (data.plannedLocationId && data.plannedLocationId !== this.plannedLocationId) {
                const { Location } = this.sequelize.models;
                const oldLocation = await Location.findOne({
                    where: {
                        id: this.locationId
                    },
                    include: 'locationGroup'
                });
                const newLocation = await Location.findOne({
                    where: {
                        id: data.plannedLocationId
                    },
                    include: 'locationGroup'
                });
                if (!newLocation) {
                    throw new _errors.InvalidOperationError('Invalid location specified');
                }
                systemNoteRows.push(`Added a planned location change from ‘${Location.formatFullLocationName(oldLocation)}’ to ‘${Location.formatFullLocationName(newLocation)}’`);
                additionalChanges.plannedLocationStartTime = data.submittedTime;
            }
            // Handle diet changes (many-to-many relationship, so handled separately)
            const generateDietString = (diets)=>{
                return diets.map((diet)=>diet.name).join(', ') || '-';
            };
            const oldDiets = await this.getDiets();
            const oldDietIds = oldDiets.map((diet)=>diet.id);
            if (data.dietIds && !(0, _lodash.isEqual)(oldDietIds, JSON.parse(data.dietIds))) {
                const newDietIds = JSON.parse(data.dietIds);
                const newDiets = await ReferenceData.findAll({
                    where: {
                        id: {
                            [_sequelize.Op.in]: newDietIds
                        }
                    }
                });
                systemNoteRows.push(`Changed diet from ‘${generateDietString(oldDiets)}’ to ‘${generateDietString(newDiets)}’`);
            }
            await onChangeForeignKey({
                columnName: 'locationId',
                noteLabel: 'location',
                model: Location,
                sequelizeOptions: {
                    include: [
                        'locationGroup'
                    ]
                },
                accessor: Location.formatFullLocationName,
                changeType: _constants.EncounterChangeType.Location,
                onChange: async ()=>{
                    // When we move to a new location, clear the planned location move
                    additionalChanges.plannedLocationId = null;
                    additionalChanges.plannedLocationStartTime = null;
                }
            });
            await onChangeTextColumn({
                columnName: 'encounterType',
                noteLabel: 'encounter type',
                formatText: _lodash.capitalize,
                changeType: _constants.EncounterChangeType.EncounterType,
                onChange: async ()=>{
                    await this.closeTriage(data.submittedTime);
                }
            });
            await onChangeForeignKey({
                columnName: 'departmentId',
                noteLabel: 'department',
                model: Department,
                changeType: _constants.EncounterChangeType.Department
            });
            await onChangeForeignKey({
                columnName: 'examinerId',
                noteLabel: 'supervising clinician',
                model: User,
                accessor: (record)=>record?.displayName ?? '-',
                changeType: _constants.EncounterChangeType.Examiner
            });
            // Start date is referred to differently in the UI based on the encounter type
            const encounterType = data.encounterType ?? this.encounterType;
            await onChangeTextColumn({
                columnName: 'startDate',
                noteLabel: encounterType === _constants.ENCOUNTER_TYPES.ADMISSION ? 'admission date & time' : 'date & time',
                formatText: (date)=>date ? (0, _dateFormatters.formatShortDateTime)(date, (0, _timeZoneCheck.getPrimaryTimeZone)(_config.default)) : '-'
            });
            await onChangeTextColumn({
                columnName: 'estimatedEndDate',
                noteLabel: 'estimated discharge date',
                formatText: (date)=>date ? (0, _dateFormatters.formatShort)(date, (0, _timeZoneCheck.getPrimaryTimeZone)(_config.default)) : '-'
            });
            await onChangeForeignKey({
                columnName: 'patientBillingTypeId',
                noteLabel: 'patient type',
                model: ReferenceData
            });
            await onChangeForeignKey({
                columnName: 'referralSourceId',
                noteLabel: 'referral source',
                model: ReferenceData
            });
            await onChangeTextColumn({
                columnName: 'reasonForEncounter',
                noteLabel: 'reason for encounter'
            });
            const { submittedTime, skipSystemNotes, ...encounterData } = data;
            const updatedEncounter = await super.update({
                ...encounterData,
                ...additionalChanges
            }, user);
            // Create snapshot with array of change types
            if (changeTypes.length > 0) {
                await EncounterHistory.createSnapshot(updatedEncounter, {
                    actorId: user?.id,
                    changeType: changeTypes,
                    submittedTime
                });
            }
            if (!skipSystemNotes && systemNoteRows.length > 0) {
                const formattedSystemNote = systemNoteRows.map((row)=>`• ${row}`).join('\n');
                await this.addSystemNote(formattedSystemNote, submittedTime || (0, _dateTime.getCurrentDateTimeString)(), user);
            }
            return updatedEncounter;
        };
        if (this.sequelize.isInsideTransaction()) {
            return updateEncounter();
        }
        // If the update is not already in a transaction, wrap it in one
        // Having nested transactions can cause bugs in postgres so only conditionally wrap
        return this.sequelize.transaction(async ()=>{
            return await updateEncounter();
        });
    }
};

//# sourceMappingURL=Encounter.js.map