@tellescope/schema 1.253.0 → 1.254.0

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/src/schema.ts CHANGED
@@ -90,6 +90,7 @@ import {
90
90
  AIDecisionAutomationAction,
91
91
  AutomationAction,
92
92
  TimeTrackTimestamp,
93
+ TimeTrackReviewHistoryItem,
93
94
  BelugaPharmacyMapping,
94
95
  BelugaAutomationMappingEntry,
95
96
  BelugaUpdateVisitPatientPreferenceItem,
@@ -172,6 +173,7 @@ import {
172
173
  fileSizeValidator,
173
174
  meetingStatusValidator,
174
175
  listOfAttendeesValidator,
176
+ listOfVideoCallParticipantEventsValidator,
175
177
  meetingInfoValidator,
176
178
  listOfUserIndentitiesValidator,
177
179
  meetingsListValidator,
@@ -287,6 +289,7 @@ import {
287
289
  userCallRoutingBehaviorValidator,
288
290
  userUIRestrictionsValidator,
289
291
  userFieldRedactionsValidator,
292
+ portalSchemaRestrictionsValidator,
290
293
  externalChatGPTMessagesValidator,
291
294
  enduserProfileViewBlocksValidator,
292
295
  customDashboardBlocksValidator,
@@ -460,6 +463,7 @@ export type ModelFieldInfo <T, R> = {
460
463
  readonly?: boolean,
461
464
  required?: boolean,
462
465
  updatesDisabled?: boolean,
466
+ enduserUpdatesDisabled?: boolean, // enduser sessions cannot update this field; user sessions still can
463
467
  examples?: JSONType[],
464
468
  initializer?: Initializer<Partial<T>, R>, // should include the required fields of T, not just partial
465
469
  dependencies?: Dependency<Partial<T>>[],
@@ -1506,13 +1510,15 @@ export const schema: SchemaV1 = build_schema({
1506
1510
  unsubscribe: {},
1507
1511
  },
1508
1512
  fields: {
1509
- ...BuiltInFields,
1513
+ ...BuiltInFields,
1514
+ sharedWithOrganizations: { ...BuiltInFields.sharedWithOrganizations, enduserUpdatesDisabled: true },
1510
1515
  recentViewers: { validator: recentViewersValidator },
1511
- healthie_dietitian_id: { validator: stringValidator100 },
1516
+ healthie_dietitian_id: { validator: stringValidator100, enduserUpdatesDisabled: true },
1512
1517
  mergedIds: { validator: listOfMongoIdStringValidatorOptionalOrEmptyOk, readonly: true, redactions: ['enduser'] },
1513
1518
  externalId: {
1514
1519
  validator: stringValidator250,
1515
1520
  examples: ['addfed3e-ddea-415b-b52b-df820c944dbb'],
1521
+ enduserUpdatesDisabled: true,
1516
1522
  },
1517
1523
  email: {
1518
1524
  validator: emailValidatorEmptyOkay,
@@ -1566,9 +1572,10 @@ export const schema: SchemaV1 = build_schema({
1566
1572
  validator: stringValidator250,
1567
1573
  redactions: ['enduser'],
1568
1574
  },
1569
- journeys: {
1575
+ journeys: {
1570
1576
  validator: journeysValidator,
1571
1577
  redactions: ['enduser'],
1578
+ enduserUpdatesDisabled: true, // endusers may still use the add_to_journey / remove_from_journey custom actions
1572
1579
  dependencies: [
1573
1580
  {
1574
1581
  dependsOn: ['journeys'],
@@ -1584,13 +1591,15 @@ export const schema: SchemaV1 = build_schema({
1584
1591
  ]
1585
1592
  },
1586
1593
  scheduledJourneys: { validator: scheduledJourneysValidator },
1587
- tags: {
1594
+ tags: {
1588
1595
  redactions: ['enduser'],
1589
1596
  validator: listOfStringsValidatorUniqueOptionalOrEmptyOkay,
1597
+ enduserUpdatesDisabled: true,
1590
1598
  },
1591
- accessTags: {
1599
+ accessTags: {
1592
1600
  redactions: ['enduser'],
1593
1601
  validator: listOfStringsValidatorEmptyOk,
1602
+ enduserUpdatesDisabled: true,
1594
1603
  },
1595
1604
  unredactedTags: {
1596
1605
  validator: listOfStringsValidatorEmptyOk,
@@ -1598,6 +1607,7 @@ export const schema: SchemaV1 = build_schema({
1598
1607
  fields: {
1599
1608
  redactions: ['enduser'],
1600
1609
  validator: fieldsValidator,
1610
+ enduserUpdatesDisabled: true, // intake forms write fields server-side; direct enduser PATCH is not a supported path
1601
1611
  },
1602
1612
  unredactedFields: {
1603
1613
  validator: fieldsValidator,
@@ -1606,17 +1616,20 @@ export const schema: SchemaV1 = build_schema({
1606
1616
  redactions: ['enduser'],
1607
1617
  validator: preferenceValidator,
1608
1618
  },
1609
- assignedTo: {
1619
+ assignedTo: {
1610
1620
  redactions: ['enduser'],
1611
1621
  validator: listOfUniqueStringsValidatorEmptyOk,
1622
+ enduserUpdatesDisabled: true,
1612
1623
  },
1613
- primaryAssignee: {
1624
+ primaryAssignee: {
1614
1625
  validator: mongoIdStringValidator,
1615
1626
  redactions: ['enduser'],
1627
+ enduserUpdatesDisabled: true,
1616
1628
  },
1617
- unread: {
1629
+ unread: {
1618
1630
  redactions: ['enduser'],
1619
1631
  validator: booleanValidator,
1632
+ enduserUpdatesDisabled: true,
1620
1633
  },
1621
1634
  lastActive: {
1622
1635
  validator: dateValidator,
@@ -1651,7 +1664,7 @@ export const schema: SchemaV1 = build_schema({
1651
1664
  pronouns: { validator: stringValidator100, redactions: ['enduser'] },
1652
1665
  height: { validator: genericUnitWithQuantityValidator, redactions: ['enduser'] },
1653
1666
  weight: { validator: genericUnitWithQuantityValidator, redactions: ['enduser'] },
1654
- source: { validator: stringValidator1000Optional },
1667
+ source: { validator: stringValidator1000Optional, enduserUpdatesDisabled: true },
1655
1668
  addressLineOne: { validator: stringValidator5000EmptyOkay, redactions: ['enduser'] },
1656
1669
  addressLineTwo: { validator: stringValidator5000EmptyOkay, redactions: ['enduser'] },
1657
1670
  city: { validator: stringValidator5000EmptyOkay, redactions: ['enduser'] },
@@ -1663,7 +1676,7 @@ export const schema: SchemaV1 = build_schema({
1663
1676
  displayName: { validator: stringValidator250 }, // intentionally not redacted for other endusers
1664
1677
  unsubscribedFromPortalChatNotifications: { validator: booleanValidator },
1665
1678
  triggeredEvents: { validator: objectAnyFieldsValidator(numberValidator) },
1666
- customTypeId: { validator: mongoIdStringValidator },
1679
+ customTypeId: { validator: mongoIdStringValidator, enduserUpdatesDisabled: true },
1667
1680
  language: { validator: languageValidator },
1668
1681
  relationships: {
1669
1682
  validator: listValidatorOptionalOrEmptyOk(objectValidator<EnduserRelationship>({
@@ -1673,10 +1686,10 @@ export const schema: SchemaV1 = build_schema({
1673
1686
  },
1674
1687
  markedReadAt: { validator: dateOptionalOrEmptyStringValidator },
1675
1688
  markedUnreadAt: { validator: dateOptionalOrEmptyStringValidator },
1676
- note: { validator: stringValidator25000EmptyOkay, redactions: ['enduser'] },
1689
+ note: { validator: stringValidator25000EmptyOkay, redactions: ['enduser'], enduserUpdatesDisabled: true },
1677
1690
  noteIsFlagged: { validator: booleanValidator },
1678
- insurance: { validator: insuranceOptionalValidator, redactions: ['enduser'] },
1679
- insuranceSecondary: { validator: insuranceOptionalValidator, redactions: ['enduser'] },
1691
+ insurance: { validator: insuranceOptionalValidator, redactions: ['enduser'], enduserUpdatesDisabled: true }, // patient insurance entry is supported via intake forms (server-side write)
1692
+ insuranceSecondary: { validator: insuranceOptionalValidator, redactions: ['enduser'], enduserUpdatesDisabled: true },
1680
1693
  bookingNotes: {
1681
1694
  validator: listValidatorOptionalOrEmptyOk(objectValidator<{ bookingPageId: string, note: string }>({
1682
1695
  bookingPageId: mongoIdStringRequired,
@@ -1694,24 +1707,25 @@ export const schema: SchemaV1 = build_schema({
1694
1707
  })),
1695
1708
  redactions: ['enduser'],
1696
1709
  },
1697
- references: { validator: listOfRelatedRecordsValidator, redactions: ['enduser'] },
1698
- athenaDepartmentId: { validator: stringValidator100, redactions: ['enduser'] },
1699
- athenaPracticeId: { validator: stringValidator100, redactions: ['enduser'] },
1700
- salesforceId: { validator: stringValidator100, redactions: ['enduser'] },
1710
+ references: { validator: listOfRelatedRecordsValidator, redactions: ['enduser'], enduserUpdatesDisabled: true },
1711
+ athenaDepartmentId: { validator: stringValidator100, redactions: ['enduser'], enduserUpdatesDisabled: true },
1712
+ athenaPracticeId: { validator: stringValidator100, redactions: ['enduser'], enduserUpdatesDisabled: true },
1713
+ salesforceId: { validator: stringValidator100, redactions: ['enduser'], enduserUpdatesDisabled: true },
1701
1714
  vitalTriggersDisabled: { validator: booleanValidator },
1702
1715
  defaultFromPhone: { validator: phoneValidator, redactions: ['enduser'] },
1703
1716
  defaultFromEmail: { validator: emailValidator, redactions: ['enduser'] },
1704
1717
  useDefaultFromEmailInAutomations: { validator: booleanValidator },
1705
1718
  useDefaultFromPhoneInAutomations: { validator: booleanValidator },
1706
- stripeCustomerId: { validator: stringValidator100, redactions: ['enduser'] },
1707
- stripeKey: { validator: stringValidator250, redactions: ['enduser'] },
1719
+ stripeCustomerId: { validator: stringValidator100, redactions: ['enduser'], enduserUpdatesDisabled: true },
1720
+ stripeKey: { validator: stringValidator250, redactions: ['enduser'], enduserUpdatesDisabled: true },
1708
1721
  diagnoses: {
1709
1722
  validator: listValidatorOptionalOrEmptyOk(enduserDiagnosisValidator),
1710
- redactions: ['enduser']
1723
+ redactions: ['enduser'],
1724
+ enduserUpdatesDisabled: true,
1711
1725
  },
1712
1726
  unsubscribedFromPhones: { validator: listOfStringsValidatorUniqueOptionalOrEmptyOkay, redactions: ['enduser'] },
1713
- lockedFromPortal: { validator: booleanValidator },
1714
- eligibleForAutoMerge: { validator: booleanValidator },
1727
+ lockedFromPortal: { validator: booleanValidator, enduserUpdatesDisabled: true },
1728
+ eligibleForAutoMerge: { validator: booleanValidator, enduserUpdatesDisabled: true },
1715
1729
  preferredPharmacy: { validator: pharmacyValidator, redactions: ['enduser'] },
1716
1730
  // recentMessagePreview: {
1717
1731
  // validator: stringValidator,
@@ -2633,6 +2647,7 @@ export const schema: SchemaV1 = build_schema({
2633
2647
  info: {},
2634
2648
  fields: {
2635
2649
  ...BuiltInFields,
2650
+ accessTags: { redactions: ['enduser'], validator: listOfStringsValidatorEmptyOk },
2636
2651
  archivedAt: { validator: dateOptionalOrEmptyStringValidator },
2637
2652
  title: {
2638
2653
  validator: stringValidator100,
@@ -3475,7 +3490,9 @@ export const schema: SchemaV1 = build_schema({
3475
3490
  {
3476
3491
  explanation: "Only admin users can update tags when accessTags is enabled",
3477
3492
  evaluate: ({ _id }, _, session, method, { updates } ) => {
3478
- if (session.type === 'user' && !session.eat) return // accessTags is not enabled
3493
+ // editing a user's tags is a privilege-escalation vector whenever tags gate visibility:
3494
+ // enduser access tags (eat) OR resource access tags (erat). Skip only when neither is enabled.
3495
+ if (session.type === 'user' && !session.eat && !(session as UserSession)?.erat) return // neither access-tags feature enabled
3479
3496
  if ((session as UserSession)?.roles?.includes('Admin')) return
3480
3497
  if (method === 'create') return
3481
3498
  if (!updates?.tags) return
@@ -4171,7 +4188,8 @@ export const schema: SchemaV1 = build_schema({
4171
4188
  },
4172
4189
  },
4173
4190
  fields: {
4174
- ...BuiltInFields,
4191
+ ...BuiltInFields,
4192
+ accessTags: { redactions: ['enduser'], validator: listOfStringsValidatorEmptyOk },
4175
4193
  archivedAt: { validator: dateOptionalOrEmptyStringValidator },
4176
4194
  mmsAttachmentURLs: { validator: listOfUniqueStringsValidatorEmptyOk },
4177
4195
  title: {
@@ -4219,7 +4237,8 @@ export const schema: SchemaV1 = build_schema({
4219
4237
  defaultActions: { read: {}, readMany: {}, update: {}, delete: {} },
4220
4238
  enduserActions: { prepare_file_upload: {}, confirm_file_upload: {}, file_download_URL: {}, read: {}, readMany: {}, delete: {}, update: { } /* allow to hide from client side */ },
4221
4239
  fields: {
4222
- ...BuiltInFields,
4240
+ ...BuiltInFields,
4241
+ accessTags: { redactions: ['enduser'], validator: listOfStringsValidatorEmptyOk },
4223
4242
  source: { validator: stringValidator100 },
4224
4243
  tags: { validator: listOfStringsValidatorUniqueOptionalOrEmptyOkay },
4225
4244
  name: {
@@ -4237,6 +4256,9 @@ export const schema: SchemaV1 = build_schema({
4237
4256
  enduserId: { // deleted as side effect of enduser delete
4238
4257
  validator: mongoIdStringValidator,
4239
4258
  },
4259
+ formResponseId: { // link to the form response a file was uploaded for (used for Beluga sync)
4260
+ validator: mongoIdStringValidator,
4261
+ },
4240
4262
  secureName: {
4241
4263
  validator: stringValidator250,
4242
4264
  readonly: true,
@@ -4485,7 +4507,7 @@ export const schema: SchemaV1 = build_schema({
4485
4507
  },
4486
4508
  enduserActions: { create: {}, read: {}, readMany: {} },
4487
4509
  fields: {
4488
- ...BuiltInFields,
4510
+ ...BuiltInFields,
4489
4511
  title: {
4490
4512
  validator: stringValidator1000,
4491
4513
  required: true,
@@ -4810,6 +4832,7 @@ export const schema: SchemaV1 = build_schema({
4810
4832
  },
4811
4833
  fields: {
4812
4834
  ...BuiltInFields,
4835
+ accessTags: { redactions: ['enduser'], validator: listOfStringsValidatorEmptyOk },
4813
4836
  showByUserTags: { validator: listOfStringsValidatorOptionalOrEmptyOk },
4814
4837
  belugaVisitType: { validator: stringValidator },
4815
4838
  belugaVerificationId: { validator: stringValidator },
@@ -5025,8 +5048,8 @@ export const schema: SchemaV1 = build_schema({
5025
5048
  fields: {
5026
5049
  ...BuiltInFields,
5027
5050
  discussionRoomId: { validator: mongoIdStringValidator },
5028
- hiddenFromTimeline: { validator: booleanValidator },
5029
- lockedAt: { validator: dateValidator },
5051
+ hiddenFromTimeline: { validator: booleanValidator, enduserUpdatesDisabled: true },
5052
+ lockedAt: { validator: dateValidator, enduserUpdatesDisabled: true },
5030
5053
  formId: {
5031
5054
  validator: stringValidator100, // allow other external id types here too, not just mongoid
5032
5055
  required: true,
@@ -5059,42 +5082,43 @@ export const schema: SchemaV1 = build_schema({
5059
5082
  copiedFrom: { validator: mongoIdStringOptional },
5060
5083
  copiedFromEnduserId: { validator: mongoIdStringOptional },
5061
5084
  publicSubmit: { validator: booleanValidator },
5062
- submittedBy: { validator: stringValidator250 },
5063
- submittedByIsPlaceholder: { validator: booleanValidator },
5064
- markedAsSubmitted: { validator: booleanValidator },
5085
+ submittedBy: { validator: stringValidator250, enduserUpdatesDisabled: true },
5086
+ submittedByIsPlaceholder: { validator: booleanValidator, enduserUpdatesDisabled: true },
5087
+ markedAsSubmitted: { validator: booleanValidator, enduserUpdatesDisabled: true }, // endusers submit via the submit_form_response custom action
5065
5088
  accessCode: { validator: stringValidator250 },
5066
- userEmail: { validator: emailValidator },
5067
- submittedAt: { validator: dateValidator },
5068
- formTitle: { validator: stringValidator250 },
5089
+ userEmail: { validator: emailValidator, enduserUpdatesDisabled: true },
5090
+ submittedAt: { validator: dateValidator, enduserUpdatesDisabled: true },
5091
+ formTitle: { validator: stringValidator250, enduserUpdatesDisabled: true },
5069
5092
  responses: { validator: formResponsesValidator },
5070
5093
  draftSavedAt: { validator: dateValidator },
5071
5094
  draftSavedBy: { validator: mongoIdStringValidator },
5072
5095
  hideFromEnduserPortal: { validator: booleanValidator },
5073
5096
  sharedVia: { validator: communicationsChannelValidator },
5074
- isInternalNote: { validator: booleanValidator },
5075
- pinnedAt: { validator: dateOptionalOrEmptyStringValidator },
5097
+ isInternalNote: { validator: booleanValidator, enduserUpdatesDisabled: true },
5098
+ pinnedAt: { validator: dateOptionalOrEmptyStringValidator, enduserUpdatesDisabled: true },
5076
5099
  publicIdentifier: { validator: stringValidator250 },
5077
5100
  source: { validator: stringValidator250 },
5078
5101
  externalId: { validator: stringValidator250 },
5079
5102
  rootResponseId: { validator: mongoIdStringValidator },
5080
5103
  parentResponseId: { validator: mongoIdStringValidator },
5081
- tags: { validator: listOfStringsValidatorOptionalOrEmptyOk },
5104
+ tags: { validator: listOfStringsValidatorOptionalOrEmptyOk, enduserUpdatesDisabled: true },
5082
5105
  carePlanId: { validator: mongoIdStringValidator },
5083
5106
  context: { validator: stringValidator1000 },
5084
- logoURL: { validator: stringValidator5000 },
5085
- logoHeight: { validator: numberValidator },
5107
+ logoURL: { validator: stringValidator5000, enduserUpdatesDisabled: true },
5108
+ logoHeight: { validator: numberValidator, enduserUpdatesDisabled: true },
5086
5109
  calendarEventId: { validator: mongoIdStringValidator },
5087
5110
  references: { validator: listOfRelatedRecordsValidator, readonly: true },
5088
5111
  groupId: { validator: mongoIdStringValidator },
5089
5112
  groupInstance: { validator: stringValidator100 },
5090
5113
  groupPosition: { validator: nonNegNumberValidator },
5091
5114
  hideAfterUnsubmittedInMS: { validator: numberValidator },
5092
- addenda: {
5093
- validator: listValidatorOptionalOrEmptyOk(objectValidator<Addendum>({
5115
+ addenda: {
5116
+ validator: listValidatorOptionalOrEmptyOk(objectValidator<Addendum>({
5094
5117
  text: stringValidator25000EmptyOkay,
5095
5118
  timestamp: dateValidator,
5096
5119
  userId: mongoIdStringRequired,
5097
- }))
5120
+ })),
5121
+ enduserUpdatesDisabled: true,
5098
5122
  },
5099
5123
  followups: {
5100
5124
  validator: listValidatorOptionalOrEmptyOk(objectValidator<FormResponseFollowup>({
@@ -5115,9 +5139,9 @@ export const schema: SchemaV1 = build_schema({
5115
5139
  }))
5116
5140
  },
5117
5141
  startedViaPinnedForm: { validator: booleanValidator },
5118
- enduserAISummary: { validator: stringValidator25000 },
5119
- procedureCodes: { validator: procedureCodesValidator },
5120
- diagnosisCodes: { validator: diagnosisCodesValidator },
5142
+ enduserAISummary: { validator: stringValidator25000, enduserUpdatesDisabled: true },
5143
+ procedureCodes: { validator: procedureCodesValidator, enduserUpdatesDisabled: true },
5144
+ diagnosisCodes: { validator: diagnosisCodesValidator, enduserUpdatesDisabled: true },
5121
5145
  },
5122
5146
  defaultActions: DEFAULT_OPERATIONS,
5123
5147
  enduserActions: {
@@ -5930,6 +5954,7 @@ export const schema: SchemaV1 = build_schema({
5930
5954
  meetingId: { validator: mongoIdStringValidator, readonly: true },
5931
5955
  bookingPageId: { validator: mongoIdStringValidator }, // allows rescheduling via booking page
5932
5956
  meetingStatus: { validator: meetingStatusValidator },
5957
+ videoCallAttendance: { validator: listOfVideoCallParticipantEventsValidator, readonly: true },
5933
5958
  attachments: { validator: listOfGenericAttachmentsValidator },
5934
5959
  cancelledAt: { validator: dateOptionalOrEmptyStringValidator },
5935
5960
  rescheduledAt: { validator: dateOptionalOrEmptyStringValidator },
@@ -6064,7 +6089,8 @@ export const schema: SchemaV1 = build_schema({
6064
6089
  customActions: {},
6065
6090
  enduserActions: { read: {}, readMany: {} },
6066
6091
  fields: {
6067
- ...BuiltInFields,
6092
+ ...BuiltInFields,
6093
+ accessTags: { redactions: ['enduser'], validator: listOfStringsValidatorEmptyOk },
6068
6094
  dontSyncToElation: { validator: booleanValidator },
6069
6095
  sendIcsEmail: { validator: booleanValidator },
6070
6096
  createAndBookAthenaSlot: { validator: booleanValidator },
@@ -6592,7 +6618,8 @@ export const schema: SchemaV1 = build_schema({
6592
6618
  search: {},
6593
6619
  },
6594
6620
  fields: {
6595
- ...BuiltInFields,
6621
+ ...BuiltInFields,
6622
+ accessTags: { redactions: ['enduser'], validator: listOfStringsValidatorEmptyOk },
6596
6623
  slug: { validator: stringValidator250 },
6597
6624
  title: {
6598
6625
  validator: stringValidator1000,
@@ -7199,7 +7226,7 @@ export const schema: SchemaV1 = build_schema({
7199
7226
  type: stringValidatorOptional,
7200
7227
  }),
7201
7228
  },
7202
- onboardingStatus: { validator: stringValidator100 }, // read/write for org admins (not super-admin restricted)
7229
+ onboardingStatus: { validator: userPortalSettingsValidator }, // key-value store, same shape as User.portalSettings; read/write for org admins (not super-admin restricted)
7203
7230
  creditCount: { validator: numberValidator, readonly: true },
7204
7231
  stripeKeyDetails: {
7205
7232
  validator: listValidatorOptionalOrEmptyOk(objectValidator<StripeKeyDetail>({
@@ -7608,6 +7635,12 @@ export const schema: SchemaV1 = build_schema({
7608
7635
  },
7609
7636
  uiRestrictions: { validator: userUIRestrictionsValidator },
7610
7637
  fieldRedactions: { validator: userFieldRedactionsValidator },
7638
+ portalSchemaRestrictions: {
7639
+ validator: portalSchemaRestrictionsValidator,
7640
+ initializer: () => ({ disableEditContent: true, disableEditTheming: true, disableEditSnippets: true }),
7641
+ },
7642
+ color: { validator: stringValidator1000 },
7643
+ description: { validator: stringValidator1000Optional },
7611
7644
  }
7612
7645
  },
7613
7646
  appointment_booking_pages: {
@@ -7651,7 +7684,8 @@ export const schema: SchemaV1 = build_schema({
7651
7684
  read: {}, readMany: {}, validate_access_token: {},
7652
7685
  },
7653
7686
  fields: {
7654
- ...BuiltInFields,
7687
+ ...BuiltInFields,
7688
+ accessTags: { redactions: ['enduser'], validator: listOfStringsValidatorEmptyOk },
7655
7689
  dontRestrictRescheduleToOriginalHost: { validator: booleanValidator },
7656
7690
  gtmTag: { validator: stringValidator100EscapeHTML },
7657
7691
  archivedAt: { validator: dateOptionalOrEmptyStringValidator },
@@ -8488,7 +8522,8 @@ If a voicemail is left, it is indicated by recordingURI, transcription, or recor
8488
8522
  },
8489
8523
  enduserActions: {},
8490
8524
  fields: {
8491
- ...BuiltInFields,
8525
+ ...BuiltInFields,
8526
+ accessTags: { redactions: ['enduser'], validator: listOfStringsValidatorEmptyOk },
8492
8527
  title: {
8493
8528
  validator: stringValidator100,
8494
8529
  required: true,
@@ -8843,7 +8878,8 @@ If a voicemail is left, it is indicated by recordingURI, transcription, or recor
8843
8878
  customActions: {},
8844
8879
  enduserActions: {},
8845
8880
  fields: {
8846
- ...BuiltInFields,
8881
+ ...BuiltInFields,
8882
+ accessTags: { redactions: ['enduser'], validator: listOfStringsValidatorEmptyOk },
8847
8883
  title: {
8848
8884
  validator: stringValidator,
8849
8885
  required: true,
@@ -9025,14 +9061,33 @@ If a voicemail is left, it is indicated by recordingURI, transcription, or recor
9025
9061
  }
9026
9062
  },
9027
9063
  {
9028
- explanation: "Locked time tracks only allow review field updates",
9029
- evaluate: (_v, _deps, _session, method, { original, updates }) => {
9064
+ explanation: "Locked time tracks only allow review field updates, or owner resubmission of rejected entries",
9065
+ evaluate: (_v, _deps, session, method, { original, updates }) => {
9030
9066
  if (method !== 'update') return
9031
9067
  const orig = original as any as TimeTrack | undefined
9032
9068
  if (!orig?.lockedAt) return
9069
+
9070
+ const u = updates as any as Partial<TimeTrack> | undefined
9033
9071
  const reviewFields = ['reviewedAt', 'reviewedByUserId', 'reviewApproved', 'reviewNote']
9034
9072
  const nonReviewFields = Object.keys(updates || {}).filter(k => !reviewFields.includes(k))
9035
- if (nonReviewFields.length > 0) return `Time track is locked. Only review fields (${reviewFields.join(', ')}) can be updated.`
9073
+ if (nonReviewFields.length === 0) return // pure review update unchanged behavior
9074
+
9075
+ const isRejected = !!orig.reviewedAt && orig.reviewApproved === false
9076
+ const resubmitFields = [
9077
+ ...reviewFields,
9078
+ 'correctedAt', 'correctedByUserId', 'correctionNote',
9079
+ 'originalTotalDurationInMS', 'totalDurationInMS',
9080
+ 'lockedAt', 'lockedByUserId',
9081
+ ]
9082
+ const disallowed = Object.keys(updates || {}).filter(k => !resubmitFields.includes(k))
9083
+
9084
+ if (isRejected && session.id === orig.userId && disallowed.length === 0) {
9085
+ if (u?.reviewedAt !== '') return "Resubmitting a rejected time track requires clearing reviewedAt (set to '')"
9086
+ if (u?.reviewApproved === true) return "Cannot approve your own time track during resubmission"
9087
+ return // valid resubmit — entry returns to Pending
9088
+ }
9089
+
9090
+ return `Time track is locked. Only review fields (${reviewFields.join(', ')}) can be updated.`
9036
9091
  }
9037
9092
  },
9038
9093
  {
@@ -9117,6 +9172,19 @@ If a voicemail is left, it is indicated by recordingURI, transcription, or recor
9117
9172
  reviewNote: { validator: stringValidator1000Optional },
9118
9173
  lockedAt: { validator: dateValidatorOptional },
9119
9174
  lockedByUserId: { validator: mongoIdStringOptional },
9175
+ // server-appended on resubmission of a rejected entry (see routing.ts update handler)
9176
+ // note: reviewApproved remains false after resubmit (booleans can't be cleared to '') — status logic must key off reviewedAt
9177
+ reviewHistory: {
9178
+ validator: listValidatorOptionalOrEmptyOk(objectValidator<TimeTrackReviewHistoryItem>({
9179
+ reviewedAt: dateValidator,
9180
+ reviewedByUserId: mongoIdStringOptional,
9181
+ reviewApproved: booleanValidatorOptional,
9182
+ reviewNote: stringValidator1000Optional,
9183
+ resubmittedAt: dateValidatorOptional,
9184
+ resubmittedByUserId: mongoIdStringOptional,
9185
+ })),
9186
+ readonly: true, // server-appended only — clients can read, never write
9187
+ },
9120
9188
  },
9121
9189
  },
9122
9190
  ticket_queues: {
@@ -9729,7 +9797,7 @@ If a voicemail is left, it is indicated by recordingURI, transcription, or recor
9729
9797
  fields: {
9730
9798
  ...BuiltInFields,
9731
9799
  integration: { validator: stringValidator, readonly: true, examples: ['Canvas'] },
9732
- status: { validator: exactMatchValidator(['Success', 'Error']), readonly: true, examples: ['Error'] },
9800
+ status: { validator: exactMatchValidator(['Success', 'Error', 'Info']), readonly: true, examples: ['Error'] },
9733
9801
  type: { validator: stringValidator, readonly: true, examples: ['Patient Create'] },
9734
9802
  payload: { validator: stringValidator, readonly: true, examples: ['{}'] },
9735
9803
  response: { validator: stringValidator, readonly: true, examples: ['{}'] },