@oystehr/sdk 4.3.2 → 4.3.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/cjs/client/client.cjs +87 -13
- package/dist/cjs/client/client.cjs.map +1 -1
- package/dist/cjs/client/client.d.ts +3 -0
- package/dist/cjs/config.d.ts +19 -0
- package/dist/cjs/index.min.cjs +1 -1
- package/dist/cjs/index.min.cjs.map +1 -1
- package/dist/cjs/logger.cjs +36 -0
- package/dist/cjs/logger.cjs.map +1 -0
- package/dist/cjs/logger.d.ts +11 -0
- package/dist/cjs/resources/classes/erx.cjs +8 -0
- package/dist/cjs/resources/classes/erx.cjs.map +1 -1
- package/dist/cjs/resources/classes/erx.d.ts +7 -1
- package/dist/cjs/resources/classes/fhir-ext.cjs +290 -19
- package/dist/cjs/resources/classes/fhir-ext.cjs.map +1 -1
- package/dist/cjs/resources/classes/index.cjs +9 -2
- package/dist/cjs/resources/classes/index.cjs.map +1 -1
- package/dist/cjs/resources/classes/index.d.ts +1 -1
- package/dist/cjs/resources/classes/rcm.cjs +18 -0
- package/dist/cjs/resources/classes/rcm.cjs.map +1 -1
- package/dist/cjs/resources/classes/rcm.d.ts +22 -1
- package/dist/cjs/resources/classes/user.cjs +9 -0
- package/dist/cjs/resources/classes/user.cjs.map +1 -1
- package/dist/cjs/resources/classes/user.d.ts +8 -1
- package/dist/cjs/resources/types/ErxGetMedicationHistoryParams.d.ts +3 -0
- package/dist/cjs/resources/types/ErxGetMedicationHistoryResponse.d.ts +43 -0
- package/dist/cjs/resources/types/ErxGetMedicationResponse.d.ts +1 -1
- package/dist/cjs/resources/types/RcmGetPayerParams.d.ts +3 -0
- package/dist/cjs/resources/types/RcmGetPayerResponse.d.ts +5 -0
- package/dist/cjs/resources/types/RcmListPayersParams.d.ts +9 -0
- package/dist/cjs/resources/types/RcmListPayersResponse.d.ts +13 -0
- package/dist/cjs/resources/types/UserChangePasswordParams.d.ts +7 -0
- package/dist/cjs/resources/types/fhir.d.ts +13 -3
- package/dist/cjs/resources/types/index.d.ts +13 -6
- package/dist/esm/client/client.d.ts +3 -0
- package/dist/esm/client/client.js +88 -15
- package/dist/esm/client/client.js.map +1 -1
- package/dist/esm/config.d.ts +19 -0
- package/dist/esm/index.min.js +1 -1
- package/dist/esm/index.min.js.map +1 -1
- package/dist/esm/logger.d.ts +11 -0
- package/dist/esm/logger.js +34 -0
- package/dist/esm/logger.js.map +1 -0
- package/dist/esm/resources/classes/erx.d.ts +7 -1
- package/dist/esm/resources/classes/erx.js +8 -0
- package/dist/esm/resources/classes/erx.js.map +1 -1
- package/dist/esm/resources/classes/fhir-ext.js +290 -19
- package/dist/esm/resources/classes/fhir-ext.js.map +1 -1
- package/dist/esm/resources/classes/index.d.ts +1 -1
- package/dist/esm/resources/classes/index.js +9 -2
- package/dist/esm/resources/classes/index.js.map +1 -1
- package/dist/esm/resources/classes/rcm.d.ts +22 -1
- package/dist/esm/resources/classes/rcm.js +18 -0
- package/dist/esm/resources/classes/rcm.js.map +1 -1
- package/dist/esm/resources/classes/user.d.ts +8 -1
- package/dist/esm/resources/classes/user.js +9 -0
- package/dist/esm/resources/classes/user.js.map +1 -1
- package/dist/esm/resources/types/ErxGetMedicationHistoryParams.d.ts +3 -0
- package/dist/esm/resources/types/ErxGetMedicationHistoryResponse.d.ts +43 -0
- package/dist/esm/resources/types/ErxGetMedicationResponse.d.ts +1 -1
- package/dist/esm/resources/types/RcmGetPayerParams.d.ts +3 -0
- package/dist/esm/resources/types/RcmGetPayerResponse.d.ts +5 -0
- package/dist/esm/resources/types/RcmListPayersParams.d.ts +9 -0
- package/dist/esm/resources/types/RcmListPayersResponse.d.ts +13 -0
- package/dist/esm/resources/types/UserChangePasswordParams.d.ts +7 -0
- package/dist/esm/resources/types/fhir.d.ts +13 -3
- package/dist/esm/resources/types/index.d.ts +13 -6
- package/package.json +1 -1
- package/src/client/client.ts +95 -15
- package/src/config.ts +20 -0
- package/src/logger.ts +36 -0
- package/src/resources/classes/erx.ts +17 -0
- package/src/resources/classes/fhir-ext.ts +322 -19
- package/src/resources/classes/index.ts +9 -2
- package/src/resources/classes/rcm.ts +39 -0
- package/src/resources/classes/user.ts +10 -0
- package/src/resources/types/ErxGetMedicationHistoryParams.ts +5 -0
- package/src/resources/types/ErxGetMedicationHistoryResponse.ts +45 -0
- package/src/resources/types/ErxGetMedicationResponse.ts +1 -1
- package/src/resources/types/RcmGetPayerParams.ts +5 -0
- package/src/resources/types/RcmGetPayerResponse.ts +7 -0
- package/src/resources/types/RcmListPayersParams.ts +11 -0
- package/src/resources/types/RcmListPayersResponse.ts +15 -0
- package/src/resources/types/UserChangePasswordParams.ts +9 -0
- package/src/resources/types/fhir.ts +16 -0
- package/src/resources/types/index.ts +13 -6
package/src/client/client.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { v4 as uuidv4 } from 'uuid';
|
|
1
|
+
import { v4 as uuidv4, validate as uuidValidate } from 'uuid';
|
|
2
2
|
import { OystehrConfig } from '../config';
|
|
3
3
|
import { OystehrFHIRError, OystehrSdkError } from '../errors';
|
|
4
|
+
import { Logger } from '../logger';
|
|
4
5
|
import { FhirBundle, FhirResource, OperationOutcome } from '../resources/types';
|
|
5
6
|
|
|
6
7
|
type HttpMethod = 'get' | 'put' | 'post' | 'delete' | 'options' | 'head' | 'patch' | 'trace';
|
|
@@ -51,17 +52,24 @@ export type FhirFetcherResponse<T extends FhirData<FhirResource> = any> = T;
|
|
|
51
52
|
|
|
52
53
|
export class SDKResource {
|
|
53
54
|
protected readonly config: OystehrConfig;
|
|
55
|
+
protected readonly logger: Logger;
|
|
54
56
|
constructor(config: OystehrConfig) {
|
|
55
57
|
this.config = config;
|
|
58
|
+
this.logger = new Logger({
|
|
59
|
+
level: this.config.logLevel,
|
|
60
|
+
});
|
|
56
61
|
}
|
|
57
62
|
|
|
58
63
|
protected request(path: string, method: string, baseUrlThunk: () => string): FetcherFunction {
|
|
59
64
|
return async (params: any, request?: InternalClientRequest): Promise<FetcherResponse> => {
|
|
60
65
|
const configThunk = (): OystehrConfig => this.config;
|
|
66
|
+
const loggerThunk = (): Logger => this.logger;
|
|
61
67
|
try {
|
|
62
|
-
|
|
68
|
+
// must await here to catch
|
|
69
|
+
return await fetcher(baseUrlThunk, configThunk, loggerThunk, path, method)(params, request);
|
|
63
70
|
} catch (err: any) {
|
|
64
71
|
const error = err as { message: string; code: number; cause?: unknown };
|
|
72
|
+
this.logger.error(error.message, { code: error.code, cause: error.cause });
|
|
65
73
|
throw new OystehrSdkError({ message: error.message, code: error.code, cause: error.cause });
|
|
66
74
|
}
|
|
67
75
|
};
|
|
@@ -72,8 +80,9 @@ export class SDKResource {
|
|
|
72
80
|
try {
|
|
73
81
|
const baseUrlThunk = (): string => this.config.services?.fhirApiUrl ?? defaultFhirApiUrl;
|
|
74
82
|
const configThunk = (): OystehrConfig => this.config;
|
|
83
|
+
const loggerThunk = (): Logger => this.logger;
|
|
75
84
|
// must await here to catch
|
|
76
|
-
return await fetcher(baseUrlThunk, configThunk, path, method)(params, request);
|
|
85
|
+
return await fetcher(baseUrlThunk, configThunk, loggerThunk, path, method)(params, request);
|
|
77
86
|
} catch (err: unknown) {
|
|
78
87
|
// FHIR API error messages are JSON strings
|
|
79
88
|
const fullError = err as { message: string | Record<string, any>; code: number; cause?: unknown };
|
|
@@ -101,7 +110,13 @@ export type FetcherFunction = (
|
|
|
101
110
|
) => Promise<FetcherResponse>;
|
|
102
111
|
|
|
103
112
|
function isInternalClientRequest(request: Record<string, any>): request is InternalClientRequest {
|
|
104
|
-
return
|
|
113
|
+
return (
|
|
114
|
+
'accessToken' in request ||
|
|
115
|
+
('projectId' in request && uuidValidate(request.projectId)) ||
|
|
116
|
+
('contentType' in request && request.contentType?.split('/').length === 2) ||
|
|
117
|
+
'requestId' in request ||
|
|
118
|
+
('ifMatch' in request && request.ifMatch.startsWith('W/"'))
|
|
119
|
+
);
|
|
105
120
|
}
|
|
106
121
|
|
|
107
122
|
/**
|
|
@@ -130,6 +145,7 @@ function parseXmlResponse(xmlString: string): Record<string, unknown> | null {
|
|
|
130
145
|
function fetcher(
|
|
131
146
|
baseUrlThunk: () => string,
|
|
132
147
|
configThunk: () => OystehrConfig,
|
|
148
|
+
loggerThunk: () => Logger,
|
|
133
149
|
path: string,
|
|
134
150
|
methodParam: string
|
|
135
151
|
): FetcherFunction {
|
|
@@ -141,19 +157,21 @@ function fetcher(
|
|
|
141
157
|
// or fetcher(baseUrl, path, method)(params) or fetcher(baseUrl, path, method)(). the types for this are handled by Client<Path, Methods>
|
|
142
158
|
// and this is the backend implementation behind it. the heuristic we're using is that if the first param is an object with an accessToken
|
|
143
159
|
// and there is no second param, assume the first one is the request object instead
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
const requestCtx =
|
|
149
|
-
!!params && !request && !Array.isArray(params) && isInternalClientRequest(params)
|
|
150
|
-
? (params as InternalClientRequest)
|
|
151
|
-
: request;
|
|
152
|
-
const method = methodParam.toLowerCase() as HttpMethod;
|
|
160
|
+
// eslint-disable-next-line prefer-const
|
|
161
|
+
let [providedParams, requestCtx] = extractParamsAndRequest(params, request);
|
|
162
|
+
requestCtx ??= {};
|
|
163
|
+
requestCtx.requestId ??= uuidv4();
|
|
153
164
|
const config = configThunk();
|
|
165
|
+
const logger = loggerThunk();
|
|
166
|
+
const method = methodParam.toLowerCase() as HttpMethod;
|
|
167
|
+
logger.debug('Beginning fetch', {
|
|
168
|
+
method,
|
|
169
|
+
path,
|
|
170
|
+
requestId: requestCtx?.requestId,
|
|
171
|
+
});
|
|
154
172
|
const fetchImpl = config.fetch ?? fetch;
|
|
155
173
|
const accessToken = requestCtx?.accessToken ?? config.accessToken;
|
|
156
|
-
const projectId = requestCtx?.projectId ??
|
|
174
|
+
const projectId = requestCtx?.projectId ?? config.projectId;
|
|
157
175
|
let finalPath = path;
|
|
158
176
|
let finalParams = providedParams;
|
|
159
177
|
if (!Array.isArray(providedParams)) {
|
|
@@ -161,6 +179,11 @@ function fetcher(
|
|
|
161
179
|
finalPath = subbedPath;
|
|
162
180
|
finalParams = addlParams;
|
|
163
181
|
}
|
|
182
|
+
logger.debug('Substituted parameters in path', {
|
|
183
|
+
method,
|
|
184
|
+
path,
|
|
185
|
+
requestId: requestCtx?.requestId,
|
|
186
|
+
});
|
|
164
187
|
finalPath = finalPath.replace(/^\//, ''); // remove leading slash
|
|
165
188
|
const baseUrlEvaluated = baseUrlThunk();
|
|
166
189
|
const fullBaseUrl = baseUrlEvaluated.endsWith('/') ? baseUrlEvaluated : baseUrlEvaluated + '/';
|
|
@@ -185,6 +208,11 @@ function fetcher(
|
|
|
185
208
|
body = '{}';
|
|
186
209
|
}
|
|
187
210
|
}
|
|
211
|
+
logger.debug('Prepared request body', {
|
|
212
|
+
method,
|
|
213
|
+
path,
|
|
214
|
+
requestId: requestCtx?.requestId,
|
|
215
|
+
});
|
|
188
216
|
|
|
189
217
|
const headers: Record<string, string> = Object.assign(
|
|
190
218
|
projectId
|
|
@@ -198,7 +226,7 @@ function fetcher(
|
|
|
198
226
|
},
|
|
199
227
|
accessToken ? { Authorization: `Bearer ${accessToken}` } : {},
|
|
200
228
|
requestCtx?.ifMatch ? { 'If-Match': requestCtx.ifMatch } : {},
|
|
201
|
-
{ 'x-oystehr-request-id': requestCtx?.requestId
|
|
229
|
+
{ 'x-oystehr-request-id': requestCtx?.requestId }
|
|
202
230
|
);
|
|
203
231
|
const retryConfig: ConstructedRetryConfig = {
|
|
204
232
|
retries: config.retry?.retries ?? 3,
|
|
@@ -210,6 +238,12 @@ function fetcher(
|
|
|
210
238
|
};
|
|
211
239
|
retryConfig.retryOn.push(...STATUS_CODES_TO_RETRY);
|
|
212
240
|
return retry(async () => {
|
|
241
|
+
logger.info('Request start', {
|
|
242
|
+
method,
|
|
243
|
+
url,
|
|
244
|
+
requestId: requestCtx?.requestId,
|
|
245
|
+
});
|
|
246
|
+
const now = Date.now();
|
|
213
247
|
const response = await fetchImpl(
|
|
214
248
|
new Request(url, {
|
|
215
249
|
method: method.toUpperCase(),
|
|
@@ -217,6 +251,12 @@ function fetcher(
|
|
|
217
251
|
headers,
|
|
218
252
|
})
|
|
219
253
|
);
|
|
254
|
+
logger.info('Request end', {
|
|
255
|
+
method,
|
|
256
|
+
url,
|
|
257
|
+
duration: Date.now() - now,
|
|
258
|
+
requestId: requestCtx?.requestId,
|
|
259
|
+
});
|
|
220
260
|
const responseBody = response.body ? await response.text() : null;
|
|
221
261
|
let responseJson: Record<string, unknown> | null;
|
|
222
262
|
const contentType = response.headers.get('content-type');
|
|
@@ -225,10 +265,30 @@ function fetcher(
|
|
|
225
265
|
responseBody &&
|
|
226
266
|
(contentType?.includes('application/json') || contentType?.includes('application/fhir+json'))
|
|
227
267
|
) {
|
|
268
|
+
logger.time('Deserialized JSON response', {
|
|
269
|
+
method,
|
|
270
|
+
url,
|
|
271
|
+
requestId: requestCtx?.requestId,
|
|
272
|
+
});
|
|
228
273
|
responseJson = JSON.parse(responseBody);
|
|
274
|
+
logger.timeEnd('Deserialized JSON response', {
|
|
275
|
+
method,
|
|
276
|
+
url,
|
|
277
|
+
requestId: requestCtx?.requestId,
|
|
278
|
+
});
|
|
229
279
|
} else if (responseBody && (contentType?.includes('application/xml') || contentType?.includes('text/xml'))) {
|
|
230
280
|
// Parse XML response into { status, output } structure
|
|
281
|
+
logger.time('Deserialized XML response', {
|
|
282
|
+
method,
|
|
283
|
+
url,
|
|
284
|
+
requestId: requestCtx?.requestId,
|
|
285
|
+
});
|
|
231
286
|
responseJson = parseXmlResponse(responseBody);
|
|
287
|
+
logger.timeEnd('Deserialized XML response', {
|
|
288
|
+
method,
|
|
289
|
+
url,
|
|
290
|
+
requestId: requestCtx?.requestId,
|
|
291
|
+
});
|
|
232
292
|
} else {
|
|
233
293
|
responseJson = null;
|
|
234
294
|
}
|
|
@@ -236,6 +296,11 @@ function fetcher(
|
|
|
236
296
|
// ignore JSON.parse errors
|
|
237
297
|
responseJson = null;
|
|
238
298
|
}
|
|
299
|
+
logger.debug('Deserialized response', {
|
|
300
|
+
method,
|
|
301
|
+
url,
|
|
302
|
+
requestId: requestCtx?.requestId,
|
|
303
|
+
});
|
|
239
304
|
const isError = !response.ok || response.status >= 400;
|
|
240
305
|
if (isError) {
|
|
241
306
|
const errObj = {
|
|
@@ -349,3 +414,18 @@ export function addParamsToSearch(params: Record<string, unknown>, search: URLSe
|
|
|
349
414
|
search.append(key, value as string);
|
|
350
415
|
}
|
|
351
416
|
}
|
|
417
|
+
|
|
418
|
+
export function extractParamsAndRequest(
|
|
419
|
+
params?: Record<string, unknown> | [any] | InternalClientRequest,
|
|
420
|
+
request?: InternalClientRequest
|
|
421
|
+
): [Record<string, unknown>, InternalClientRequest | undefined] {
|
|
422
|
+
const providedParams: Record<string, unknown> | [any] =
|
|
423
|
+
!!params && !request && !Array.isArray(params) && isInternalClientRequest(params)
|
|
424
|
+
? {}
|
|
425
|
+
: (params as Record<string, unknown>) ?? {};
|
|
426
|
+
const requestCtx =
|
|
427
|
+
!!params && !request && !Array.isArray(params) && isInternalClientRequest(params)
|
|
428
|
+
? (params as InternalClientRequest)
|
|
429
|
+
: request;
|
|
430
|
+
return [providedParams, requestCtx];
|
|
431
|
+
}
|
package/src/config.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { Coding } from './resources/types/fhir';
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* Configuration for the Oystehr SDK client
|
|
3
5
|
*/
|
|
@@ -10,6 +12,10 @@ export interface OystehrConfig {
|
|
|
10
12
|
* Optional Oystehr Project ID. Required for developer accessTokens.
|
|
11
13
|
*/
|
|
12
14
|
projectId?: string;
|
|
15
|
+
/**
|
|
16
|
+
* Optional log level for the SDK logger. Possible values are 'error', 'info', and 'debug'.
|
|
17
|
+
*/
|
|
18
|
+
logLevel?: 'error' | 'info' | 'debug';
|
|
13
19
|
fhirApiUrl?: string;
|
|
14
20
|
projectApiUrl?: string;
|
|
15
21
|
services?: {
|
|
@@ -58,6 +64,20 @@ export interface OystehrConfig {
|
|
|
58
64
|
*/
|
|
59
65
|
retryOn?: number[];
|
|
60
66
|
};
|
|
67
|
+
/**
|
|
68
|
+
* Optional workspace tag configuration. When set, all FHIR searches will be
|
|
69
|
+
* filtered to only return resources with this tag, and all FHIR mutations
|
|
70
|
+
* (create, update, patch) will have this tag injected automatically.
|
|
71
|
+
* Mutually exclusive with `ignoreTags`.
|
|
72
|
+
*/
|
|
73
|
+
workspaceTag?: Coding;
|
|
74
|
+
/**
|
|
75
|
+
* Optional list of tags to ignore. When set, all FHIR searches will be
|
|
76
|
+
* filtered to exclude resources matching any of these tags, and mutations
|
|
77
|
+
* will throw an error if the resource carries any of these tags.
|
|
78
|
+
* Mutually exclusive with `workspaceTag`.
|
|
79
|
+
*/
|
|
80
|
+
ignoreTags?: Coding[];
|
|
61
81
|
}
|
|
62
82
|
|
|
63
83
|
let globalConfig: OystehrConfig;
|
package/src/logger.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export class Logger {
|
|
2
|
+
_level?: 'error' | 'info' | 'debug';
|
|
3
|
+
constructor({ level }: { level?: 'error' | 'info' | 'debug' }) {
|
|
4
|
+
this._level = level;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
error(message: string, extra: Record<string, any> = {}): void {
|
|
8
|
+
if (this._level && ['error', 'info', 'debug'].includes(this._level)) {
|
|
9
|
+
console.error(JSON.stringify({ message, ...extra }));
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
info(message: string, extra: Record<string, any> = {}): void {
|
|
14
|
+
if (this._level && ['info', 'debug'].includes(this._level)) {
|
|
15
|
+
console.info(JSON.stringify({ message, ...extra }));
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
debug(message: string, extra: Record<string, any> = {}): void {
|
|
20
|
+
if (this._level && ['debug'].includes(this._level)) {
|
|
21
|
+
console.debug(JSON.stringify({ message, ...extra }));
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
time(message: string, extra: Record<string, any> = {}): void {
|
|
26
|
+
if (this._level && ['debug'].includes(this._level)) {
|
|
27
|
+
console.time(JSON.stringify({ message, ...extra }));
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
timeEnd(message: string, extra: Record<string, any> = {}): void {
|
|
32
|
+
if (this._level && ['debug'].includes(this._level)) {
|
|
33
|
+
console.timeEnd(JSON.stringify({ message, ...extra }));
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -15,6 +15,8 @@ import {
|
|
|
15
15
|
ErxConnectPractitionerResponse,
|
|
16
16
|
ErxEnrollPractitionerParams,
|
|
17
17
|
ErxGetConfigurationResponse,
|
|
18
|
+
ErxGetMedicationHistoryParams,
|
|
19
|
+
ErxGetMedicationHistoryResponse,
|
|
18
20
|
ErxGetMedicationParams,
|
|
19
21
|
ErxGetMedicationResponse,
|
|
20
22
|
ErxSearchAllergensParams,
|
|
@@ -135,6 +137,21 @@ export class Erx extends SDKResource {
|
|
|
135
137
|
this.#baseUrlThunk.bind(this)
|
|
136
138
|
)(params, request);
|
|
137
139
|
}
|
|
140
|
+
/**
|
|
141
|
+
* Retrieve claims-based medication history for a patient. This endpoint requires the patient to be synced with the upstream eRx provider.
|
|
142
|
+
* Action: `eRx:GetMedicationHistory`
|
|
143
|
+
* Access Policy Resource: `eRx:Patient`
|
|
144
|
+
*/
|
|
145
|
+
getMedicationHistory(
|
|
146
|
+
params: ErxGetMedicationHistoryParams,
|
|
147
|
+
request?: OystehrClientRequest
|
|
148
|
+
): Promise<ErxGetMedicationHistoryResponse> {
|
|
149
|
+
return this.request(
|
|
150
|
+
'/patient/{patientId}/medication-history',
|
|
151
|
+
'get',
|
|
152
|
+
this.#baseUrlThunk.bind(this)
|
|
153
|
+
)(params, request);
|
|
154
|
+
}
|
|
138
155
|
/**
|
|
139
156
|
* Adds pharmacy for a patient with the upstream eRx provider, setting it as primary if specified.
|
|
140
157
|
* Action: `eRx:AddPatientPharmacy`
|