@crvouga/mockingbird-service-junction 0.1.0 → 0.1.2

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.
Files changed (62) hide show
  1. package/README.md +23 -11
  2. package/dist/index.d.ts +492 -21
  3. package/dist/index.js +6086 -107
  4. package/dist/index.js.map +7 -1
  5. package/package.json +11 -10
  6. package/dist/catalog.d.ts +0 -63
  7. package/dist/catalog.d.ts.map +0 -1
  8. package/dist/catalog.js +0 -736
  9. package/dist/catalog.js.map +0 -1
  10. package/dist/coverage-corpus.d.ts +0 -76
  11. package/dist/coverage-corpus.d.ts.map +0 -1
  12. package/dist/coverage-corpus.js +0 -166
  13. package/dist/coverage-corpus.js.map +0 -1
  14. package/dist/generated/openapi.d.ts +0 -7
  15. package/dist/generated/openapi.d.ts.map +0 -1
  16. package/dist/generated/openapi.js +0 -5
  17. package/dist/generated/openapi.js.map +0 -1
  18. package/dist/index.d.ts.map +0 -1
  19. package/dist/lab-accounts.d.ts +0 -38
  20. package/dist/lab-accounts.d.ts.map +0 -1
  21. package/dist/lab-accounts.js +0 -213
  22. package/dist/lab-accounts.js.map +0 -1
  23. package/dist/orders.d.ts +0 -18
  24. package/dist/orders.d.ts.map +0 -1
  25. package/dist/orders.js +0 -1247
  26. package/dist/orders.js.map +0 -1
  27. package/dist/prefetch.d.ts +0 -43
  28. package/dist/prefetch.d.ts.map +0 -1
  29. package/dist/prefetch.js +0 -131
  30. package/dist/prefetch.js.map +0 -1
  31. package/dist/reshape.d.ts +0 -22
  32. package/dist/reshape.d.ts.map +0 -1
  33. package/dist/reshape.js +0 -190
  34. package/dist/reshape.js.map +0 -1
  35. package/dist/results.d.ts +0 -19
  36. package/dist/results.d.ts.map +0 -1
  37. package/dist/results.js +0 -221
  38. package/dist/results.js.map +0 -1
  39. package/dist/scheduling.d.ts +0 -113
  40. package/dist/scheduling.d.ts.map +0 -1
  41. package/dist/scheduling.js +0 -1508
  42. package/dist/scheduling.js.map +0 -1
  43. package/dist/sealed-corpus.d.ts +0 -28
  44. package/dist/sealed-corpus.d.ts.map +0 -1
  45. package/dist/sealed-corpus.js +0 -17
  46. package/dist/sealed-corpus.js.map +0 -1
  47. package/dist/seed-from.d.ts +0 -27
  48. package/dist/seed-from.d.ts.map +0 -1
  49. package/dist/seed-from.js +0 -523
  50. package/dist/seed-from.js.map +0 -1
  51. package/dist/seeds.d.ts +0 -9
  52. package/dist/seeds.d.ts.map +0 -1
  53. package/dist/seeds.js +0 -9
  54. package/dist/seeds.js.map +0 -1
  55. package/dist/state.d.ts +0 -270
  56. package/dist/state.d.ts.map +0 -1
  57. package/dist/state.js +0 -448
  58. package/dist/state.js.map +0 -1
  59. package/dist/users.d.ts +0 -13
  60. package/dist/users.d.ts.map +0 -1
  61. package/dist/users.js +0 -669
  62. package/dist/users.js.map +0 -1
