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.
- {canvas-0.72.1.dist-info → canvas-0.74.0.dist-info}/METADATA +1 -1
- {canvas-0.72.1.dist-info → canvas-0.74.0.dist-info}/RECORD +28 -22
- canvas_generated/messages/effects_pb2.py +2 -2
- canvas_generated/messages/effects_pb2.pyi +16 -0
- canvas_generated/messages/events_pb2.py +2 -2
- canvas_generated/messages/events_pb2.pyi +20 -0
- canvas_sdk/effects/claim_label.py +93 -0
- canvas_sdk/effects/claim_line_item.py +47 -0
- canvas_sdk/effects/fax/__init__.py +3 -0
- canvas_sdk/effects/fax/base.py +77 -0
- canvas_sdk/effects/fax/note.py +42 -0
- canvas_sdk/effects/note/__init__.py +8 -1
- canvas_sdk/effects/note/appointment.py +130 -4
- canvas_sdk/effects/note/note.py +38 -0
- canvas_sdk/effects/task/__init__.py +2 -1
- canvas_sdk/effects/task/task.py +30 -0
- canvas_sdk/test_utils/factories/__init__.py +12 -0
- canvas_sdk/test_utils/factories/task.py +66 -0
- canvas_sdk/v1/data/__init__.py +9 -2
- canvas_sdk/v1/data/appointment.py +17 -1
- canvas_sdk/v1/data/claim_line_item.py +2 -2
- canvas_sdk/v1/data/facility.py +1 -0
- canvas_sdk/v1/data/task.py +16 -0
- plugin_runner/allowed-module-imports.json +21 -0
- protobufs/canvas_generated/messages/effects.proto +11 -0
- protobufs/canvas_generated/messages/events.proto +11 -0
- {canvas-0.72.1.dist-info → canvas-0.74.0.dist-info}/WHEEL +0 -0
- {canvas-0.72.1.dist-info → canvas-0.74.0.dist-info}/entry_points.txt +0 -0
|
@@ -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,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
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
+
)
|
canvas_sdk/effects/note/note.py
CHANGED
|
@@ -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
|
)
|
canvas_sdk/effects/task/task.py
CHANGED
|
@@ -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
|
)
|