/* Official Front-End AiTmed Software Development Kit */ import { schema } from 'normalizr' import reduce from 'lodash/reduce' import get from 'lodash/get' import forEach from 'lodash/forEach' import isPlainObject from 'lodash/isPlainObject' import makeRequest from './makeRequest' import makeAccountClient from './makeAccountClient' import subscriptions from './subscriptions' import * as utils from './utils' import * as validate from './validate' import * as types from './types/sdk' import * as E from './customErrors' import * as errorHelpers from './errorHelpers' const { logError, isArray } = utils const aitmedApi = function(opts: any) { const _store: types.Store = { accountType: 'guest', prevCalls: [], } const requestUtils = makeRequest(opts) const client = requestUtils.getClient() const accountClient = makeAccountClient({ client, requestUtils, _store }) const dataPath = 'data.result' const accountDataPath = 'data.data' // new API response structure for /account/ endpoints const _accountTypes: types.AccountType[] = [ 'admin', 'patient', 'provider', 'marketer', 'company', 'staffing_company', 'guest', // When the user is not logged in ] // TODO: unit test const addPrevCall = (uri: string) => { let removedCall if (_store.prevCalls.length > 30) { removedCall = _store.prevCalls.shift() } _store.prevCalls.push(uri) return removedCall } // TODO: unit test const wrappedReq = function(method: string) { return async function(...args: any[]) { try { if (typeof args[0] === 'string') addPrevCall(args[0]) if (!_accountTypes.includes(_store.accountType)) { throw new Error('accountType must be set in the store') } const response = await client[method](...args) return response } catch (error) { if (error.response) { const { response = {} } = error const reason = response.data ? response.data.detail : '' const permissionErr = /permission/i.test(reason) const signatureErr = /signature/i.test(reason) if (signatureErr) subscriptions.publish('signatureError', error) if (permissionErr) { throw new E.PermissionError( 'You do not have permission to perform this action.', ) } } throw error } } } return { account: accountClient, ...requestUtils, ...subscriptions, // key must be passed in by having it grabbed from the URL on an activation page async activateAccount(key: string) { try { const accountType = this.getCurrentAccountType() const url = `/account/active/?key=${key}` // https://testapi.aitmed.com/v2/account/activate/?key= await this.req.get(url) // 302 - redirect to success or fail page } catch (error) { throw error } }, async activateCompany() {}, async activateProvider(userId: string): Promise { try { this._validateParams('activateProvider', userId) const url = `/account/provider/${userId}/approve/` const response = await this.req.put(url) return get(response, accountDataPath, null) } catch (error) { throw error } }, async activateMarketer() {}, async activateStaffingCompany() {}, async addCompanyMember() {}, async approveProvider(...args: any[]) { return this.activateProvider(...args) }, // When provider clicks on the SMS link from the blue dot from patient's Get Care flow async authOfflineProvider( token: string, ): Promise { try { const url = '/account/auth_with_token/' const params = { token } const response = await this.req.get(url, { params }) return get(response, dataPath, null) } catch (error) { throw error } }, async authPatient(params: { phoneNumber?: string phone_number?: string password: string }): Promise { try { // Allow the caller to have some flexibility passing in either format const phone_number = params.phone_number || params.phoneNumber const password = params.password const url = `/account/patient/login/` const postData = { phone_number, password } const response = await this.req.post(url, postData) const { data } = response.data this.setToken(data.jwt_token) this.setHeader('Authorization', `JWT ${data.jwt_token}`) this.setAccountType('patient') await this.initTruevaultAccessToken(data.tv_access_token) return data } catch (error) { throw error } }, async authProvider(params: { phoneNumber?: string phone_number?: string password: string }): Promise { try { // Allow the caller to have some flexibility passing in either format const phone_number = params.phone_number || params.phoneNumber const password = params.password const url = `/account/provider/login/` const postData = { phone_number, password } const response = await this.req.post(url, postData) const { data } = response.data this.setToken(data.jwt_token) this.setHeader('Authorization', `JWT ${data.jwt_token}`) this.setAccountType('provider') await this.initTruevaultAccessToken(data.tv_access_token) return data } catch (error) { throw error } }, async cancelAppointment(id: string, reason: string) { try { const url = `/appointment/${id}/cancel/` const response = await this.req.post(url, { reason }) return get(response, dataPath, null) } catch (error) { throw error } }, async changePassword() {}, async completeAppointment(id: string) { try { this._validateParams('completeAppointment', id) const url = `/appointment/${id}/complete/` const response = await this.req.put(url) return get(response, dataPath, null) } catch (error) { throw error } }, async confirmEmail(key: string): Promise { try { this._validateParams('confirmEmail', key) const url = '/account/confirm_email/' const response = await this.req.get(url) return 'success' } catch (error) { throw error } }, async createAppointment( params: types.CreateAppointmentParams, ): Promise { try { const finalParams = { ...params } const url = '/appointment/' const accountType = this.getCurrentAccountType() if (!['patient'].includes(accountType)) { throw new E.WrongAccountTypeError('Only patients can use this URI') } // this._validateParams('createAppointment', params) if (params.coupons !== undefined) { // Backend only takes the first coupon at the moment // Backend requires an array as the parameter value finalParams.coupons = [utils.ensureArray(params.coupons)[0]] } const response = await this.req.post(url, finalParams) const result = get(response, dataPath, null) return result } catch (error) { throw error } }, async createAppointmentReview( values: types.CreateAppointmentReviewParams, ): Promise { try { const url = '/appointment_review/' const accountType = this.getCurrentAccountType() if (accountType !== 'patient') { throw new E.WrongAccountTypeError( 'Only patients can create an appointment review', ) } this._validateParams('createAppointmentReview', values) const response = await this.req.post(url, values) return get(response, dataPath, null) } catch (error) { throw error } }, async createBlob(file: any, ownerId?: string) { try { const vaultId = this.getTvVaultId() const result = await this.tvClient.createBlob(vaultId, file, ownerId) return result } catch (error) { throw error } }, async createPatient( username: string, password: string, otherData?: types.CreatePatientOtherDataArgs, ) { try { let tv_uid let tv_token const email = username const attributes = otherData ? { email, ...otherData } : { email } const tvUser = await this.createTvUser(username, password, attributes) if (tvUser) { this.setAccountType('patient') tv_uid = tvUser.user_id tv_token = tvUser.access_token const url = '/account/patient/' const reqData: types.CreatePatientRequestData = { tv_uid, tv_token } if (otherData) { if (isArray(otherData.languages)) { reqData.languages = otherData.languages } if (otherData.inviter_code) { reqData.inviter_code = otherData.inviter_code } } const response = await this.req.post(url, reqData) const patient = get(response, dataPath, null) return patient } else { throw new Error( 'Could not create a patient account. (Truevault error).', ) } } catch (error) { throw error } }, async createPatientv2(params: any) { // try { // } catch (error) { throw error } }, async createPatientBlob(blobInfo: types.CreatePatientBlobBlobInfo) { try { this._validateParams('createPatientBlob', blobInfo) const url = '/account/patient_blob/' await this.req.post(url, blobInfo) return 'success' } catch (error) { throw error } }, // TODO: unit test async createPatientMember({ username, password, attributes, }: types.CreatePatientMemberArgs) { try { if (!username) { throw new Error('Please provide a username') } const tvArgs = [username, password, attributes] const tvUser = await this.createTvUser(...tvArgs) const url = '/account/patient_member/' const data = { tv_uid: tvUser.id, tv_token: tvUser.access_token } const response = await this.req.post(url, data) return get(response, dataPath, null) } catch (error) { throw error } }, async createChasePaymentProfile( params: types.ChasePaymentProfileParams, ): Promise { try { const url = '/payment/chase_payment_profile/' const response = await this.req.post(url, params) const result = get(response, dataPath, null) return result } catch (error) { throw error } }, async createProvider(values: any) { try { const url = '/v2/account/provider/' const response = await this.req.post(url, values) return get(response, dataPath, null) } catch (error) { throw error } }, async createCompany() {}, async createMarketer() {}, async createReferringPatient( email: string, ): Promise { try { const url = '/referral/' const response = await this.req.post(url, { email }) return get(response, dataPath, null) } catch (error) { throw error } }, async createStaffingCompany( values: types.CreateStaffingCompaniesParams, ): Promise { try { const url = '/account/staffing_company/' const response = await this.req.post(url, values) return get(response, dataPath, null) } catch (error) { throw error } }, async createStaffingCompanyProvider() {}, async deactivatePatient(userId: string) { try { const url = `/account/patient/${userId}/` const response = this.req.delete(url) return get(response, accountDataPath, null) } catch (error) { throw error } }, async deactivateCompany() {}, async deactivateMarketer() {}, async deactivateProvider(userId: string): Promise { try { if (userId === undefined) { throw new E.MissingParametersError('userId is undefined') } const url = `/account/provider/${userId}/suspend/` const response = await this.req.put(url) return get(response, accountDataPath, null) } catch (error) { throw error } }, async deactivateStaffingCompany() {}, async deleteChasePaymentProfile(id: string): Promise<'success'> { try { const url = `/payment/chase_payment_profile/${id}/` await this.req.delete(url) return 'success' } catch (error) { throw error } }, async deleteFromAvailabilityAgenda(start: string, end: string) { try { const url = `/agenda/availability/remove/?start=${start}&end=${end}` const response = await this.req.delete(url) return get(response, dataPath, null) } catch (error) { throw error } }, async deletePatientBlob(id: string) { try { this._validateParams('deletePatientBlob', id) const url = `/account/patient_blob/${id}/` await this.req.delete(url) return 'success' } catch (error) { if (error.response) { // 403 - Forbidden -- the owner of this blob id is not related to this user // 404 - Blob id not found } throw error } }, getCurrentAccountType() { return _store.accountType }, async getFundingStatus() { try { const url = '/payment/dwolla/get_default_funding_status/' const response = await this.req.get(url) return get(response, dataPath, null) } catch (error) { throw error } }, getPreviousApiCalls(limit: number = 30) { return _store.prevCalls.slice(0, limit) }, getStore() { return _store }, async fetchAppointment(id: string) { const url = `/appointment/${id}/` const response = await this.req.get(url) const appointment = get(response, dataPath, null) return appointment }, async fetchAppointments({ status = [1, 2], limit, order_by, filter: _filter, patients, }: types.AppointmentFuncArgs = {}) { try { const result: types.FetchAppointmentsReturnObject = { appointments: {}, ids: [], pagination: { num_pages: 0, object_count: 0, page_index: 0, page_limit: 0, }, } const options: types.AppointmentFuncArgs = { status: this._toClonedQueries('status', status), limit, order_by, } if (patients) { // @ts-ignore if (isArray(patients)) options.patients = patients.join(',') else options.patients = patients } const url = this._constructURL(`/appointment/?`, options) const response = await this.req.get(url) const pagination = get(response, `${dataPath}.pagination`, null) const data = get(response, `${dataPath}.data`, []) const normalizeArgs = ['appointments', data, this.schemas.appointments] const normalizedData = utils._normalize(...normalizeArgs) const { ids, appointments } = normalizedData const reducerFilterer = (...args: any[]): any => { const acc = args[0] const appt = args[1] const key = args[2] const collection = args[3] if (_filter && _filter(appt, key, collection)) acc[key] = appt return acc } result.appointments = _filter ? reduce(appointments, reducerFilterer, {}) : appointments result.pagination = pagination result.ids = _filter ? Object.keys(result.appointments) : ids return result } catch (error) { throw error } }, async fetchAppointmentPatientImages(appointmentId: string) { try { const url = `/account/patient_blob/?appointment=${appointmentId}` const response = await this.req.get(url) // TODO: normalize the data return get(response, dataPath, null) } catch (error) { throw error } }, async fetchAppointmentReview(id: string): Promise { try { if (!id) throw new E.InvalidParametersError( 'Tried to retrieve an appointment review but the review "id" is invalid', ) const url = `/appointment_review/${id}/` const response = await this.req.get(url) return get(response, dataPath, null) } catch (error) { throw error } }, async fetchAppointmentReviews( userId: string, ): Promise { try { this._validateParams('fetchAppointmentReviews', userId) const url = `/appointment_review/?provider_id=${userId}` const response = await this.req.get(url) const pagination = get(response, `${dataPath}.pagination`) const data = get(response, `${dataPath}.data`) const normalizeArgs = ['reviews', data, this.schemas.reviews] const { reviews, ids } = utils._normalize(...normalizeArgs) return { pagination, reviews, ids } } catch (error) { throw error } }, async fetchAppointmentTransactionDetails(id: string) { try { const accountType = this.getCurrentAccountType() const url = `/payment/transfer/${accountType}/get_appt_transaction_details/${id}` const response = await this.req.get(url) return get(response, dataPath, null) } catch (error) { throw error } }, async fetchAvailabilityAgenda(start: string, end: string) { try { const url = `/agenda/availability/?start=${start}&end=${end}` const response = await this.req.get(url) return get(response, dataPath, null) } catch (error) { throw error } }, async fetchBillingCycleRevenue() { try { const accountType = this.getCurrentAccountType() const url = `/payment/transfer/${accountType}/get_billing_cycle_revenue/` const response = await this.req.get(url) return get(response, dataPath, null) } catch (error) { throw error } }, async fetchCategories() { const response = await this.req.get(`/info/categories/`) return get(response, dataPath, null) }, async fetchCompanies() {}, async fetchCompanyMembers() {}, async fetchCompletedAppointments() {}, async fetchCoupons(): Promise { try { this._validateParams('fetchCoupons') const url = '/payment/coupon/' const response = await this.req.get(url) return get(response, dataPath, null) } catch (error) { throw error } }, async fetchGenders() { const response = await this.req.get(`/info/gender/`) return get(response, dataPath, null) }, async fetchDwollaToken(): Promise { try { const url = '/payment/dwolla/iav_token/' const response = await this.req.get(url) return get(response, dataPath, null) } catch (error) { throw error } }, async fetchLanguages() { const response = await this.req.get(`/info/languages/`) return get(response, dataPath) }, async fetchMarketers() {}, async fetchPatients( params?: types.FetchPatientsParams, ): Promise { try { const response = await this.req.get('/account/patient/', { params }) // Backend API response changed so we customized the data path here const { data, pagination } = get(response, 'data', null) const normalizeArgs = [ 'patientsDownsized', data, this.schemas.patientsDownsized, ] const { ids, patientsDownsized: patients } = utils._normalize( ...normalizeArgs, ) const result = { pagination, patients, ids } return result } catch (error) { throw error } }, async fetchPaidTransactionDetails(queryDate: string) { try { // queryDate format --> yyyy-mm-dd const accountType = this.getCurrentAccountType() const url = `/payment/transfer/${accountType}/get_paid_transaction_details/` // const queryKey = 'query_date' const response = await this.req.get(url) return get(response, dataPath, null) } catch (error) { throw error } }, async fetchPaymentProfile() { try { const url = '/payment/chase_payment_profile/' const response = await this.req.get(url) const data: types.PaymentProfile[] = get(response, dataPath, null) const normalizeArgs = [ 'paymentProfile', data, this.schemas.paymentProfile, ] const { paymentProfile, ids } = utils._normalize(...normalizeArgs) const result = { paymentProfiles: paymentProfile, ids } return result } catch (error) { throw error } }, async fetchPharmacies( params: types.FetchPharmaciesParams, options?: { asArray?: boolean }, ): Promise< | { pharmacies: { [company: string]: types.Pharmacy }; ids: string[] } | types.Pharmacy[] > { try { const url = '/pharmacy/' const response = await this.req.get(url, { params }) const data: types.Pharmacy[] = get(response, dataPath, null) if (options && options.asArray) return data const normalizeArgs = ['pharmacies', data, this.schemas.pharmacies] const { pharmacies, ids } = utils._normalize(...normalizeArgs) return { pharmacies, ids } } catch (error) { throw error } }, async fetchPreviousEncounters() {}, async fetchProvider(userId: string) { try { const url = `/account/provider/${userId}/` const result = await this.req.get(url) return get(result, 'data', null) } catch (error) { throw error } }, async fetchProviders( params: types.FetchProvidersParams = {}, ): Promise { try { //this._validateParams('fetchProviders', params) const response = await this.req.get('/account/provider/', { params }) // Backend API response changed so we customized the data path here const { data, pagination } = get(response, 'data', null) const normalizeArgs = ['providers', data, this.schemas.providers] const { ids, providers } = utils._normalize(...normalizeArgs) const result = { pagination, providers, ids } return result } catch (error) { throw error } }, async fetchReferralCompanies() {}, async fetchSpecialties() { try { const url = '/info/specialties/' const response = await this.req.get(url) const data = get(response, dataPath, null) const normalizeArgs = ['specialties', data, this.schemas.specialties] const result = utils._normalize(...normalizeArgs) result.ids = result.ids.sort() return result } catch (error) { throw error } }, async fetchStaffingCompanies( params: types.FetchStaffingCompaniesParams = {}, ): Promise { try { this._validateParams('staffingCompanies', params) const response = await this.req.get('/account/staffing_company/', { params, }) const { data, pagination } = get(response, dataPath, null) const normalizeArgs = [ 'staffingCompanies', data, this.schemas.staffingCompanies, ] const { ids, staffingCompanies } = utils._normalize(...normalizeArgs) const result = { pagination, staffingCompanies, ids } return result } catch (error) { throw error } }, async fetchStaffingCompanyProviders() {}, async fetchStaffingCompanyPaymentCode(userId: string) { try { const url = `/account/staffing_company/${userId}/payment_codes/` const result = await this.req.get(url) return get(result, dataPath, null) } catch (error) { throw error } }, async fetchStaffingCompanyPaymentCodeByCount( user_id: string, count: number, ) { try { const url = '/account/staffing_company/create_payment_codes/' const data = { user_id, count } const response = await this.req.post(url, data) return get(response, dataPath, null) } catch (error) { throw error } }, async fetchBillingCycle() {}, async fetchFundingStatus() {}, async fetchICD10() {}, async fetchMeetingToken(appointmentId: string) { try { const url = `/appointment/${appointmentId}/join_meeting/` const response = await this.req.get(url) return get(response, dataPath, null) } catch (error) { throw error } }, async fetchPatient(userId: string) { try { const response = await this.req.get(`/account/patient/${userId}/`) return get(response, 'data.data', null) } catch (error) { throw error } }, async fetchPatientBlob(id: string) { try { const url = `/account/patient_blob/${id}/` const response = await this.req.get(url) return get(response, dataPath, null) } catch (error) { throw error } }, async fetchPatientBlobs(userIds: string[] = []): Promise { try { let _userIds = userIds if (typeof _userIds === 'string') _userIds = [_userIds] let url = `/account/patient_blob/?` _userIds.forEach((id) => (url += `patient=${id}&`)) url = utils.trimAmpersand(url) const response = await this.req.get(url) const pagination = get(response, `${dataPath}.pagination`, null) const data = get(response, `${dataPath}.data`, []) const normalizeArgs = ['blobs', data, this.schemas.blobs] const { blobs, ids } = utils._normalize(...normalizeArgs) return { pagination, blobs, ids } } catch (error) { throw error } }, async fetchPatientDocument(docId: string) { try { this._validateParams('fetchPatientDocument', docId) const vaultId = this.getTvVaultId() const result = await this.tvClient.getDocuments(vaultId, [docId]) const document = result[0] return document } catch (error) { throw error } }, async fetchPatientDocuments(docIds: string[]) { try { this._validateParams('fetchPatientDocuments', docIds) const vaultId = this.getTvVaultId() const data = await this.tvClient.getDocuments(vaultId, docIds) const normalizeArgs = ['documents', data, this.schemas.documents] const { documents, ids } = utils._normalize(...normalizeArgs) const result = { documents, ids } return result } catch (error) { throw error } }, async fetchPatientInfo() {}, async fetchPatientMembers( tvUids: string[], { full = true }: { full?: boolean } = {}, ) { try { this._validateParams('fetchPatientMembers', tvUids, { full }) const withEnsuredArr = utils.ensureArray(tvUids) const result = await this.tvClient.readUsers(withEnsuredArr, full) const args = ['patientMembers', result, this.schemas.patientMembers] const normalizedData = utils._normalize(...args) return normalizedData } catch (error) { throw error } }, async fetchPreviousAppointments(_opts: any) { return this.fetchAppointments({ ..._opts, status: [3], }) }, async fetchRxDrug() {}, async fetchStaffingCompanyProvidersAppointmentRevenues() {}, async fetchStaffingCompanyProvidersSpecialtyPrices() {}, async fetchTotalRevenue() { try { const accountType = this.getCurrentAccountType() const url = `/payment/transfer/${accountType}/get_total_revenue/` const response = await this.req.get(url) return get(response, dataPath, null) } catch (error) { throw error } }, async fetchUpcomingAppointments(_opts: any) { return this.fetchAppointments({ ..._opts, status: [1, 2], }) }, async findProviders( params: types.FindProvidersParams, ): Promise<{ pagination: types.Pagination providers: types.FindProviderResult[] timeSlotIds: string[] | number[] }> { try { const accountType = this.getCurrentAccountType() if (accountType !== 'patient') { throw new E.WrongAccountTypeError('Only patients can use this URI') } this._validateParams('findProviders', params) const keysToLookFor = ['specialty', 'language', 'time', 'state'] const keys = Object.keys(params) const { validKeys, invalidKeys } = validate.objectKeys( keysToLookFor, keys, ) if (!validKeys.length) { throw new E.InvalidParametersError( 'Cannot proceed to find a provider. Please use any of the following params:' + ' specialty, language, time, state', ) } // TODO: seems like backend is not accepteing more than 1 specialty query if (invalidKeys.length) { logError( `Warning: you tried to use "${invalidKeys.join(', ')} "` + 'as params but they are not used on the server.', ) } const constructURLArgs = ['/appointment/find_provider/?', params] const url = this._constructURL(...constructURLArgs) const response = await this.req.get(url) const pagination = get(response, `${dataPath}.pagination`, null) const data = get(response, `${dataPath}.data`, []) const normalizeArgs = [ 'findProviders', data, this.schemas.findProviders, ] const { findProviders: providers, ids: timeSlotIds } = utils._normalize( ...normalizeArgs, ) const result = { pagination, providers: providers || {}, timeSlotIds } return result } catch (error) { throw error } }, async findProviderAvailabilities( params: types.FindProviderAvailabilitiesParams, ): Promise<{ pagination: types.Pagination providers: types.FindProviderAvailabilityResult[] ids: string[] | number[] }> { try { const accountType = this.getCurrentAccountType() if (accountType !== 'patient') { throw new E.WrongAccountTypeError('Only patients can use this URI') } const keysToLookFor = ['specialty', 'language', 'time', 'state'] const keys = Object.keys(params) const { validKeys, invalidKeys } = validate.objectKeys( keysToLookFor, keys, ) if (!validKeys.length) { throw new E.InvalidParametersError( 'Cannot proceed to find a provider. Please use any of the following params:' + ' specialty, language, time, state', ) } if (invalidKeys.length) { logError( `Warning: you tried to use "${invalidKeys.join(', ')} "` + 'as params but they are not used on the server.', ) } this._validateParams('findProviderAvailabilities', params) const endpoint = '/appointment/find_provider_availability/?' const constructURLArgs = [endpoint, params] const url = this._constructURL(...constructURLArgs) const response = await this.req.get(url) const pagination = get(response, `${dataPath}.pagination`, null) const data = get(response, `${dataPath}.data`, []) const normalizeArgs = ['providers', data, this.schemas.providers] const { providers, ids } = utils._normalize(...normalizeArgs) const result = { pagination, providers: providers || {}, ids } return result } catch (error) { throw error } }, async forgotPassword( email: string, ): Promise<'success' | 'Email sent' | object | undefined> { try { const accountType = this.getCurrentAccountType() if (accountType === 'patient') { this._validateParams('forgotPassword', email, accountType) const resetFlowId = this.getTvResetFlowId() const username = email await this.tvClient.sendPasswordResetEmail(resetFlowId, username) return 'success' } else if (accountType === 'guest') { throw new E.WrongAccountTypeError( 'Guests cannot reset their password', ) } else { this._validateParams('forgotPassword', email, accountType) const url = `/account/${accountType}/forgot_password/` await this.req.post(url, { email }) return 'Email sent' } } catch (error) { const accountType = this.getCurrentAccountType() if (accountType === 'patient') { // } else { if (error.response && error.response.data) { const { message } = error.response.data const emailNotFound = /enter a valid email/i.test(message) const fieldWasBlank = /may not be blank/i.test(message) } } throw error } }, async forgotPasswordProvider(email: string): Promise<'success'> { try { const url = '/account/provider/forgot_password/' await this.req.post(url, { email }) return 'success' } catch (error) { throw error } }, async joinMeeting(id: string) { try { if (!id) { throw new E.InvalidParametersError( 'Cannot join the meeting without an ID', ) } const url = `/appointment/${id}/join_meeting/` const response = await this.req.get(url) return get(response, dataPath, null) } catch (error) { throw error } }, /** * @param { string } args[0] - Account type (patient, provider, etc) * @param { object|string } args[1] - * @param { object?|string? } args[2] - */ async login(...args: types.LoginArguments) { try { let options: types.LoginOptions = {} let method = 'post' let url let token let _dataPath = dataPath let tvAuth this._validateParams('login', args) const accountType = args[0] _store.accountType = accountType if (accountType === 'admin') return this.loginAsAdmin(args[1]) const usingToken = typeof args[1] === 'string' const usingCredentials = isPlainObject(args[0]) || isPlainObject(args[1]) if (usingToken) { const userId = args[1] token = args[2] method = 'get' url = `/account/${accountType}/${userId}/` this.setToken(token).setHeader('Authorization', `JWT ${token}`) } if (usingCredentials) { const data: any = args[1] // Patients if ('username' in data) { tvAuth = await this.initTruevault(data.username, data.password) options.tv_uid = tvAuth.tv_uid options.tv_token = tvAuth.access_token } else { options = data } url = `/account/${accountType}/login/` } const response = await this.req[method](url, options) if (usingCredentials) { token = response.data.result.token this.setToken(token).setHeader('Authorization', `JWT ${token}`) if (accountType === 'provider') { const { provider } = response.data.result if (provider.tv_api_key) this.setTvApiKey(provider.tv_api_key) // Create the truevault instance for the provider to access some patient methods await this.initTruevaultApiKey(provider.tv_api_key) _dataPath = `${dataPath}.provider` } } // Grab the main data from the response by lodash's path strategy const profile = get(response, _dataPath, null) return profile } catch (error) { throw error } }, async loginAsAdmin({ email, password, }: types.LoginWithEmailCreds): Promise<{ token: string }> { try { const url = `/account/admin/login/` const response = await this.req.post(url, { email, password }) const token = get(response, `${dataPath}.token`, null) this.setToken(token).setHeader('Authorization', `JWT ${token}`) return token } catch (err) { throw err } }, // Should init a .tvClient 1st, then proceed with backend async loginAsPatient(data: { tv_uid: string tv_access_token?: string tv_token?: string }) { try { const result: types.PatientFullBackend = { user: null, token: '', members: {}, } data.tv_token = data.tv_access_token || data.tv_token this._validateParams('loginAsPatient', data) const url = `/account/patient/login/` await this.initTruevaultAccessToken(data.tv_token) const response = await this.req.post(url, data) const { token, user, members } = response.data.result this.setToken(token) this.setHeader('Authorization', `JWT ${token}`) this.setAccountType('patient') const normalizeArgs = ['patients', members, this.schemas.patients] const normalizedData = utils._normalize(...normalizeArgs) result.user = user result.token = token result.members = normalizedData.patients return result } catch (error) { throw error } }, // TODO: merge with .login // Returns the profile of user // NOTE: THIS DOESN'T CREATE A TV CLIENT INSTANCE async loginWithJwtToken( accountType: types.AccountType, params: types.LoginWithJwtTokensParams, ) { try { const userId = params.userId || params.user_id const token = params.token this._validateParams('loginWithJwtToken', accountType, params) const url = `/account/${accountType}/${userId}/` this.setToken(token) this.setHeader('Authorization', `JWT ${token}`) this.setAccountType(accountType) const response = await this.req.get(url) const result = get(response, dataPath, null) return result } catch (error) { throw error } }, async queryAppointments({ patients, providers, status, start_time, end_time, filter: _filter, }: types.ConstructQueryAppointmentsUrlArgs) { try { const accountType = this.getCurrentAccountType() if (accountType !== 'patient') { throw new E.WrongAccountTypeError('Only patients can use this URI') } const result: types.FetchAppointmentsReturnObject = { appointments: {}, ids: [], pagination: { num_pages: 0, object_count: 0, page_index: 0, page_limit: 0, }, } const _options: types.ConstructQueryAppointmentsUrlArgs = {} if (status) _options.status = this._toClonedQueries('status', status) if (start_time) _options.start_time = start_time if (end_time) _options.end_time = end_time if (patients) { const args = ['patients', utils.ensureArray(patients)] _options.patients = this._toClonedQueries(...args) } if (providers) { const args = ['patients', utils.ensureArray(providers)] _options.providers = this._toClonedQueries(...args) } this._validateParams('queryAppointments', _options) const url = this._constructURL(`/appointment/`, _options) const response = await this.req.get(url) const pagination = get(response, `${dataPath}.pagination`, null) const data = get(response, `${dataPath}.data`, []) const normalizeArgs = ['appointments', data, this.schemas.appointments] const normalizedData = utils._normalize(...normalizeArgs) const { ids, appointments = {} } = normalizedData const appointmentsReducer = ( predicate: types.AppointmentsFilterFunc, ) => ( acc: types.NormalizedAppointments, appt: types.Appointment, id: string, ): types.NormalizedAppointments => { if (predicate(appt, id, appointments)) acc[id] = appt return acc } result.pagination = pagination result.ids = ids result.appointments = _filter ? reduce(appointments, appointmentsReducer(_filter), {}) : appointments return result } catch (error) { throw error } }, async resetPassword(key: string, password: string): Promise { try { const url = `/account/reset_password/` await this.req.post(url, { password, key }) return 'success' } catch (error) { throw error } }, async sendConfirmationEmail(email: string): Promise<'success'> { try { const url = '/account/resend_confirmation_email/' await this.req.get(url, { params: { email } }) return 'success' } catch (error) { throw error } }, req: { get: wrappedReq('get'), post: wrappedReq('post'), put: wrappedReq('put'), delete: wrappedReq('delete'), }, schemas: { appointments: new schema.Entity('appointments'), blobs: new schema.Entity('blobs', {}, { idAttribute: 'tv_blob_id' }), categories: new schema.Entity('categories', {}, { idAttribute: 'code' }), documents: new schema.Entity( 'documents', {}, { processStrategy: ({ document }) => { let doc if (document.allergies) doc = document.allergies else if (document.medications) doc = document.medications else doc = document return doc }, }, ), languages: new schema.Entity('languages', {}, { idAttribute: 'code' }), patients: new schema.Entity('patients', {}, { idAttribute: 'user_id' }), patientsDownsized: new schema.Entity( 'patientsDownsized', {}, { idAttribute: 'tv_uid' }, ), patientMembers: new schema.Entity( 'patientMembers', {}, { idAttribute: 'user_id' }, ), paymentProfile: new schema.Entity( 'paymentProfile', {}, { idAttribute: 'payment_profile_id' }, ), pharmacies: new schema.Entity('pharmacies', {}, { idAttribute: 'fax' }), providers: new schema.Entity('providers', {}, { idAttribute: 'user_id' }), staffingCompanies: new schema.Entity( 'staffingCompanies', {}, { idAttribute: 'user_id' }, ), reviews: new schema.Entity('reviews', {}, { idAttribute: 'appointment' }), specialties: new schema.Entity( 'specialties', {}, { idAttribute: 'code' }, ), findProviders: new schema.Entity( 'findProviders', {}, { idAttribute: 'time_slot_id' }, ), }, async sendPrescriptionToPharmacy( data: types.SendPrescriptionToPharmacyParams, ) { try { const url = '/prescription/send_fax/' // NOTE: Backend is using /v1 atm. Watch over this in case they switch to v2 without telling us const baseURL = `https://${opts.env}.aitmed.com/v1` const response = await this.req.post(url, data, { baseURL }) const result = get(response, dataPath, null) return result } catch (error) { throw error } }, async sendSmsVerificationCode(phoneNum: string) { try { const headers = this.getCurrentHeaders() || {} let authValue if ('Authorization' in headers) { authValue = headers.Authorization this.removeHeader('Authorization') } const url = '/sms/send_verification_code/' const response = await this.req.post(url, { phone_number: phoneNum }) if (authValue) this.setHeader('Authorization', authValue) return get(response, 'data', null) } catch (error) { throw error } }, setAccountType(type: types.AccountType) { _store.accountType = type this.publish('accountType', _store.accountType) return this }, async setAvailabilityDates(dates: any) { try { const url = `/agenda/availability/` const data = { availability: dates } const response = await this.req.post(url, { data }) return get(response, dataPath, null) } catch (error) { throw error } }, async suspendProvider(userId: string): Promise { try { if (userId === undefined) { throw new E.MissingParametersError('userId is undefined') } const url = `/account/provider/${userId}/suspend/` const response = await this.req.get(url) return get(response, dataPath, null) } catch (error) { throw error } }, async uploadAbsenceDocument() {}, async updateAppointment( id: string, updates: types.UpdateAppointmentOptions, ) { try { const url = `/appointment/${id}/` const keysToLookFor = [ 'purpose', 'document_present_history_illness', 'document_provider_note', 'document_prescription', 'document_absence', 'document_legal_absence', ] const { validKeys, invalidKeys } = validate.objectKeys( keysToLookFor, updates, ) if (!validKeys.length) { throw new E.InvalidKeysError( 'Cannot update appointment. Please use one of the following to update: ' + 'purpose, document_present_history_illness, document_provider_note, ' + 'document_prescription, document_absence, document_legal_absence', ) } if (invalidKeys.length) { logError( `Warning: you tried to update "${invalidKeys.join(', ')} "` + 'but they cannot be updated in the database.', ) } const response = await this.req.put(url, updates) return get(response, dataPath, null) } catch (error) { if (error instanceof E.InvalidKeysError) { // } throw error } }, async updateAppointmentReview( params: types.AppointmentReview, ): Promise { try { const { appointment } = params const url = `/appointment_review/${appointment}/` this._validateParams('updateAppointmentReview', params) const response = await this.req.put(url, params) return get(response, dataPath, null) } catch (error) { throw error } }, async uploadLegalDocument() {}, async updateBackendPatient( userId: string, changes: types.UpdatePatientOptions, ): Promise { try { this._validateParams('updateBackendPatient', userId, changes) const url = `/account/patient/${userId}/` const response = await this.req.put(url, changes) return get(response, dataPath, null) } catch (error) { throw error } }, async updateChasePaymentProfile( id: string, changes: types.ChasePaymentProfileParams, ): Promise { try { const url = `/payment/chase_payment_profile/${id}/` const response = await this.req.put(url, changes) return get(response, dataPath, null) } catch (error) { throw error } }, async updatePatientDocument(docId: string, changes: any) { try { this._validateParams('updatePatientDocument', docId, changes) const vaultId = this.getTvVaultId() const args = [vaultId, docId, changes] const response = await this.tvClient.updateDocument(...args) const result = { id: response.id, ownerId: response.owner_id } return result } catch (error) { throw error } }, // id === blob id async uploadPatientDocument( appointmentId: string, documentName: string, blobId: string, ) { try { const url = `/appointment/${appointmentId}/` const params = { [documentName]: blobId } const response = await this.req.put(url, params) return get(response, dataPath, null) } catch (error) { throw error } }, async uploadPrescription() {}, async updateProvider( userId: string, options: types.UpdateProviderOptions, ): Promise { try { this._validateParams('updateProvider', userId, options) const url = `/account/provider/${userId}/` const response = await this.req.put(url, options) return get(response, 'data.data', null) } catch (error) { throw error } }, async updateProviderNotes() {}, // TODO: unit test updateSchema(name: string, ...changes: any[]) { let definition = {} let options if (name) { if (changes.length > 1) { definition = changes[0] options = changes[1] } else { options = changes[0] } // @ts-ignore this.schemas[name] = new schema.Entity(name, definition, options) } return this }, async updateStaffingCompanyProvider() {}, _constructURL(url: string, queries: types.ConstructURLItem[]) { let _url = utils.prepareForQueryParams(url) const keys = Object.keys(queries) forEach(keys, (key: any) => { const query = queries[key] if (!key || !query) return if (typeof query === 'function') return if (isArray(query)) { _url += `${key}=${query.join(',')}&` } else _url += `${key}=${String(query)}&` }) return utils.trimAmpersand(_url) }, // Pass in the result value to this._constructURL inside the object of the 2nd argument _toClonedQueries(key: string, queries: string[] | number[]): string { let str = '' queries.forEach((query: string | number, index: number) => { if (index === 0) str += `${String(query)}&` else if (index > 0) str += `${key}=${String(query)}&` }) return utils.trimAmpersand(str) }, _validateParams(methodName: string, ...args: any[]) { // @ts-ignore return errorHelpers.params[methodName](this, ...args) }, async validateInviterCode(code: string) { try { const url = `/account/patient/${code}/verify_inviter_code/` const response = await this.req.get(url) return get(response, dataPath, null) } catch (error) { throw error } }, async verifyPaymentCode(code: string) { try { const url = '/payment/charge/verify_payment_code/' const data = { payment_code: code } const response = await this.req.post(url, data) return get(response, dataPath, null) } catch (error) { throw error } }, } } // UNCOMMENT BELOW TO TEST THESE APIS IF NEEDED // const accountId = 'd58f0fea-b8f5-414a-8514-e25251ba8a7b' // const registerToken = 'cbab5f27-0676-4ea4-b828-aad2113f9975' // const groupIds = '7ddf61f2-d13f-47b8-9ecd-69bfb4e44a6b' // const api = aitmedApi({ // env: 'testapi', // tvKeys: { // dev: { // accountId, // groupIds, // registerToken, // passwordResetFlowId: 'd81dd323-0ba3-493a-9653-218733175ec9', // scopedAccessToken: // 'c2F0LTY5Mjk2NWY2LTQ2YWYtNDkyOC04M2MwLWQwZGQxYWM1YzNhZDplb2gxbEliNjVXenkwbEFVMGJFMjRFWmcyNWJLTlp0ZFVvMDlTeVIwV1o4', // vaultId: '79c8079b-250c-444d-bd52-2f46855db234', // }, // }, // baseURL: 'https://testapi.aitmed.com/v2', // }) // api.account.provider // .auth({ // phone_number: '+1 6262468491', // password: '14225111', // }) // .then((result: any) => { // // return api.account.provider.fetchBankAccount() // return api.account.provider.fetchDocument('profile_photo') // }) // .then(console.log) // .catch(console.error) export default aitmedApi