"use strict";
Object.defineProperty(exports, "__esModule", {
    value: true
});
Object.defineProperty(exports, "mSupplyMedIntegrationProcessor", {
    enumerable: true,
    get: function() {
        return mSupplyMedIntegrationProcessor;
    }
});
const _config = /*#__PURE__*/ _interop_require_default(require("config"));
const _undici = require("undici");
const _sequelize = require("sequelize");
const _facts = require("@tamanu/constants/facts");
const _tasks = require("@tamanu/shared/tasks");
const _logging = require("@tamanu/shared/services/logging");
const _fetchWithRetryBackoff = require("@tamanu/api-client/fetchWithRetryBackoff");
const _selectFacilityIds = require("@tamanu/utils/selectFacilityIds");
const _sleepAsync = require("@tamanu/utils/sleepAsync");
function _interop_require_default(obj) {
    return obj && obj.__esModule ? obj : {
        default: obj
    };
}
// INTEGRATION DETAILS
const INTEGRATION_PLUGIN_CODE = 'bes-plugins';
const INTEGRATION_ENDPOINT = '/graphql';
// GRAPHQL QUERIES
const AUTH_QUERY = `
  query Auth($password: String!, $username: String!) {
    authToken(password: $password, username: $username) {
      ... on AuthToken {
        __typename
        token
      }
      ... on AuthTokenError {
        __typename
        error {
          description
        }
      }
    }
  }
`;
function getPostQuery(storeId) {
    return `
    query GraphqlPlugin($input: JSON!) {
      pluginGraphqlQuery(
        pluginCode: "${INTEGRATION_PLUGIN_CODE}"
        storeId: "${storeId}"
        input: $input
      )
    }
  `;
}
let mSupplyMedIntegrationProcessor = class mSupplyMedIntegrationProcessor extends _tasks.ScheduledTask {
    getName() {
        return 'mSupplyMedIntegrationProcessor';
    }
    async createLog(values) {
        const { items, ...rest } = values;
        const hasItems = items && Array.isArray(items) && items.length > 0;
        const payload = {
            ...rest,
            ...hasItems && {
                items
            }
        };
        await this.models.MSupplyPushLog.create(payload);
    }
    async getSettings(facilityId) {
        const integrationSettings = await this.context.settings[facilityId]?.get('integrations.mSupplyMed');
        return integrationSettings ?? {};
    }
    async getAuthHeader(facilityId) {
        const { host, backoff } = await this.getSettings(facilityId);
        const { username, password } = _config.default.integrations.mSupplyMed;
        const variables = {
            username,
            password
        };
        try {
            const response = await (0, _fetchWithRetryBackoff.fetchWithRetryBackoff)(`${host}${INTEGRATION_ENDPOINT}`, {
                fetch: _undici.fetch,
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json'
                },
                body: JSON.stringify({
                    query: AUTH_QUERY,
                    variables
                })
            }, {
                ...backoff,
                log: _logging.log
            });
            const { data } = await response.json();
            const authToken = data?.authToken?.token;
            if (!authToken) {
                throw new Error('No auth token found');
            }
            return `Bearer ${authToken}`;
        } catch (error) {
            throw new Error('Authentication failed: ' + error.message);
        }
    }
    async updateMSupplyStock(medications, facilityId) {
        const minMedicationId = medications[0].id;
        const minMedicationCreatedAt = medications[0].createdAt;
        const maxMedicationCreatedAt = medications[medications.length - 1].createdAt;
        const maxMedicationId = medications[medications.length - 1].id;
        _logging.log.info(`Sending ${medications.length} dispensed medications to mSupply`, {
            minMedicationId,
            minMedicationCreatedAt,
            maxMedicationCreatedAt,
            maxMedicationId
        });
        const { host, backoff, storeId, customerCode } = await this.getSettings(facilityId);
        const postQuery = getPostQuery(storeId);
        const variables = {
            input: {
                invoiceId: minMedicationId,
                customerCode,
                items: medications.map((medication)=>({
                        itemCode: medication.pharmacyOrderPrescription.prescription.medication.code,
                        numberOfUnits: medication.quantity
                    }))
            }
        };
        try {
            const response = await (0, _fetchWithRetryBackoff.fetchWithRetryBackoff)(`${host}${INTEGRATION_ENDPOINT}`, {
                fetch: _undici.fetch,
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json',
                    Authorization: this.authHeader
                },
                body: JSON.stringify({
                    query: postQuery,
                    variables
                })
            }, {
                ...backoff,
                log: _logging.log
            });
            const { data } = await response.json();
            const { success, message, items } = data?.pluginGraphqlQuery ?? {};
            if (success) {
                await this.createLog({
                    minMedicationCreatedAt,
                    maxMedicationCreatedAt,
                    minMedicationId,
                    maxMedicationId,
                    status: 'success',
                    message
                });
            } else {
                const err = new Error(message);
                // Add items to the error for debugging
                err.items = items;
                throw err;
            }
        } catch (error) {
            await this.createLog({
                minMedicationCreatedAt,
                maxMedicationCreatedAt,
                minMedicationId,
                maxMedicationId,
                status: 'failed',
                message: error.message,
                items: error.items
            });
            throw error;
        }
    }
    async getBaseQuery(facilityId, enabledAt) {
        const lastSuccessfulPush = await this.models.MSupplyPushLog.findOne({
            where: {
                status: 'success'
            },
            order: [
                [
                    'createdAt',
                    'DESC'
                ]
            ]
        });
        const enabledAtCondition = {
            createdAt: {
                [_sequelize.Op.gte]: enabledAt
            }
        };
        const include = [
            {
                model: this.models.PharmacyOrderPrescription,
                as: 'pharmacyOrderPrescription',
                required: true,
                include: [
                    {
                        model: this.models.Prescription,
                        as: 'prescription',
                        required: true,
                        include: [
                            {
                                model: this.models.ReferenceData,
                                as: 'medication',
                                required: true
                            }
                        ]
                    },
                    {
                        model: this.models.PharmacyOrder,
                        as: 'pharmacyOrder',
                        required: true,
                        include: [
                            {
                                model: this.models.Facility,
                                as: 'facility',
                                required: true,
                                where: {
                                    id: facilityId
                                }
                            }
                        ]
                    }
                ]
            }
        ];
        // Process everything after enabledAt if there is no successful push
        if (!lastSuccessfulPush) {
            return {
                where: enabledAtCondition,
                include
            };
        }
        const { maxMedicationCreatedAt, maxMedicationId } = lastSuccessfulPush;
        return {
            where: {
                [_sequelize.Op.and]: [
                    enabledAtCondition,
                    {
                        [_sequelize.Op.or]: [
                            // Case 1: Newer timestamps
                            {
                                createdAt: {
                                    [_sequelize.Op.gt]: maxMedicationCreatedAt
                                }
                            },
                            // Case 2: Exact same timestamp, newer ID
                            {
                                createdAt: maxMedicationCreatedAt,
                                id: {
                                    [_sequelize.Op.gt]: maxMedicationId
                                }
                            }
                        ]
                    }
                ]
            },
            include
        };
    }
    async getEnabledAt() {
        const enabledAt = await this.models.LocalSystemFact.get(_facts.FACT_MSUPPLY_MED_INTEGRATION_ENABLED_AT);
        if (enabledAt) return enabledAt;
        const newEnabledAt = new Date().toISOString();
        await this.models.LocalSystemFact.set(_facts.FACT_MSUPPLY_MED_INTEGRATION_ENABLED_AT, newEnabledAt);
        return newEnabledAt;
    }
    async run() {
        const { enabled, username, password } = _config.default.integrations.mSupplyMed;
        // If the integration is disabled, delete the enabled-at fact and skip
        if (!enabled) {
            await this.models.LocalSystemFact.set(_facts.FACT_MSUPPLY_MED_INTEGRATION_ENABLED_AT, null);
            _logging.log.warn('mSupplyMedIntegrationProcessor is disabled, skipping');
            return;
        }
        // Get the enabled-at timestamp from the database or
        // set it to the current date if it doesn't exist
        const enabledAt = await this.getEnabledAt();
        // Ensure this facility is not an omni server
        if (this.serverFacilityIds.length > 1) {
            _logging.log.warn('This facility is an omni server, skipping mSupplyMedIntegrationProcessor');
            return;
        }
        const [facilityId] = this.serverFacilityIds;
        const { host, storeId, customerCode } = await this.getSettings(facilityId);
        if (!host || !username || !password || !storeId || !customerCode) {
            _logging.log.warn('Integration for mSupplyMedIntegrationProcessor not configured, skipping');
            return;
        }
        const { batchSize, batchSleepAsyncDurationInMilliseconds } = this.scheduleConfig;
        if (!batchSize || !batchSleepAsyncDurationInMilliseconds) {
            throw new Error('batchSize and batchSleepAsyncDurationInMilliseconds must be set for mSupplyMedIntegrationProcessor');
        }
        const baseQuery = await this.getBaseQuery(facilityId, enabledAt);
        const toProcess = await this.models.MedicationDispense.count(baseQuery);
        if (toProcess === 0) return;
        // Log in and process the dispensed medications in batches
        this.authHeader = await this.getAuthHeader(facilityId);
        const batchCount = Math.ceil(toProcess / batchSize);
        for(let i = 0; i < batchCount; i++){
            const medications = await this.models.MedicationDispense.findAll({
                ...baseQuery,
                order: [
                    [
                        'createdAt',
                        'ASC'
                    ],
                    [
                        'id',
                        'ASC'
                    ]
                ],
                limit: batchSize,
                offset: i * batchSize
            });
            // Ensure we have at least one dispensed medication to process,
            // even though we already counted them, this could happen if they
            // were deleted between batch count and query.
            if (medications.length === 0) break;
            try {
                await this.updateMSupplyStock(medications, facilityId);
            } catch (error) {
                _logging.log.error('Error sending dispensed medications to mSupplyMed', {
                    error
                });
                break;
            }
            if (i < batchCount - 1) {
                await (0, _sleepAsync.sleepAsync)(batchSleepAsyncDurationInMilliseconds);
            }
        }
    }
    constructor(context){
        const conf = _config.default.schedules.mSupplyMedIntegrationProcessor;
        const { schedule, jitterTime, enabled } = conf;
        super(schedule, _logging.log, jitterTime, enabled);
        this.scheduleConfig = conf;
        this.context = context;
        this.models = context.models;
        this.serverFacilityIds = (0, _selectFacilityIds.selectFacilityIds)(_config.default);
        this.authHeader = null;
    }
};

//# sourceMappingURL=mSupplyMedIntegrationProcessor.js.map