canvas 0.72.1__py3-none-any.whl → 0.74.0__py3-none-any.whl

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.

Potentially problematic release.


This version of canvas might be problematic. Click here for more details.

@@ -93,12 +93,16 @@ class EventType(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
93
93
  DETECTED_ISSUE_UPDATED: _ClassVar[EventType]
94
94
  TASK_CLOSED: _ClassVar[EventType]
95
95
  TASK_COMPLETED: _ClassVar[EventType]
96
+ TASK_METADATA_CREATED: _ClassVar[EventType]
97
+ TASK_METADATA_UPDATED: _ClassVar[EventType]
96
98
  DETECTED_ISSUE_EVIDENCE_CREATED: _ClassVar[EventType]
97
99
  DETECTED_ISSUE_EVIDENCE_UPDATED: _ClassVar[EventType]
98
100
  STAFF_ACTIVATED: _ClassVar[EventType]
99
101
  STAFF_DEACTIVATED: _ClassVar[EventType]
100
102
  COMPOUND_MEDICATION_CREATED: _ClassVar[EventType]
101
103
  COMPOUND_MEDICATION_UPDATED: _ClassVar[EventType]
104
+ APPOINTMENT_LABEL_ADDED: _ClassVar[EventType]
105
+ APPOINTMENT_LABEL_REMOVED: _ClassVar[EventType]
102
106
  PRE_COMMAND_ORIGINATE: _ClassVar[EventType]
103
107
  POST_COMMAND_ORIGINATE: _ClassVar[EventType]
104
108
  PRE_COMMAND_UPDATE: _ClassVar[EventType]
@@ -142,6 +146,8 @@ class EventType(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
142
146
  ADJUST_PRESCRIPTION__CHANGE_MEDICATION_TO__POST_SEARCH: _ClassVar[EventType]
143
147
  ADJUST_PRESCRIPTION__SUPERVISING_PROVIDER__PRE_SEARCH: _ClassVar[EventType]
144
148
  ADJUST_PRESCRIPTION__SUPERVISING_PROVIDER__POST_SEARCH: _ClassVar[EventType]
149
+ ADJUST_PRESCRIPTION__PRESCRIBER__PRE_SEARCH: _ClassVar[EventType]
150
+ ADJUST_PRESCRIPTION__PRESCRIBER__POST_SEARCH: _ClassVar[EventType]
145
151
  ALLERGY_COMMAND__PRE_ORIGINATE: _ClassVar[EventType]
146
152
  ALLERGY_COMMAND__POST_ORIGINATE: _ClassVar[EventType]
147
153
  ALLERGY_COMMAND__PRE_UPDATE: _ClassVar[EventType]
@@ -523,6 +529,8 @@ class EventType(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
523
529
  PRESCRIBE__PHARMACY__POST_SEARCH: _ClassVar[EventType]
524
530
  PRESCRIBE__SUPERVISING_PROVIDER__POST_SEARCH: _ClassVar[EventType]
525
531
  PRESCRIBE__SUPERVISING_PROVIDER__PRE_SEARCH: _ClassVar[EventType]
532
+ PRESCRIBE__PRESCRIBER__PRE_SEARCH: _ClassVar[EventType]
533
+ PRESCRIBE__PRESCRIBER__POST_SEARCH: _ClassVar[EventType]
526
534
  QUESTIONNAIRE_COMMAND__PRE_ORIGINATE: _ClassVar[EventType]
527
535
  QUESTIONNAIRE_COMMAND__POST_ORIGINATE: _ClassVar[EventType]
528
536
  QUESTIONNAIRE_COMMAND__PRE_UPDATE: _ClassVar[EventType]
@@ -599,6 +607,8 @@ class EventType(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
599
607
  REFILL__PHARMACY__POST_SEARCH: _ClassVar[EventType]
600
608
  REFILL__SUPERVISING_PROVIDER__PRE_SEARCH: _ClassVar[EventType]
601
609
  REFILL__SUPERVISING_PROVIDER__POST_SEARCH: _ClassVar[EventType]
610
+ REFILL__PRESCRIBER__PRE_SEARCH: _ClassVar[EventType]
611
+ REFILL__PRESCRIBER__POST_SEARCH: _ClassVar[EventType]
602
612
  REMOVE_ALLERGY_COMMAND__PRE_ORIGINATE: _ClassVar[EventType]
603
613
  REMOVE_ALLERGY_COMMAND__POST_ORIGINATE: _ClassVar[EventType]
604
614
  REMOVE_ALLERGY_COMMAND__PRE_UPDATE: _ClassVar[EventType]
@@ -1011,12 +1021,16 @@ DETECTED_ISSUE_CREATED: EventType
1011
1021
  DETECTED_ISSUE_UPDATED: EventType
1012
1022
  TASK_CLOSED: EventType
1013
1023
  TASK_COMPLETED: EventType
1024
+ TASK_METADATA_CREATED: EventType
1025
+ TASK_METADATA_UPDATED: EventType
1014
1026
  DETECTED_ISSUE_EVIDENCE_CREATED: EventType
1015
1027
  DETECTED_ISSUE_EVIDENCE_UPDATED: EventType
1016
1028
  STAFF_ACTIVATED: EventType
1017
1029
  STAFF_DEACTIVATED: EventType
1018
1030
  COMPOUND_MEDICATION_CREATED: EventType
1019
1031
  COMPOUND_MEDICATION_UPDATED: EventType
1032
+ APPOINTMENT_LABEL_ADDED: EventType
1033
+ APPOINTMENT_LABEL_REMOVED: EventType
1020
1034
  PRE_COMMAND_ORIGINATE: EventType
1021
1035
  POST_COMMAND_ORIGINATE: EventType
1022
1036
  PRE_COMMAND_UPDATE: EventType
@@ -1060,6 +1074,8 @@ ADJUST_PRESCRIPTION__CHANGE_MEDICATION_TO__PRE_SEARCH: EventType
1060
1074
  ADJUST_PRESCRIPTION__CHANGE_MEDICATION_TO__POST_SEARCH: EventType
1061
1075
  ADJUST_PRESCRIPTION__SUPERVISING_PROVIDER__PRE_SEARCH: EventType
1062
1076
  ADJUST_PRESCRIPTION__SUPERVISING_PROVIDER__POST_SEARCH: EventType
1077
+ ADJUST_PRESCRIPTION__PRESCRIBER__PRE_SEARCH: EventType
1078
+ ADJUST_PRESCRIPTION__PRESCRIBER__POST_SEARCH: EventType
1063
1079
  ALLERGY_COMMAND__PRE_ORIGINATE: EventType
1064
1080
  ALLERGY_COMMAND__POST_ORIGINATE: EventType
1065
1081
  ALLERGY_COMMAND__PRE_UPDATE: EventType
@@ -1441,6 +1457,8 @@ PRESCRIBE__PHARMACY__PRE_SEARCH: EventType
1441
1457
  PRESCRIBE__PHARMACY__POST_SEARCH: EventType
1442
1458
  PRESCRIBE__SUPERVISING_PROVIDER__POST_SEARCH: EventType
1443
1459
  PRESCRIBE__SUPERVISING_PROVIDER__PRE_SEARCH: EventType
1460
+ PRESCRIBE__PRESCRIBER__PRE_SEARCH: EventType
1461
+ PRESCRIBE__PRESCRIBER__POST_SEARCH: EventType
1444
1462
  QUESTIONNAIRE_COMMAND__PRE_ORIGINATE: EventType
1445
1463
  QUESTIONNAIRE_COMMAND__POST_ORIGINATE: EventType
1446
1464
  QUESTIONNAIRE_COMMAND__PRE_UPDATE: EventType
@@ -1517,6 +1535,8 @@ REFILL__PHARMACY__PRE_SEARCH: EventType
1517
1535
  REFILL__PHARMACY__POST_SEARCH: EventType
1518
1536
  REFILL__SUPERVISING_PROVIDER__PRE_SEARCH: EventType
1519
1537
  REFILL__SUPERVISING_PROVIDER__POST_SEARCH: EventType
1538
+ REFILL__PRESCRIBER__PRE_SEARCH: EventType
1539
+ REFILL__PRESCRIBER__POST_SEARCH: EventType
1520
1540
  REMOVE_ALLERGY_COMMAND__PRE_ORIGINATE: EventType
1521
1541
  REMOVE_ALLERGY_COMMAND__POST_ORIGINATE: EventType
1522
1542
  REMOVE_ALLERGY_COMMAND__PRE_UPDATE: EventType
@@ -0,0 +1,93 @@
1
+ from dataclasses import dataclass
2
+ from typing import Any
3
+ from uuid import UUID
4
+
5
+ from pydantic import conlist
6
+ from pydantic_core import InitErrorDetails
7
+
8
+ from canvas_sdk.effects.base import EffectType, _BaseEffect
9
+ from canvas_sdk.v1.data import Claim
10
+ from canvas_sdk.v1.data.common import ColorEnum
11
+
12
+
13
+ @dataclass
14
+ class Label:
15
+ """A class representing a label."""
16
+
17
+ color: ColorEnum
18
+ name: str
19
+
20
+ def to_dict(self) -> dict[str, Any]:
21
+ """Convert the label to a dictionary."""
22
+ return {"color": self.color.value, "name": self.name}
23
+
24
+
25
+ class _ClaimLabelBase(_BaseEffect):
26
+ """Base class for managing ClaimLabels."""
27
+
28
+ claim_id: UUID | str
29
+ labels: conlist(str | Label, min_length=1) # type: ignore
30
+
31
+ def _check_if_claim_exists(self) -> list[InitErrorDetails]:
32
+ if Claim.objects.filter(id=self.claim_id).exists():
33
+ return []
34
+ return [
35
+ (
36
+ self._create_error_detail(
37
+ "value",
38
+ f"Claim with id {self.claim_id} does not exist.",
39
+ self.claim_id,
40
+ )
41
+ )
42
+ ]
43
+
44
+ def _get_error_details(self, method: Any) -> list[InitErrorDetails]:
45
+ errors = super()._get_error_details(method)
46
+ if not Claim.objects.filter(id=self.claim_id).exists():
47
+ errors.append(
48
+ self._create_error_detail(
49
+ "value",
50
+ f"Claim with id {self.claim_id} does not exist.",
51
+ self.claim_id,
52
+ )
53
+ )
54
+ return errors
55
+
56
+
57
+ class AddClaimLabel(_ClaimLabelBase):
58
+ """Effect to add a label to a Claim."""
59
+
60
+ class Meta:
61
+ effect_type = EffectType.ADD_CLAIM_LABEL
62
+
63
+ @property
64
+ def values(self) -> dict[str, Any]:
65
+ """The values for adding a claim label."""
66
+ return {
67
+ "claim_id": str(self.claim_id),
68
+ "labels": [
69
+ label.to_dict() if isinstance(label, Label) else {"name": label}
70
+ for label in self.labels
71
+ ],
72
+ }
73
+
74
+
75
+ class RemoveClaimLabel(_ClaimLabelBase):
76
+ """Effect to remove a label from a Claim."""
77
+
78
+ class Meta:
79
+ effect_type = EffectType.REMOVE_CLAIM_LABEL
80
+
81
+ labels: conlist(str, min_length=1) # type: ignore
82
+
83
+ @property
84
+ def values(self) -> dict[str, Any]:
85
+ """The values for removing a claim label."""
86
+ return {"claim_id": str(self.claim_id), "labels": list(self.labels)}
87
+
88
+
89
+ __exports__ = (
90
+ "Label",
91
+ "AddClaimLabel",
92
+ "RemoveClaimLabel",
93
+ )
@@ -0,0 +1,47 @@
1
+ from typing import Any
2
+ from uuid import UUID
3
+
4
+ from pydantic_core import InitErrorDetails
5
+
6
+ from canvas_sdk.effects.base import EffectType, _BaseEffect
7
+ from canvas_sdk.v1.data import ClaimLineItem
8
+
9
+
10
+ class UpdateClaimLineItem(_BaseEffect):
11
+ """
12
+ An Effect that updates a Claim Line Item.
13
+ """
14
+
15
+ class Meta:
16
+ effect_type = EffectType.UPDATE_CLAIM_LINE_ITEM
17
+
18
+ claim_line_item_id: str | UUID
19
+ charge: float | None = None
20
+
21
+ @property
22
+ def values(self) -> dict[str, Any]:
23
+ """The values for the payload."""
24
+ if self.charge is None:
25
+ return {}
26
+ return {"charge": str(self.charge)}
27
+
28
+ @property
29
+ def effect_payload(self) -> dict[str, Any]:
30
+ """The payload of the effect."""
31
+ return {"data": self.values, "claim_line_item_id": str(self.claim_line_item_id)}
32
+
33
+ def _get_error_details(self, method: Any) -> list[InitErrorDetails]:
34
+ errors = super()._get_error_details(method)
35
+
36
+ if not ClaimLineItem.objects.filter(id=self.claim_line_item_id).exists():
37
+ errors.append(
38
+ self._create_error_detail(
39
+ "value",
40
+ "Claim Line Item does not exist",
41
+ self.claim_line_item_id,
42
+ )
43
+ )
44
+ return errors
45
+
46
+
47
+ __exports__ = ("UpdateClaimLineItem",)
@@ -0,0 +1,3 @@
1
+ from canvas_sdk.effects.fax.note import FaxNoteEffect
2
+
3
+ __all__ = __exports__ = ("FaxNoteEffect",)
@@ -0,0 +1,77 @@
1
+ from abc import ABC
2
+ from typing import Any, Literal
3
+ from uuid import UUID
4
+
5
+ from pydantic_core import InitErrorDetails
6
+
7
+ from canvas_sdk.effects import EffectType, _BaseEffect
8
+ from canvas_sdk.v1.data import PracticeLocation
9
+
10
+
11
+ class BaseFaxEffect(_BaseEffect, ABC):
12
+ """Base class for fax effects."""
13
+
14
+ class Meta:
15
+ effect_type = EffectType.UNKNOWN_EFFECT
16
+
17
+ recipient_name: str
18
+ recipient_fax_number: str
19
+ include_coversheet: bool = False
20
+ subject: str | None = None
21
+ comment: str | None = None
22
+ location_id: str | UUID | None = None
23
+
24
+ def _get_error_details(self, method: Literal["apply"]) -> list[InitErrorDetails]:
25
+ errors = super()._get_error_details(method)
26
+
27
+ if self.include_coversheet:
28
+ if not self.subject:
29
+ errors.append(
30
+ self._create_error_detail(
31
+ "value",
32
+ "subject is required when include_coversheet is True",
33
+ self.subject,
34
+ )
35
+ )
36
+ if not self.comment:
37
+ errors.append(
38
+ self._create_error_detail(
39
+ "value",
40
+ "comment is required when include_coversheet is True",
41
+ self.comment,
42
+ )
43
+ )
44
+ if self.location_id:
45
+ if not PracticeLocation.objects.filter(id=self.location_id).exists():
46
+ errors.append(
47
+ self._create_error_detail(
48
+ "value",
49
+ f"Practice Location {self.location_id} does not exist",
50
+ self.location_id,
51
+ )
52
+ )
53
+ else:
54
+ errors.append(
55
+ self._create_error_detail(
56
+ "value",
57
+ "location_id is required when include_coversheet is True",
58
+ self.location_id,
59
+ )
60
+ )
61
+
62
+ return errors
63
+
64
+ @property
65
+ def values(self) -> dict[str, Any]:
66
+ """Return the values of the fax effect."""
67
+ return {
68
+ "recipient_name": self.recipient_name,
69
+ "recipient_fax_number": self.recipient_fax_number,
70
+ "include_coversheet": self.include_coversheet,
71
+ "subject": self.subject,
72
+ "comment": self.comment,
73
+ "location_id": str(self.location_id) if self.location_id else None,
74
+ }
75
+
76
+
77
+ __exports__ = ()
@@ -0,0 +1,42 @@
1
+ from typing import Literal
2
+ from uuid import UUID
3
+
4
+ from pydantic_core import InitErrorDetails
5
+
6
+ from canvas_generated.messages.effects_pb2 import EffectType
7
+ from canvas_sdk.effects.fax.base import BaseFaxEffect
8
+ from canvas_sdk.v1.data import Note
9
+
10
+
11
+ class FaxNoteEffect(BaseFaxEffect):
12
+ """Fax note effect."""
13
+
14
+ class Meta:
15
+ effect_type = EffectType.FAX_NOTE
16
+
17
+ note_id: str | UUID
18
+
19
+ def _get_error_details(self, method: Literal["apply"]) -> list[InitErrorDetails]:
20
+ errors = super()._get_error_details(method)
21
+ if self.note_id and not Note.objects.filter(id=self.note_id).exists():
22
+ errors.append(
23
+ self._create_error_detail(
24
+ "value",
25
+ f"Note with ID {self.note_id} does not exist.",
26
+ self.note_id,
27
+ )
28
+ )
29
+
30
+ return errors
31
+
32
+ @property
33
+ def values(self) -> dict[str, str]:
34
+ """Return the values of the note fax effect."""
35
+ values = super().values
36
+ return {
37
+ **values,
38
+ "note_id": str(self.note_id),
39
+ }
40
+
41
+
42
+ __exports__ = ()
@@ -1,4 +1,9 @@
1
- from canvas_sdk.effects.note.appointment import Appointment, ScheduleEvent
1
+ from canvas_sdk.effects.note.appointment import (
2
+ AddAppointmentLabel,
3
+ Appointment,
4
+ RemoveAppointmentLabel,
5
+ ScheduleEvent,
6
+ )
2
7
  from canvas_sdk.effects.note.base import AppointmentIdentifier
3
8
  from canvas_sdk.effects.note.note import Note
4
9
 
@@ -7,4 +12,6 @@ __all__ = __exports__ = (
7
12
  "Note",
8
13
  "Appointment",
9
14
  "ScheduleEvent",
15
+ "AddAppointmentLabel",
16
+ "RemoveAppointmentLabel",
10
17
  )
@@ -1,12 +1,17 @@
1
1
  import json
2
- from typing import Any
2
+ from typing import Annotated, Any
3
3
  from uuid import UUID
4
4
 
5
+ from django.db.models import Count
6
+ from pydantic import Field
5
7
  from pydantic_core import InitErrorDetails
6
8
 
7
- from canvas_sdk.effects import Effect
9
+ from canvas_sdk.base import TrackableFieldsModel
10
+ from canvas_sdk.effects import Effect, EffectType
11
+ from canvas_sdk.effects.base import _BaseEffect
8
12
  from canvas_sdk.effects.note.base import AppointmentABC
9
- from canvas_sdk.v1.data import NoteType, Patient
13
+ from canvas_sdk.v1.data import Appointment as AppointmentDataModel
14
+ from canvas_sdk.v1.data import AppointmentLabel, NoteType, Patient
10
15
  from canvas_sdk.v1.data.note import NoteTypeCategories
11
16
 
12
17
 
@@ -127,6 +132,7 @@ class Appointment(AppointmentABC):
127
132
  appointment_note_type_id (UUID | str | None): The ID of the appointment note type.
128
133
  meeting_link (str | None): The meeting link for the appointment, if any.
129
134
  patient_id (str | None): The ID of the patient.
135
+ labels (conset[str] | None): A set of label names to apply to the appointment.
130
136
  """
131
137
 
132
138
  class Meta:
@@ -135,6 +141,13 @@ class Appointment(AppointmentABC):
135
141
  appointment_note_type_id: UUID | str | None = None
136
142
  meeting_link: str | None = None
137
143
  patient_id: str | None = None
144
+ labels: (
145
+ Annotated[
146
+ set[Annotated[str, Field(min_length=1, max_length=50)]],
147
+ Field(min_length=1, max_length=3),
148
+ ]
149
+ | None
150
+ ) = None
138
151
 
139
152
  def _get_error_details(self, method: Any) -> list[InitErrorDetails]:
140
153
  """
@@ -215,8 +228,39 @@ class Appointment(AppointmentABC):
215
228
  )
216
229
  )
217
230
 
231
+ if method == "update" and self.labels and self.instance_id:
232
+ existing_count = AppointmentLabel.objects.filter(
233
+ appointment__id=self.instance_id
234
+ ).count()
235
+ if existing_count + len(self.labels) > 3:
236
+ errors.append(
237
+ self._create_error_detail(
238
+ "value",
239
+ f"Limit reached: Only 3 appointment labels allowed. Attempted to add {len(self.labels)} label(s) to appointment with {existing_count} existing label(s).",
240
+ sorted(self.labels),
241
+ )
242
+ )
243
+
218
244
  return errors
219
245
 
246
+ @property
247
+ def values(self) -> dict:
248
+ """
249
+ Returns a dictionary of modified attributes with type-specific transformations.
250
+ """
251
+ values = super().values
252
+ # Convert labels set to list for JSON serialization
253
+ # This is necessary because:
254
+ # 1. The labels field is defined as a conset (constrained set) for validation
255
+ # 2. JSON cannot serialize Python sets directly - it only supports lists, dicts, strings, numbers, booleans, and None
256
+ # 3. When the effect payload is serialized to JSON in the base Effect class, it would fail with:
257
+ # "TypeError: Object of type set is not JSON serializable"
258
+ # 4. Converting to list maintains the same data while making it JSON-compatible
259
+ # 5. Sort the labels to ensure consistent ordering for tests and API responses
260
+ if self.labels is not None:
261
+ values["labels"] = sorted(self.labels)
262
+ return values
263
+
220
264
  def cancel(self) -> Effect:
221
265
  """Send a CANCEL effect for the appointment."""
222
266
  self._validate_before_effect("cancel")
@@ -230,4 +274,86 @@ class Appointment(AppointmentABC):
230
274
  )
231
275
 
232
276
 
233
- __exports__ = ("ScheduleEvent", "Appointment")
277
+ class _AppointmentLabelBase(_BaseEffect, TrackableFieldsModel):
278
+ """
279
+ Base class for appointment label effects.
280
+
281
+ Attributes:
282
+ appointment_id (UUID | str): The ID of the appointment.
283
+ labels (conset[str]): A set of label names (1-3 labels allowed).
284
+ """
285
+
286
+ appointment_id: str
287
+ labels: Annotated[set[str], Field(min_length=1, max_length=3)]
288
+
289
+ @property
290
+ def values(self) -> dict:
291
+ """The effect's values."""
292
+ result = {
293
+ "appointment_id": str(self.appointment_id),
294
+ "labels": sorted(self.labels),
295
+ }
296
+ return result
297
+
298
+
299
+ class AddAppointmentLabel(_AppointmentLabelBase):
300
+ """
301
+ Effect to add one or more labels to an appointment.
302
+ """
303
+
304
+ class Meta:
305
+ effect_type = EffectType.ADD_APPOINTMENT_LABEL
306
+
307
+ def _get_error_details(self, method: Any) -> list[InitErrorDetails]:
308
+ """Validate that the appointment does not exceed the 3-label limit."""
309
+ errors = super()._get_error_details(method)
310
+
311
+ appointment_label_count = (
312
+ AppointmentDataModel.objects.filter(id=self.appointment_id)
313
+ .annotate(label_count=Count("labels"))
314
+ .values_list("label_count", flat=True)
315
+ .first()
316
+ )
317
+
318
+ # note that appointment_label_count will be None if the appointment doesn't exist
319
+ # and appointment_label_count will be 0 if the appointment exists but has no labels
320
+ if appointment_label_count is None:
321
+ errors.append(
322
+ self._create_error_detail(
323
+ "value",
324
+ f"Appointment {self.appointment_id} does not exist",
325
+ self.appointment_id,
326
+ )
327
+ )
328
+ elif appointment_label_count + len(self.labels) > 3:
329
+ errors.append(
330
+ self._create_error_detail(
331
+ "value",
332
+ f"Limit reached: Only 3 appointment labels allowed. "
333
+ f"Attempted to add {len(self.labels)} label(s) to appointment with "
334
+ f"{appointment_label_count} existing label(s).",
335
+ sorted(self.labels),
336
+ )
337
+ )
338
+ return errors
339
+
340
+
341
+ class RemoveAppointmentLabel(_AppointmentLabelBase):
342
+ """
343
+ Effect to remove one or more labels from an appointment.
344
+
345
+ Attributes:
346
+ appointment_id (UUID | str): The ID of the appointment to remove labels from.
347
+ labels (list[str]): A list of label names to remove.
348
+ """
349
+
350
+ class Meta:
351
+ effect_type = EffectType.REMOVE_APPOINTMENT_LABEL
352
+
353
+
354
+ __exports__ = (
355
+ "ScheduleEvent",
356
+ "Appointment",
357
+ "AddAppointmentLabel",
358
+ "RemoveAppointmentLabel",
359
+ )
@@ -1,9 +1,11 @@
1
1
  import datetime
2
+ import json
2
3
  from typing import Any
3
4
  from uuid import UUID
4
5
 
5
6
  from pydantic_core import InitErrorDetails
6
7
 
8
+ from canvas_sdk.effects import Effect
7
9
  from canvas_sdk.effects.note.base import NoteOrAppointmentABC
8
10
  from canvas_sdk.v1.data import Note as NoteModel
9
11
  from canvas_sdk.v1.data import NoteType, Patient
@@ -28,6 +30,14 @@ class Note(NoteOrAppointmentABC):
28
30
  patient_id: str | None = None
29
31
  title: str | None = None
30
32
 
33
+ def push_charges(self) -> Effect:
34
+ """Pushes BillingLineItems from the Note to the associated Claim. Identicial to clicking the Push Charges button in the note footer."""
35
+ self._validate_before_effect("push_charges")
36
+ return Effect(
37
+ type="PUSH_NOTE_CHARGES",
38
+ payload=json.dumps({"note": self.instance_id}),
39
+ )
40
+
31
41
  def _get_error_details(self, method: Any) -> list[InitErrorDetails]:
32
42
  """
33
43
  Validates the note type category and returns a list of error details if validation fails.
@@ -40,6 +50,34 @@ class Note(NoteOrAppointmentABC):
40
50
  """
41
51
  errors = super()._get_error_details(method)
42
52
 
53
+ if method == "push_charges":
54
+ if not self.instance_id:
55
+ errors.append(
56
+ self._create_error_detail(
57
+ "missing",
58
+ "Field 'instance_id' is required to push charges from a note to a claim.",
59
+ None,
60
+ )
61
+ )
62
+ elif not (note := NoteModel.objects.filter(id=self.instance_id).first()):
63
+ errors.append(
64
+ self._create_error_detail(
65
+ "value",
66
+ f"Note with ID {self.instance_id} does not exist.",
67
+ self.instance_id,
68
+ )
69
+ )
70
+ elif not note.note_type_version or not note.note_type_version.is_billable:
71
+ errors.append(
72
+ self._create_error_detail(
73
+ "value",
74
+ f"Note with note type {note.note_type_version} is not billable and has no associated claim.",
75
+ note.note_type_version,
76
+ )
77
+ )
78
+
79
+ return errors
80
+
43
81
  if method == "create":
44
82
  if not self.note_type_id:
45
83
  errors.append(
@@ -1,8 +1,9 @@
1
- from .task import AddTask, AddTaskComment, TaskStatus, UpdateTask
1
+ from .task import AddTask, AddTaskComment, TaskMetadata, TaskStatus, UpdateTask
2
2
 
3
3
  __all__ = __exports__ = (
4
4
  "AddTask",
5
5
  "AddTaskComment",
6
6
  "TaskStatus",
7
+ "TaskMetadata",
7
8
  "UpdateTask",
8
9
  )
@@ -4,8 +4,11 @@ from typing import Any, Self, cast
4
4
  from uuid import UUID
5
5
 
6
6
  from pydantic import model_validator
7
+ from pydantic_core import InitErrorDetails
7
8
 
8
9
  from canvas_sdk.effects.base import EffectType, _BaseEffect
10
+ from canvas_sdk.effects.metadata import BaseMetadata
11
+ from canvas_sdk.v1.data import Task
9
12
 
10
13
 
11
14
  class TaskStatus(Enum):
@@ -140,9 +143,36 @@ class UpdateTask(_BaseEffect):
140
143
  return value_dict
141
144
 
142
145
 
146
+ class TaskMetadata(BaseMetadata):
147
+ """Task Metadata."""
148
+
149
+ class Meta:
150
+ effect_type = "TASK_METADATA"
151
+
152
+ task_id: str
153
+
154
+ def _get_error_details(self, method: Any) -> list[InitErrorDetails]:
155
+ """Get the error details for the effect.
156
+ If task_id is not found, return an error detail.
157
+ """
158
+ errors = super()._get_error_details(method)
159
+
160
+ if not Task.objects.filter(id=self.task_id).exists():
161
+ errors.append(
162
+ self._create_error_detail(
163
+ "task_id",
164
+ f"Task with id: {self.task_id} does not exist.",
165
+ self.task_id,
166
+ )
167
+ )
168
+
169
+ return errors
170
+
171
+
143
172
  __exports__ = (
144
173
  "AddTask",
145
174
  "AddTaskComment",
146
175
  "TaskStatus",
176
+ "TaskMetadata",
147
177
  "UpdateTask",
148
178
  )
@@ -19,6 +19,13 @@ from .staff import (
19
19
  StaffPhotoFactory,
20
20
  StaffRoleFactory,
21
21
  )
22
+ from .task import (
23
+ TaskCommentFactory,
24
+ TaskFactory,
25
+ TaskLabelFactory,
26
+ TaskMetadataFactory,
27
+ TaskTaskLabelFactory,
28
+ )
22
29
  from .user import CanvasUserFactory
23
30
 
24
31
  __all__ = (
@@ -45,4 +52,9 @@ __all__ = (
45
52
  "StaffLicenseFactory",
46
53
  "StaffContactPointFactory",
47
54
  "StaffAddressFactory",
55
+ "TaskCommentFactory",
56
+ "TaskFactory",
57
+ "TaskLabelFactory",
58
+ "TaskMetadataFactory",
59
+ "TaskTaskLabelFactory",
48
60
  )