package/dist/orders.js DELETED
@@ -1,1247 +0,0 @@
1
- import { HttpError, jsonRes, opaqueToken, } from "@crvouga/mockingbird-service";
2
- import { effectiveBilling, LAB_ACCOUNT_STATUSES, renderLabAccount, selectLabAccount, TEAM_LAB_ACCOUNTS, } from "./lab-accounts.js";
3
- import { applySimulateTransition, cascadeCancelAppointments } from "./scheduling.js";
4
- import { deterministicUuid, MOCK_TEAM_ID } from "./state.js";
5
- const RESULT_TYPES = ["numeric", "range", "comment", "coded_value"];
6
- const INTERPRETATIONS = ["normal", "abnormal", "critical", "unknown"];
7
- const simulationFlagsOf = (context) => {
8
- const body = context.body;
9
- if (body.kind !== "json")
10
- return null;
11
- const value = body.value;
12
- if (typeof value !== "object" || value === null || Array.isArray(value))
13
- return null;
14
- const raw = value;
15
- const flags = {};
16
- if (typeof raw.interpretation === "string" &&
17
- INTERPRETATIONS.includes(raw.interpretation))
18
- flags.interpretation = raw.interpretation;
19
- if (Array.isArray(raw.result_types)) {
20
- const types = raw.result_types.filter((entry) => typeof entry === "string" && RESULT_TYPES.includes(entry));
21
- if (types.length > 0)
22
- flags.result_types = types;
23
- }
24
- if (typeof raw.has_missing_results === "boolean")
25
- flags.has_missing_results = raw.has_missing_results;
26
- return Object.keys(flags).length > 0 ? flags : null;
27
- };
28
- const aoeQuestion = (state, markerId, questionId) => {
29
- for (const test of state.listLabTests()) {
30
- for (const marker of test.markers ?? []) {
31
- if (marker.id !== markerId)
32
- continue;
33
- const aoe = marker.aoe;
34
- if (!aoe)
35
- return undefined;
36
- return aoe.questions?.find((question) => question.id === questionId) ?? undefined;
37
- }
38
- }
39
- return undefined;
40
- };
41
- function notFound(message) {
42
- throw new HttpError(404, { detail: message });
43
- }
44
- const queryInt = (context, name, fallback) => {
45
- const raw = context.query[name];
46
- if (raw === undefined)
47
- return fallback;
48
- if (typeof raw !== "string" || !/^-?\d+$/.test(raw))
49
- throw new HttpError(422, { detail: `${name} must be an integer` });
50
- return Number(raw);
51
- };
52
- const MARKERS_DEFAULT_SIZE = 50;
53
- const MARKERS_MAX_SIZE = 100;
54
- const markersBoundsError = (name, value, bound, limit) => ({
55
- type: bound === "ge" ? "greater_than_equal" : "less_than_equal",
56
- loc: ["query", name],
57
- msg: `Input should be ${bound === "ge" ? "greater than or equal to" : "less than or equal to"} ${limit}`,
58
- input: value,
59
- ctx: bound === "ge" ? { ge: limit } : { le: limit },
60
- });
61
- const parseMarkersInt = (raw, name, fallback) => {
62
- if (raw === undefined)
63
- return fallback;
64
- if (typeof raw !== "string" || !/^-?\d+$/.test(raw)) {
65
- throw new HttpError(422, {
66
- detail: [
67
- {
68
- type: "int_parsing",
69
- loc: ["query", name],
70
- msg: "Input should be a valid integer, unable to parse string as an integer",
71
- input: raw,
72
- },
73
- ],
74
- });
75
- }
76
- return Number(raw);
77
- };
78
- /**
79
- * Pagination mirrors the sandbox markers endpoints: `total` counts the returned page,
80
- * `pages` is ceil(overall/size) over the whole marker list, and page/size accept
81
- * 1..100 with 422s outside those bounds.
82
- */
83
- const paginateMarkers = (query, markers) => {
84
- const page = parseMarkersInt(query.page, "page", 1);
85
- const size = parseMarkersInt(query.size, "size", MARKERS_DEFAULT_SIZE);
86
- if (page < 1) {
87
- throw new HttpError(422, {
88
- detail: [markersBoundsError("page", String(page), "ge", 1)],
89
- });
90
- }
91
- if (size < 1 || size > MARKERS_MAX_SIZE) {
92
- throw new HttpError(422, {
93
- detail: [
94
- markersBoundsError("size", String(size), size < 1 ? "ge" : "le", size < 1 ? 1 : MARKERS_MAX_SIZE),
95
- ],
96
- });
97
- }
98
- const start = (page - 1) * size;
99
- const pageMarkers = markers.slice(start, start + size);
100
- const pages = Math.ceil(markers.length / size);
101
- return {
102
- markers: pageMarkers,
103
- total: pageMarkers.length,
104
- page,
105
- size,
106
- pages,
107
- };
108
- };
109
- const jsonObject = (context) => {
110
- const body = context.body;
111
- const value = body.kind === "json" ? body.value : undefined;
112
- if (typeof value !== "object" || value === null || Array.isArray(value)) {
113
- throw new HttpError(422, {
114
- detail: [
115
- {
116
- type: "model_attributes_type",
117
- loc: ["body"],
118
- msg: "Input should be a valid dictionary or object to extract fields from",
119
- input: value,
120
- },
121
- ],
122
- });
123
- }
124
- return value;
125
- };
126
- /** Pydantic `str_strip_whitespace`: trim every string value before validation and use. */
127
- const trimStrings = (value) => {
128
- if (typeof value === "string")
129
- return value.trim();
130
- if (Array.isArray(value))
131
- return value.map(trimStrings);
132
- if (typeof value === "object" && value !== null) {
133
- const out = {};
134
- for (const [key, entry] of Object.entries(value))
135
- out[key] = trimStrings(entry);
136
- return out;
137
- }
138
- return value;
139
- };
140
- const patientDetails = (input) => ({
141
- ...input,
142
- dob: typeof input.dob === "string" && /^\d{4}-\d{2}-\d{2}$/.test(input.dob)
143
- ? `${input.dob}T00:00:00+00:00`
144
- : input.dob,
145
- medical_proxy: null,
146
- race: null,
147
- ethnicity: null,
148
- sexual_orientation: null,
149
- gender_identity: null,
150
- });
151
- const patientAddress = (input) => ({
152
- ...input,
153
- second_line: null,
154
- access_notes: null,
155
- });
156
- const missingError = (loc, input) => ({
157
- type: "missing",
158
- loc,
159
- msg: "Field required",
160
- input,
161
- });
162
- const stringTypeError = (loc, value) => ({
163
- type: "string_type",
164
- loc,
165
- msg: "Input should be a valid string",
166
- input: value,
167
- });
168
- const stringTooShortError = (loc, value) => ({
169
- type: "string_too_short",
170
- loc,
171
- msg: "String should have at least 1 character",
172
- input: value,
173
- ctx: { min_length: 1 },
174
- });
175
- const patternError = (loc, value, pattern) => ({
176
- type: "string_pattern_mismatch",
177
- loc,
178
- msg: `String should match pattern '${pattern}'`,
179
- input: value,
180
- ctx: { pattern },
181
- });
182
- const stateError = (value) => ({
183
- type: "value_error",
184
- loc: ["body", "patient_address", "state"],
185
- msg: `Value error, Invalid state: ${value}`,
186
- input: value,
187
- ctx: { error: {} },
188
- });
189
- const phoneError = (loc, value) => ({
190
- type: "value_error",
191
- loc,
192
- msg: `Value error, Invalid phone number: ${value}`,
193
- input: value,
194
- ctx: { error: {} },
195
- });
196
- const isUuid = (value) => /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(value);
197
- const uuidError = (value, index, loc) => {
198
- const characters = Array.from(value);
199
- const invalidIndex = characters.findIndex((character) => !/^[0-9a-f-]$/i.test(character));
200
- const error = invalidIndex >= 0
201
- ? `invalid character: expected an optional prefix of \`urn:uuid:\` followed by [0-9a-fA-F-], found \`${characters[invalidIndex]}\` at ${invalidIndex + 1}`
202
- : `invalid length: expected length 32 for simple format, found ${value.replaceAll("-", "").length}`;
203
- return {
204
- type: "uuid_parsing",
205
- loc: loc ??
206
- (index === undefined
207
- ? ["body", "lab_account_id"]
208
- : ["body", "order_set", "lab_test_ids", index]),
209
- msg: `Input should be a valid UUID, ${error}`,
210
- input: value,
211
- ctx: { error },
212
- };
213
- };
214
- /** `GET /v3/lab_test/lab_account` query filters: Pydantic-shaped 422s, then exact match. */
215
- const labAccountIdFilter = (context) => {
216
- const raw = context.query.lab_account_id;
217
- if (raw === undefined)
218
- return null;
219
- if (typeof raw === "string" && isUuid(raw))
220
- return raw;
221
- throw new HttpError(422, {
222
- detail: [
223
- uuidError(typeof raw === "string" ? raw : String(raw), undefined, [
224
- "query",
225
- "lab_account_id",
226
- ]),
227
- ],
228
- });
229
- };
230
- const labAccountStatusFilter = (context) => {
231
- const raw = context.query.status;
232
- if (raw === undefined)
233
- return null;
234
- if (typeof raw === "string" && LAB_ACCOUNT_STATUSES.includes(raw)) {
235
- return raw;
236
- }
237
- throw new HttpError(422, {
238
- detail: [
239
- {
240
- type: "enum",
241
- loc: ["query", "status"],
242
- msg: "Input should be 'active', 'pending', 'suspended' or 'ready_to_launch'",
243
- input: raw,
244
- ctx: { expected: "'active', 'pending', 'suspended' or 'ready_to_launch'" },
245
- },
246
- ],
247
- });
248
- };
249
- const isValidPhone = (value) => {
250
- const digits = value.replace(/\D/g, "");
251
- if (digits.length < 10 || digits.length > 15)
252
- return false;
253
- if (/[a-zA-Z]/.test(value))
254
- return false;
255
- if (digits.length === 10 || digits.length === 11) {
256
- const area = digits.length === 11 ? digits.slice(1, 4) : digits.slice(0, 3);
257
- if (area[0] === "0" || area[0] === "1")
258
- return false;
259
- }
260
- return true;
261
- };
262
- const isValidName = (value) => {
263
- if (!/^[a-zA-Z0-9][a-zA-Z0-9 .,'-]*$/.test(value))
264
- return false;
265
- if (/\s\s/.test(value))
266
- return false;
267
- return true;
268
- };
269
- const nameError = (field, value) => ({
270
- type: "value_error",
271
- loc: ["body", "patient_details", field],
272
- msg: "Value error, The field must start with a letter (a-z or A-Z or 0-9) and can only contain -,. special characters, but cannot start with a space or hyphen.",
273
- input: value,
274
- ctx: { error: {} },
275
- });
276
- const pyIso = (value) => {
277
- const d = new Date(value);
278
- const iso = d.toISOString();
279
- const [date, time = ""] = iso.split("T");
280
- const match = time.match(/\.(\d+)Z$/);
281
- const ms = (match?.[1] ?? "000").padEnd(6, "0");
282
- return `${date}T${time.slice(0, 8)}.${ms}+00:00`;
283
- };
284
- const orderValidation = (body) => {
285
- const errors = [];
286
- const userId = body.user_id;
287
- if (userId === undefined)
288
- errors.push(missingError(["body", "user_id"], body));
289
- else if (typeof userId !== "string")
290
- errors.push(stringTypeError(["body", "user_id"], userId));
291
- else if (!isUuid(userId))
292
- errors.push(uuidError(userId));
293
- const labAccountId = body.lab_account_id;
294
- if (typeof labAccountId === "string" && labAccountId.trim().length === 0) {
295
- errors.push({
296
- type: "string_too_short",
297
- loc: ["body", "lab_account_id"],
298
- msg: "String should have at least 1 character",
299
- input: labAccountId,
300
- });
301
- }
302
- const os = body.order_set;
303
- const osWrongType = os !== undefined && (typeof os !== "object" || os === null || Array.isArray(os));
304
- if (osWrongType) {
305
- errors.push({
306
- type: "model_attributes_type",
307
- loc: ["body", "order_set"],
308
- msg: "Input should be a valid dictionary or object to extract fields from",
309
- input: os,
310
- });
311
- }
312
- else if (os !== undefined) {
313
- const o = os;
314
- const ids = o.lab_test_ids;
315
- if (ids === undefined)
316
- errors.push(missingError(["body", "order_set", "lab_test_ids"], o));
317
- else if (!Array.isArray(ids)) {
318
- errors.push({
319
- type: "list_type",
320
- loc: ["body", "order_set", "lab_test_ids"],
321
- msg: "Input should be a valid list",
322
- input: ids,
323
- });
324
- }
325
- else {
326
- ids.forEach((id, index) => {
327
- if (typeof id !== "string" || !isUuid(id))
328
- errors.push(uuidError(String(id), index));
329
- });
330
- }
331
- }
332
- const pd = body.patient_details;
333
- if (pd === undefined) {
334
- errors.push(missingError(["body", "patient_details"], body));
335
- }
336
- else if (typeof pd !== "object" || pd === null || Array.isArray(pd)) {
337
- errors.push({
338
- type: "model_attributes_type",
339
- loc: ["body", "patient_details"],
340
- msg: "Input should be a valid dictionary or object to extract fields from",
341
- input: pd,
342
- });
343
- }
344
- else {
345
- const p = pd;
346
- for (const field of ["first_name", "last_name", "dob", "gender", "phone_number", "email"]) {
347
- if (p[field] === undefined)
348
- errors.push(missingError(["body", "patient_details", field], p));
349
- else if (field === "gender" && p[field] === null) {
350
- errors.push({
351
- type: "value_error",
352
- loc: ["body", "patient_details", "gender"],
353
- msg: "Value error, gender cannot be None.",
354
- input: null,
355
- ctx: { error: {} },
356
- });
357
- }
358
- else if (field === "phone_number" && p[field] === null) {
359
- errors.push({
360
- type: "value_error",
361
- loc: ["body", "patient_details", "phone_number"],
362
- msg: "Value error, Phone number cannot be None",
363
- input: null,
364
- ctx: { error: {} },
365
- });
366
- }
367
- else if (field === "dob" && typeof p[field] === "number") {
368
- const asSeconds = Math.abs(p[field]) < 1e12;
369
- const normalized = new Date(p[field] * (asSeconds ? 1000 : 1))
370
- .toISOString()
371
- .replace(/\.\d{3}Z$/, "+00:00")
372
- .replace(/Z$/, "+00:00");
373
- errors.push({
374
- type: "date_from_datetime_inexact",
375
- loc: ["body", "patient_details", "dob"],
376
- msg: "Datetimes provided to dates should have zero time - e.g. be exact dates",
377
- input: normalized,
378
- });
379
- }
380
- else if (field === "dob" && (p[field] === null || typeof p[field] === "object")) {
381
- errors.push({
382
- type: "value_error",
383
- loc: ["body", "patient_details", "dob"],
384
- msg: "Value error, date of birth is not specified.",
385
- input: p[field],
386
- ctx: { error: {} },
387
- });
388
- }
389
- else if (typeof p[field] !== "string")
390
- errors.push(stringTypeError(["body", "patient_details", field], p[field]));
391
- else if ((field === "first_name" || field === "last_name") &&
392
- typeof p[field] === "string" &&
393
- p[field].length === 0)
394
- errors.push(stringTooShortError(["body", "patient_details", field], p[field]));
395
- else if ((field === "first_name" || field === "last_name") &&
396
- !isValidName(p[field]))
397
- errors.push(nameError(field, p[field]));
398
- else if (field === "email") {
399
- const emailError = userEmailError(p[field]);
400
- if (emailError)
401
- errors.push(emailError);
402
- }
403
- else if (field === "dob") {
404
- const dobError = orderDobError(p[field]);
405
- if (dobError)
406
- errors.push(dobError);
407
- }
408
- else if (field === "phone_number" && !isValidPhone(p[field])) {
409
- errors.push(phoneError(["body", "patient_details", "phone_number"], p[field]));
410
- }
411
- }
412
- }
413
- const pa = body.patient_address;
414
- if (pa === undefined) {
415
- errors.push(missingError(["body", "patient_address"], body));
416
- }
417
- else if (typeof pa !== "object" || pa === null || Array.isArray(pa)) {
418
- errors.push({
419
- type: "model_attributes_type",
420
- loc: ["body", "patient_address"],
421
- msg: "Input should be a valid dictionary or object to extract fields from",
422
- input: pa,
423
- });
424
- }
425
- else {
426
- const a = pa;
427
- for (const field of ["first_line", "city", "state", "zip", "country"]) {
428
- if (a[field] === undefined)
429
- errors.push(missingError(["body", "patient_address", field], a));
430
- else if (typeof a[field] !== "string")
431
- errors.push(stringTypeError(["body", "patient_address", field], a[field]));
432
- }
433
- if (typeof a.state === "string" && !/^[A-Z]{2}$/.test(a.state)) {
434
- errors.push(stateError(a.state));
435
- }
436
- if (typeof a.zip === "string" && !/^\d{5}(-\d{4})?$/.test(a.zip)) {
437
- errors.push(patternError(["body", "patient_address", "zip"], a.zip, "^\\d{5}(-\\d{4})?$"));
438
- }
439
- if (typeof a.phone_number === "string" && !isValidPhone(a.phone_number)) {
440
- errors.push(phoneError(["body", "patient_address", "phone_number"], a.phone_number));
441
- }
442
- }
443
- return errors;
444
- };
445
- /** Sandbox email validation for `patient_details.email` (only the missing-@ sign case is reachable here). */
446
- const userEmailError = (value) => {
447
- if (!value.includes("@")) {
448
- return {
449
- type: "value_error",
450
- loc: ["body", "patient_details", "email"],
451
- msg: "value is not a valid email address: An email address must have an @-sign.",
452
- input: value,
453
- ctx: { reason: "An email address must have an @-sign." },
454
- };
455
- }
456
- return undefined;
457
- };
458
- /**
459
- * Sandbox dob validation for create_order: many string formats are accepted
460
- * (`1990-01-01`, `1990-01`, `1990/01/01`, `19900101`, ...); exact zero-time datetimes are
461
- * accepted, non-zero times produce `date_from_datetime_inexact` with a normalized input,
462
- * and unmatchable strings produce the "Could not match" value error.
463
- */
464
- const orderDobError = (dob) => {
465
- if (/^\d{4}-\d{2}-\d{2}T/.test(dob)) {
466
- if (/^\d{4}-\d{2}-\d{2}T00:00:00/.test(dob)) {
467
- const dateOnly = dob.slice(0, 10);
468
- if (new Date(`${dateOnly}T00:00:00Z`).getTime() > Date.now()) {
469
- return {
470
- type: "value_error",
471
- loc: ["body", "patient_details", "dob"],
472
- msg: "Value error, dob cannot be in the future",
473
- input: `${dateOnly}T00:00:00+00:00`,
474
- ctx: { error: {} },
475
- };
476
- }
477
- return undefined;
478
- }
479
- return {
480
- type: "date_from_datetime_inexact",
481
- loc: ["body", "patient_details", "dob"],
482
- msg: "Datetimes provided to dates should have zero time - e.g. be exact dates",
483
- input: pyIso(dob),
484
- };
485
- }
486
- if (/^\d{4}-\d{2}-\d{2}$/.test(dob)) {
487
- const [year = "0", month = "0", day = "0"] = dob.split("-");
488
- const monthNumber = Number(month);
489
- const dayNumber = Number(day);
490
- if (monthNumber < 1 || monthNumber > 12) {
491
- return {
492
- type: "value_error",
493
- loc: ["body", "patient_details", "dob"],
494
- msg: `Value error, month must be in 1..12, not ${monthNumber}`,
495
- input: dob,
496
- ctx: { error: {} },
497
- };
498
- }
499
- const daysInMonth = new Date(Date.UTC(Number(year), monthNumber, 0)).getUTCDate();
500
- if (dayNumber < 1 || dayNumber > daysInMonth) {
501
- return {
502
- type: "value_error",
503
- loc: ["body", "patient_details", "dob"],
504
- msg: `Value error, day ${dayNumber} must be in range 1..${daysInMonth} for month ${monthNumber} in year ${year}`,
505
- input: dob,
506
- ctx: { error: {} },
507
- };
508
- }
509
- if (new Date(`${dob}T00:00:00Z`).getTime() > Date.now()) {
510
- return {
511
- type: "value_error",
512
- loc: ["body", "patient_details", "dob"],
513
- msg: "Value error, dob cannot be in the future",
514
- input: `${dob}T00:00:00+00:00`,
515
- ctx: { error: {} },
516
- };
517
- }
518
- return undefined;
519
- }
520
- return {
521
- type: "value_error",
522
- loc: ["body", "patient_details", "dob"],
523
- msg: "Value error, Could not match input '" +
524
- dob +
525
- "' to any of the following formats: YYYY-MM-DD, YYYY-M-DD, YYYY-M-D, YYYY/MM/DD, YYYY/M/DD, YYYY/M/D, YYYY.MM.DD, YYYY.M.DD, YYYY.M.D, YYYYMMDD, YYYY-DDDD, YYYYDDDD, YYYY-MM, YYYY/MM, YYYY.MM, YYYY, W.",
526
- input: dob,
527
- ctx: { error: {} },
528
- };
529
- };
530
- const aoeValidationError = (state, body) => {
531
- const answers = body.aoe_answers;
532
- if (!Array.isArray(answers))
533
- return undefined;
534
- for (const answer of answers) {
535
- if (typeof answer !== "object" || answer === null || Array.isArray(answer))
536
- continue;
537
- const record = answer;
538
- if (typeof record.marker_id !== "number")
539
- continue;
540
- const marker = state
541
- .listLabTests()
542
- .flatMap((test) => test.markers ?? [])
543
- .find((entry) => entry.id === record.marker_id);
544
- if (!marker?.aoe)
545
- return `Marker id ${record.marker_id} does not have AOE.`;
546
- const question = aoeQuestion(state, record.marker_id, record.question_id);
547
- if (!question)
548
- return `Invalid question_id ${String(record.question_id)} for marker id ${record.marker_id}.`;
549
- const answerCode = typeof record.answer === "string" ? record.answer.toUpperCase() : "";
550
- if (!question.answers.some((entry) => entry.code === answerCode))
551
- return `Invalid answer ${answerCode} for question ${question.value}.`;
552
- }
553
- return undefined;
554
- };
555
- const METHOD_DETAILS = (method, id, at) => {
556
- switch (method) {
557
- case "testkit":
558
- return {
559
- type: "testkit",
560
- data: { id, shipment: null, created_at: at, updated_at: at },
561
- };
562
- case "walk_in_test":
563
- case "on_site_collection":
564
- return {
565
- type: method,
566
- data: { id, appointment_id: null, created_at: at, updated_at: at },
567
- };
568
- default:
569
- return {
570
- type: method,
571
- data: { id, appointment_id: null, created_at: at, updated_at: at },
572
- };
573
- }
574
- };
575
- const PHYSICIAN = { first_name: "Leo", last_name: "Damasco", npi: "1134326366" };
576
- /** Sandbox Labcorp rejects these patient states (`Lab Labcorp does not support state XX`). */
577
- const LAB_UNSUPPORTED_STATES = {
578
- labcorp: ["NY", "NJ", "RI"],
579
- };
580
- /** Low-level events that Junction allows transitioning to `cancelled.*.cancelled`. */
581
- const CANCELLABLE_ORDER_EVENTS = new Set([
582
- "received.walk_in_test.ordered",
583
- "received.walk_in_test.requisition_created",
584
- "received.walk_in_test.requisition_bypassed",
585
- "collecting_sample.walk_in_test.appointment_scheduled",
586
- "collecting_sample.walk_in_test.appointment_cancelled",
587
- "collecting_sample.walk_in_test.appointment_pending",
588
- "received.at_home_phlebotomy.ordered",
589
- "received.at_home_phlebotomy.requisition_created",
590
- "received.at_home_phlebotomy.requisition_bypassed",
591
- "collecting_sample.at_home_phlebotomy.appointment_pending",
592
- "collecting_sample.at_home_phlebotomy.appointment_scheduled",
593
- "collecting_sample.at_home_phlebotomy.appointment_cancelled",
594
- "collecting_sample.at_home_phlebotomy.draw_completed",
595
- "received.testkit.ordered",
596
- "received.testkit.awaiting_registration",
597
- "received.testkit.requisition_created",
598
- "received.testkit.requisition_bypassed",
599
- "received.testkit.testkit_registered",
600
- "received.on_site_collection.ordered",
601
- "received.on_site_collection.requisition_created",
602
- "received.on_site_collection.requisition_bypassed",
603
- "collecting_sample.on_site_collection.draw_completed",
604
- ]);
605
- const labStateSupportError = (labTest, state) => {
606
- if (typeof state !== "string" || state.length === 0)
607
- return undefined;
608
- const lab = labTest.lab;
609
- if (!lab || typeof lab !== "object")
610
- return undefined;
611
- const slug = typeof lab.slug === "string" ? lab.slug.toLowerCase() : "";
612
- const name = typeof lab.name === "string" ? lab.name : "Lab";
613
- const unsupported = LAB_UNSUPPORTED_STATES[slug];
614
- if (!unsupported)
615
- return undefined;
616
- const normalized = state.trim().toUpperCase();
617
- if (!unsupported.includes(normalized))
618
- return undefined;
619
- return `Lab ${name} does not support state ${normalized}`;
620
- };
621
- const FINAL_STATUSES = [
622
- "received.walk_in_test.ordered",
623
- "received.walk_in_test.requisition_created",
624
- "received.walk_in_test.requisition_bypassed",
625
- "completed.walk_in_test.completed",
626
- "sample_with_lab.walk_in_test.partial_results",
627
- "failed.walk_in_test.sample_error",
628
- "cancelled.walk_in_test.cancelled",
629
- "collecting_sample.walk_in_test.appointment_pending",
630
- "collecting_sample.walk_in_test.appointment_scheduled",
631
- "collecting_sample.walk_in_test.appointment_cancelled",
632
- "collecting_sample.walk_in_test.redraw_available",
633
- "received.at_home_phlebotomy.ordered",
634
- "received.at_home_phlebotomy.requisition_created",
635
- "received.at_home_phlebotomy.requisition_bypassed",
636
- "collecting_sample.at_home_phlebotomy.appointment_pending",
637
- "collecting_sample.at_home_phlebotomy.appointment_scheduled",
638
- "collecting_sample.at_home_phlebotomy.draw_completed",
639
- "collecting_sample.at_home_phlebotomy.appointment_cancelled",
640
- "completed.at_home_phlebotomy.completed",
641
- "sample_with_lab.at_home_phlebotomy.partial_results",
642
- "cancelled.at_home_phlebotomy.cancelled",
643
- "failed.at_home_phlebotomy.sample_error",
644
- "received.testkit.ordered",
645
- "received.testkit.awaiting_registration",
646
- "received.testkit.requisition_created",
647
- "received.testkit.requisition_bypassed",
648
- "received.testkit.registered",
649
- "collecting_sample.testkit.transit_customer",
650
- "collecting_sample.testkit.out_for_delivery",
651
- "collecting_sample.testkit.with_customer",
652
- "collecting_sample.testkit.transit_lab",
653
- "sample_with_lab.testkit.delivered_to_lab",
654
- "sample_with_lab.testkit.lab_processing_blocked",
655
- "completed.testkit.completed",
656
- "failed.testkit.failure_to_deliver_to_customer",
657
- "failed.testkit.failure_to_deliver_to_lab",
658
- "failed.testkit.sample_error",
659
- "failed.testkit.lost",
660
- "cancelled.testkit.cancelled",
661
- "cancelled.testkit.do_not_process",
662
- "collecting_sample.testkit.problem_in_transit_customer",
663
- "collecting_sample.testkit.problem_in_transit_lab",
664
- "received.on_site_collection.ordered",
665
- "received.on_site_collection.requisition_created",
666
- "received.on_site_collection.requisition_bypassed",
667
- "sample_with_lab.on_site_collection.draw_completed",
668
- "completed.on_site_collection.completed",
669
- "cancelled.on_site_collection.cancelled",
670
- "sample_with_lab.on_site_collection.partial_results",
671
- "failed.on_site_collection.sample_error",
672
- "completed.walk_in_test.corrected",
673
- "completed.at_home_phlebotomy.corrected",
674
- "completed.on_site_collection.corrected",
675
- "completed.testkit.corrected",
676
- ];
677
- const finalStatusError = (value) => ({
678
- type: "enum",
679
- loc: ["query", "final_status"],
680
- msg: `Input should be ${FINAL_STATUSES.map((status) => `'${status}'`)
681
- .join(", ")
682
- .replace(/, ([^,]*)$/, " or $1")}`,
683
- input: value,
684
- ctx: { expected: FINAL_STATUSES.map((status) => `'${status}'`).join(", ") },
685
- });
686
- export const orderHandlers = (state) => ({
687
- get_paginated_lab_tests_for_team_v3_lab_test_get: async () => jsonRes(200, {
688
- data: state.listLabTests().filter((test) => test.auto_generated !== true),
689
- next_cursor: null,
690
- }),
691
- get_lab_test_for_team_v3_lab_tests__lab_test_id__get: async (context) => {
692
- const id = context.params.lab_test_id ?? "";
693
- const test = state.labTestById(id);
694
- if (!test)
695
- notFound("Lab test does not exist");
696
- return jsonRes(200, test);
697
- },
698
- get_labs_v3_lab_tests_labs_get: async () => jsonRes(200, state.listLabs()),
699
- get_team_lab_accounts_v3_lab_test_lab_account_get: async (context) => {
700
- const requestedId = labAccountIdFilter(context);
701
- const status = labAccountStatusFilter(context);
702
- const data = TEAM_LAB_ACCOUNTS.filter((entry) => entry.team_id_allowlist.includes(MOCK_TEAM_ID))
703
- .filter((entry) => requestedId === null || entry.id === requestedId)
704
- .filter((entry) => status === null || entry.status === status)
705
- .map(renderLabAccount);
706
- return jsonRes(200, { data });
707
- },
708
- get_markers_for_lab_test_v3_lab_tests__lab_test_id__markers_get: async (context) => {
709
- const id = context.params.lab_test_id ?? "";
710
- const test = state.labTestById(id);
711
- if (!test)
712
- notFound("Lab test does not exist");
713
- const expected = state.expectedResultsFor(id);
714
- const markers = (test.markers ?? []).map((marker) => ({
715
- ...marker,
716
- expected_results: expected,
717
- }));
718
- return jsonRes(200, paginateMarkers(context.query, markers));
719
- },
720
- list_order_set_markers_v3_lab_tests_list_order_set_markers_post: async (context) => {
721
- const body = jsonObject(context);
722
- const orderSetId = body.order_set_id;
723
- if (typeof orderSetId !== "string" || !isUuid(orderSetId)) {
724
- throw new HttpError(422, {
725
- detail: [uuidError(String(orderSetId ?? ""), 0, ["body", "order_set_id"])],
726
- });
727
- }
728
- const binding = state.orderByTransaction.get(orderSetId);
729
- const order = binding ? state.orders.get(binding.order_id) : undefined;
730
- if (!order)
731
- notFound("Order set not found");
732
- const providerId = typeof body.provider_id === "string" ? body.provider_id : null;
733
- const markers = (order.lab_test.markers ?? [])
734
- .filter((marker) => providerId === null || marker.provider_id === providerId)
735
- .map((marker) => ({
736
- ...marker,
737
- expected_results: state.expectedResultsFor(order.lab_test.id),
738
- }));
739
- return jsonRes(200, paginateMarkers(context.query, markers));
740
- },
741
- create_order_v3_order_post: async (context) => {
742
- const rawBody = jsonObject(context);
743
- const body = trimStrings(rawBody);
744
- body.aoe_answers = rawBody.aoe_answers;
745
- const patient = rawBody.patient_details;
746
- if (typeof patient === "object" && patient !== null && !Array.isArray(patient)) {
747
- const details = patient;
748
- const crashes = Object.entries(details).some(([key, value]) => {
749
- if ((key === "first_name" || key === "last_name") &&
750
- (typeof value === "number" || typeof value === "boolean"))
751
- return true;
752
- if (key === "gender" && value !== null && typeof value !== "string")
753
- return true;
754
- if (key === "phone_number" && value !== null && typeof value !== "string")
755
- return true;
756
- if (key === "dob" && typeof value === "boolean")
757
- return true;
758
- return false;
759
- });
760
- if (crashes) {
761
- return new Response("Internal Server Error", {
762
- status: 500,
763
- headers: { "content-type": "text/plain; charset=utf-8" },
764
- });
765
- }
766
- }
767
- const errors = orderValidation(body);
768
- if (errors.length > 0)
769
- throw new HttpError(422, { detail: errors });
770
- const rawAddress = body.patient_address;
771
- if (typeof rawAddress === "object" &&
772
- rawAddress !== null &&
773
- !Array.isArray(rawAddress) &&
774
- typeof rawAddress.country === "string" &&
775
- /[^A-Za-z ]/.test(rawAddress.country ?? "")) {
776
- return new Response("Internal Server Error", {
777
- status: 500,
778
- headers: { "content-type": "text/plain; charset=utf-8" },
779
- });
780
- }
781
- const userId = body.user_id;
782
- if (typeof userId !== "string")
783
- throw new HttpError(422, { detail: "user_id must be a string" });
784
- if (!state.users.has(userId) && !state.deletedUsers.has(userId))
785
- notFound("User does not exist on this team");
786
- const phone = body.patient_details?.phone_number;
787
- if (typeof phone === "string" && /^\+1\d{10}$/.test(phone) && phone.slice(2, 5) === "555") {
788
- throw new HttpError(400, { detail: "Phone number is not correct" });
789
- }
790
- if (body.collection_method === "testkit")
791
- throw new HttpError(400, { detail: "Cannot set collection_method to TESTKIT" });
792
- const aoeError = aoeValidationError(state, body);
793
- if (aoeError)
794
- throw new HttpError(400, { detail: aoeError });
795
- const rawOrderSet = body.order_set;
796
- if (rawOrderSet === undefined && body.lab_test_id === undefined) {
797
- throw new HttpError(400, { detail: "Either lab_test_id or order_set must be set" });
798
- }
799
- if (typeof rawOrderSet === "object" &&
800
- rawOrderSet !== null &&
801
- !Array.isArray(rawOrderSet) &&
802
- rawOrderSet.lab_test_ids === undefined &&
803
- rawOrderSet.add_on === undefined) {
804
- throw new HttpError(400, { detail: "lab_test_ids or add_on must be set in order_set" });
805
- }
806
- const details = body.patient_details;
807
- const address = body.patient_address;
808
- const labTestIds = Array.isArray(body.order_set.lab_test_ids)
809
- ? body.order_set.lab_test_ids
810
- : [];
811
- // Vital: empty lab_test_ids → "No markers found…"; unknown id → "Test does not exist";
812
- // known panels with markers:null (e.g. Female General Wellness) still create successfully.
813
- if (labTestIds.length === 0) {
814
- throw new HttpError(400, {
815
- detail: "No markers found for the supplied lab tests. Please check the lab test ids",
816
- });
817
- }
818
- const resolved = labTestIds.map((id) => typeof id === "string" ? state.labTestById(id) : undefined);
819
- if (resolved.some((test) => test === undefined)) {
820
- throw new HttpError(400, { detail: "Test does not exist" });
821
- }
822
- const labTests = resolved;
823
- const methods = new Set(labTests.map((test) => typeof test.method === "string" && test.method.length > 0 ? test.method : ""));
824
- if (methods.size > 1) {
825
- throw new HttpError(400, {
826
- detail: "Cannot order with lab tests with multiple collection methods",
827
- });
828
- }
829
- const labTest = labTests[0];
830
- if (!labTest)
831
- throw new HttpError(400, { detail: "Test does not exist" });
832
- const stateError = labStateSupportError(labTest, address.state);
833
- if (stateError)
834
- throw new HttpError(400, { detail: stateError });
835
- // Lab-account routing: the account must be active, linked to the team and associated
836
- // with the ordered lab; with no explicit id, linked accounts decide the branch.
837
- const labSlug = typeof labTest.lab?.slug === "string" ? labTest.lab.slug.toLowerCase() : "";
838
- const labAccount = selectLabAccount(labSlug, typeof body.lab_account_id === "string" ? body.lab_account_id : null, MOCK_TEAM_ID);
839
- const billingType = typeof body.billing_type === "string" && body.billing_type !== ""
840
- ? body.billing_type
841
- : "client_bill";
842
- const allowedStates = effectiveBilling(labAccount)[billingType];
843
- if (!allowedStates)
844
- throw new HttpError(400, {
845
- detail: `Billing type ${billingType} is not supported by the lab account used for this order`,
846
- });
847
- const patientState = String(address.state ?? "")
848
- .trim()
849
- .toUpperCase();
850
- if (patientState !== "" && !allowedStates.includes(patientState))
851
- throw new HttpError(400, {
852
- detail: `Billing type ${billingType} is not available in state ${patientState} for the lab account used for this order`,
853
- });
854
- const icdCodes = Array.isArray(body.icd_codes) ? [...body.icd_codes] : null;
855
- if (billingType === "commercial_insurance" && (icdCodes === null || icdCodes.length === 0))
856
- throw new HttpError(400, {
857
- detail: "Commercial insurance orders require at least one ICD code in icd_codes",
858
- });
859
- const nowIso = state.isoNow(context.now);
860
- const nowMicro = new Date(context.now()).toISOString();
861
- const orderId = state.nextOrderId();
862
- const transactionId = state.transactionIdFor(orderId);
863
- const method = typeof body.collection_method === "string" ? body.collection_method : labTest.method;
864
- // Sandbox: same collection_method as the panel → embed the panel as-is (even when
865
- // markers are null). Different method requires markers to synthesize auto_generated.
866
- const nativeMethod = typeof labTest.method === "string" && labTest.method.length > 0 ? labTest.method : method;
867
- const markerCount = labTests.reduce((sum, test) => sum + (test.markers?.length ?? 0), 0);
868
- if (method !== nativeMethod && markerCount === 0) {
869
- throw new HttpError(400, {
870
- detail: "No markers found for the supplied lab tests. Please check the lab test ids",
871
- });
872
- }
873
- const idempotencyKey = body.idempotency_key;
874
- const requestFingerprint = JSON.stringify(body);
875
- if (typeof idempotencyKey === "string" && idempotencyKey.length > 0) {
876
- const replay = state.orderIdempotency.get(idempotencyKey);
877
- if (replay) {
878
- if (replay.fingerprint !== requestFingerprint)
879
- throw new HttpError(400, {
880
- detail: "Idempotency key was reused with a different request",
881
- });
882
- return jsonRes(200, replay.response);
883
- }
884
- }
885
- const eventStatus = `received.${method}.ordered`;
886
- const eventId = Number.parseInt(opaqueToken(`junction:order-event:${orderId}`, 8), 16) % 1_000_000_000 || 1;
887
- const event = {
888
- id: eventId,
889
- created_at: nowIso,
890
- status: eventStatus,
891
- status_detail: null,
892
- };
893
- let orderLabTest;
894
- if (method === nativeMethod) {
895
- orderLabTest = labTest;
896
- }
897
- else {
898
- const markerFingerprint = (test) => (test.markers ?? [])
899
- .map((marker) => marker.id)
900
- .sort((a, b) => a - b)
901
- .join(",");
902
- const sourceFingerprint = markerFingerprint(labTest);
903
- const existingAuto = state
904
- .listLabTests()
905
- .find((test) => test.auto_generated === true &&
906
- test.method === method &&
907
- markerFingerprint(test) === sourceFingerprint);
908
- const orderLabTestKey = `${labTest.id}:${method}`;
909
- const orderLabTestName = existingAuto?.name ?? opaqueToken(`junction:order-lab-name:${orderLabTestKey}`, 16);
910
- orderLabTest = existingAuto ?? {
911
- ...labTest,
912
- id: deterministicUuid(`junction:order-lab-test:${orderLabTestKey}`),
913
- method,
914
- auto_generated: true,
915
- name: orderLabTestName,
916
- slug: `afd62b39-${orderLabTestName}`,
917
- };
918
- }
919
- const order = {
920
- id: orderId,
921
- user_id: userId,
922
- team_id: MOCK_TEAM_ID,
923
- patient_details: patientDetails(details),
924
- patient_address: patientAddress(address),
925
- lab_test: orderLabTest,
926
- details: METHOD_DETAILS(method, state.testkitIdFor(orderId), nowIso),
927
- sample_id: null,
928
- notes: null,
929
- clinical_notes: typeof body.clinical_notes === "string" ? body.clinical_notes : null,
930
- passthrough: typeof body.passthrough === "string" ? body.passthrough : null,
931
- ...(typeof body.lab_account_id === "string" ? { lab_account_id: body.lab_account_id } : {}),
932
- created_at: nowIso,
933
- updated_at: nowIso,
934
- events: [event],
935
- status: "received",
936
- last_event: event,
937
- physician: PHYSICIAN,
938
- health_insurance_id: null,
939
- requisition_form_url: null,
940
- shipping_details: null,
941
- has_abn: false,
942
- billing_type: billingType,
943
- priority: false,
944
- activate_by: null,
945
- icd_codes: icdCodes,
946
- interpretation: null,
947
- has_missing_results: null,
948
- result_types: null,
949
- expected_result_by_date: null,
950
- worst_case_result_by_date: null,
951
- origin: "initial",
952
- order_transaction: {
953
- id: transactionId,
954
- status: "active",
955
- orders: [
956
- {
957
- id: orderId,
958
- low_level_status: "ordered",
959
- low_level_status_created_at: nowMicro,
960
- origin: "initial",
961
- parent_id: null,
962
- created_at: nowMicro,
963
- updated_at: nowMicro,
964
- },
965
- ],
966
- },
967
- };
968
- state.orders.insert(orderId, order);
969
- state.orderByTransaction.insert(transactionId, { order_id: orderId });
970
- state.upsertLabTest(orderLabTest, orderLabTest.id === labTest.id ? undefined : state.expectedResultsFor(labTest.id));
971
- const demographics = {
972
- first_name: details.first_name ?? null,
973
- last_name: details.last_name ?? null,
974
- dob: details.dob ?? null,
975
- gender: details.gender ?? null,
976
- phone_number: details.phone_number ?? null,
977
- email: details.email ?? null,
978
- gender_identity: null,
979
- sexual_orientation: null,
980
- race: null,
981
- ethnicity: null,
982
- medical_proxy: null,
983
- address: {
984
- first_line: address.first_line ?? "",
985
- second_line: typeof address.second_line === "string" ? address.second_line : "",
986
- country: address.country ?? "",
987
- zip: address.zip ?? "",
988
- city: address.city ?? "",
989
- state: address.state ?? "",
990
- access_notes: null,
991
- },
992
- };
993
- if (state.userInfo.get(userId))
994
- state.userInfo.update(userId, demographics);
995
- else
996
- state.userInfo.insert(userId, demographics);
997
- const responseOrder = { ...order };
998
- if (responseOrder.result_types === null)
999
- delete responseOrder.result_types;
1000
- const response = {
1001
- order: responseOrder,
1002
- status: "success",
1003
- message: "Order created successfully",
1004
- };
1005
- if (typeof idempotencyKey === "string" && idempotencyKey.length > 0)
1006
- state.orderIdempotency.insert(idempotencyKey, {
1007
- order_id: orderId,
1008
- response,
1009
- fingerprint: requestFingerprint,
1010
- });
1011
- state.publishOrderWebhook(order, "labtest.order.created", context.now());
1012
- return jsonRes(200, response);
1013
- },
1014
- cancel_order_v3_order__order_id__cancel_post: async (context) => {
1015
- const id = context.params.order_id ?? "";
1016
- const order = state.orders.get(id);
1017
- if (!order)
1018
- notFound("Order doesn't exist");
1019
- const alreadyCancelled = order.events.some((entry) => typeof entry.status === "string" && entry.status.startsWith("cancelled."));
1020
- if (alreadyCancelled) {
1021
- const responseOrder = { ...order };
1022
- if (responseOrder.result_types === null)
1023
- delete responseOrder.result_types;
1024
- return jsonRes(200, {
1025
- order: responseOrder,
1026
- status: "success",
1027
- message: "order already cancelled",
1028
- });
1029
- }
1030
- const lastEvent = order.last_event ?? order.events[order.events.length - 1];
1031
- const lowLevel = typeof lastEvent?.status === "string" ? lastEvent.status : "";
1032
- if (!CANCELLABLE_ORDER_EVENTS.has(lowLevel)) {
1033
- throw new HttpError(400, {
1034
- detail: "This order is not in a Cancellable Status. Refer to the documentation to check the order lifecycle (https://docs.tryvital.io/lab/workflow/lab-test-lifecycle).",
1035
- });
1036
- }
1037
- const now = state.isoNow(context.now);
1038
- const method = typeof order.lab_test.method === "string" && order.lab_test.method.length > 0
1039
- ? order.lab_test.method
1040
- : typeof order.details?.type === "string" && order.details.type.length > 0
1041
- ? order.details.type
1042
- : "at_home_phlebotomy";
1043
- order.status = "cancelled";
1044
- order.updated_at = now;
1045
- const event = {
1046
- id: order.events.length + 1,
1047
- created_at: now,
1048
- status: `cancelled.${method}.cancelled`,
1049
- status_detail: null,
1050
- };
1051
- order.events.push(event);
1052
- order.last_event = event;
1053
- order.order_transaction.status = "cancelled";
1054
- const transactionOrder = order.order_transaction.orders[0];
1055
- if (transactionOrder) {
1056
- transactionOrder.low_level_status = "cancelled";
1057
- transactionOrder.updated_at = new Date(context.now()).toISOString();
1058
- }
1059
- state.orders.update(id, order);
1060
- state.publishOrderWebhook(order, "labtest.order.updated", context.now());
1061
- // Junction auto-cancels mobile phlebotomy appointments on order cancel, but
1062
- // leaves walk-in PSC appointments alone (patient may already be en route).
1063
- if (method === "at_home_phlebotomy") {
1064
- cascadeCancelAppointments(state, id, context.now());
1065
- }
1066
- const responseOrder = { ...order };
1067
- if (responseOrder.result_types === null)
1068
- delete responseOrder.result_types;
1069
- const response = {
1070
- order: responseOrder,
1071
- status: "success",
1072
- message: "order cancelled",
1073
- };
1074
- return jsonRes(200, response);
1075
- },
1076
- simulate_order_v3_order__order_id__test_post: async (context) => {
1077
- const id = context.params.order_id ?? "";
1078
- const finalStatus = context.query.final_status;
1079
- if (!isUuid(id))
1080
- throw new HttpError(422, { detail: [uuidError(id, undefined, ["path", "order_id"])] });
1081
- if (finalStatus !== undefined &&
1082
- (typeof finalStatus !== "string" ||
1083
- !FINAL_STATUSES.includes(finalStatus)))
1084
- throw new HttpError(422, { detail: [finalStatusError(String(finalStatus ?? ""))] });
1085
- const delayRaw = context.query.delay;
1086
- let delaySeconds = 0;
1087
- if (delayRaw !== undefined) {
1088
- if (typeof delayRaw !== "string" || !/^\d+$/.test(delayRaw))
1089
- throw new HttpError(422, { detail: "delay must be a non-negative integer" });
1090
- delaySeconds = Number(delayRaw);
1091
- }
1092
- const body = context.body;
1093
- if (body.kind === "json") {
1094
- const value = body.value;
1095
- if (typeof value !== "object" || Array.isArray(value)) {
1096
- throw new HttpError(422, {
1097
- detail: [
1098
- {
1099
- type: "model_attributes_type",
1100
- loc: ["body"],
1101
- msg: "Input should be a valid dictionary or object to extract fields from",
1102
- input: value,
1103
- },
1104
- ],
1105
- });
1106
- }
1107
- }
1108
- const order = state.orders.get(id);
1109
- if (!order)
1110
- notFound("Order doesn't exist");
1111
- if (typeof finalStatus !== "string" ||
1112
- !FINAL_STATUSES.includes(finalStatus))
1113
- throw new HttpError(422, { detail: [finalStatusError(String(finalStatus ?? ""))] });
1114
- const flags = simulationFlagsOf(context);
1115
- if (delaySeconds > 0) {
1116
- state.queueSimulateTransition({
1117
- order_id: id,
1118
- due_at: context.now() + delaySeconds * 1000,
1119
- final_status: finalStatus,
1120
- flags,
1121
- });
1122
- return jsonRes(200, "Success");
1123
- }
1124
- applySimulateTransition(state, order, finalStatus, flags, context);
1125
- return jsonRes(200, "Success");
1126
- },
1127
- get_order_v3_order__order_id__get: async (context) => {
1128
- const id = context.params.order_id ?? "";
1129
- const order = state.orders.get(id);
1130
- if (!order)
1131
- notFound("This order doesn't exist");
1132
- state.applyDueSimulateTransitions(context.now(), (due, finalStatus, flags) => {
1133
- applySimulateTransition(state, due, finalStatus, flags, context);
1134
- });
1135
- const fresh = state.orders.get(id);
1136
- if (!fresh)
1137
- notFound("This order doesn't exist");
1138
- const body = { ...fresh };
1139
- if (body.result_types === null)
1140
- delete body.result_types;
1141
- return jsonRes(200, body);
1142
- },
1143
- get_orders_v3_orders_get: async (context) => {
1144
- const page = queryInt(context, "page", 1);
1145
- const size = queryInt(context, "size", 50);
1146
- if (page < 1 || size < 1 || size > 100)
1147
- throw new HttpError(422, { detail: "page must be >= 1 and size must be 1..100" });
1148
- const userId = context.query.user_id;
1149
- if (userId !== undefined && typeof userId !== "string")
1150
- throw new HttpError(422, { detail: "user_id must be a string" });
1151
- if (userId !== undefined && !state.users.has(userId) && !state.deletedUsers.has(userId))
1152
- notFound("User does not exist on this team");
1153
- let all = state.orders.list({ order: "oldest" });
1154
- if (userId !== undefined)
1155
- all = all.filter((entry) => entry.value.user_id === userId);
1156
- // Sandbox lists by updated_at desc (not created_at) — wrong order breaks
1157
- // seedParity identity pairing on list pages, which then poisons get_order.
1158
- all = [...all].sort((left, right) => {
1159
- const leftAt = Date.parse(String(left.value.updated_at ?? left.value.created_at ?? "")) || 0;
1160
- const rightAt = Date.parse(String(right.value.updated_at ?? right.value.created_at ?? "")) || 0;
1161
- if (rightAt !== leftAt)
1162
- return rightAt - leftAt;
1163
- // Frozen clocks make updated_at ties common; seq matches sandbox insertion order.
1164
- return right.seq - left.seq;
1165
- });
1166
- const pageItems = all.slice((page - 1) * size, page * size);
1167
- return jsonRes(200, {
1168
- orders: pageItems.map((entry) => {
1169
- const order = { ...entry.value };
1170
- if (order.result_types === null)
1171
- delete order.result_types;
1172
- return order;
1173
- }),
1174
- total: all.length,
1175
- page,
1176
- size,
1177
- });
1178
- },
1179
- get_order_transaction_v3_order_transaction__transaction_id__get: async (context) => {
1180
- const id = context.params.transaction_id ?? "";
1181
- const binding = state.orderByTransaction.get(id);
1182
- const order = binding ? state.orders.get(binding.order_id) : undefined;
1183
- if (!order)
1184
- notFound("Order transaction not found");
1185
- return jsonRes(200, {
1186
- id,
1187
- team_id: MOCK_TEAM_ID,
1188
- status: order.order_transaction.status,
1189
- orders: [orderSummary(order)],
1190
- });
1191
- },
1192
- get_order_transaction_result_v3_order_transaction__transaction_id__result_get: async (context) => {
1193
- const id = context.params.transaction_id ?? "";
1194
- const binding = state.orderByTransaction.get(id);
1195
- const order = binding ? state.orders.get(binding.order_id) : undefined;
1196
- if (!order)
1197
- notFound("Order transaction not found");
1198
- const user = order.user_id ? state.users.get(order.user_id) : undefined;
1199
- return jsonRes(200, {
1200
- metadata: {
1201
- age: "41",
1202
- dob: "1983-06-23",
1203
- patient: user?.client_user_id ?? order.user_id,
1204
- date_reported: "2024-11-02",
1205
- specimen_number: opaqueToken(`junction:specimen:${order.id}`, 24),
1206
- status: "final",
1207
- laboratory: "Mockingbird Central Lab",
1208
- provider: null,
1209
- interpretation: null,
1210
- patient_id: null,
1211
- account_id: null,
1212
- date_collected: null,
1213
- date_received: null,
1214
- "clia_#": null,
1215
- },
1216
- results: biomarker(order, order.created_at),
1217
- missing_results: null,
1218
- sample_information: null,
1219
- order_transaction: order.order_transaction,
1220
- });
1221
- },
1222
- });
1223
- const orderSummary = (order) => {
1224
- const transactionOrder = order.order_transaction.orders.find((entry) => entry.id === order.id);
1225
- return {
1226
- id: order.id,
1227
- origin: transactionOrder?.origin ?? "initial",
1228
- parent_id: transactionOrder?.parent_id ?? null,
1229
- last_status: transactionOrder?.low_level_status ?? "ordered",
1230
- last_status_created_at: transactionOrder?.low_level_status_created_at ?? order.created_at,
1231
- updated_at: order.updated_at,
1232
- created_at: order.created_at,
1233
- };
1234
- };
1235
- const biomarker = (order, created_at) => (order.lab_test.markers ?? []).map((marker) => ({
1236
- name: marker.name,
1237
- slug: marker.slug,
1238
- result: marker.slug,
1239
- type: "numeric",
1240
- unit: marker.unit,
1241
- timestamp: created_at,
1242
- reference_range: null,
1243
- interpretation: "normal",
1244
- performing_laboratory: "Mockingbird Central Lab",
1245
- source_sample_id: null,
1246
- }));
1247
- //# sourceMappingURL=orders.js.map