canvas 0.53.2__py3-none-any.whl → 0.54.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.53.2.dist-info → canvas-0.54.0.dist-info}/METADATA +1 -1
- {canvas-0.53.2.dist-info → canvas-0.54.0.dist-info}/RECORD +25 -21
- canvas_cli/apps/plugin/plugin.py +40 -8
- canvas_generated/messages/effects_pb2.py +2 -2
- canvas_generated/messages/effects_pb2.pyi +12 -0
- canvas_generated/messages/events_pb2.py +2 -2
- canvas_generated/messages/events_pb2.pyi +12 -0
- canvas_sdk/caching/client.py +3 -1
- canvas_sdk/caching/plugins.py +7 -4
- canvas_sdk/effects/payment_processor.py +137 -0
- canvas_sdk/handlers/payment_processors/__init__.py +1 -0
- canvas_sdk/handlers/payment_processors/base.py +93 -0
- canvas_sdk/handlers/payment_processors/card.py +200 -0
- canvas_sdk/questionnaires/utils.py +2 -2
- canvas_sdk/templates/utils.py +5 -3
- canvas_sdk/utils/plugins.py +33 -20
- canvas_sdk/v1/data/patient.py +7 -0
- plugin_runner/allowed-module-imports.json +14 -0
- plugin_runner/installation.py +4 -1
- plugin_runner/plugin_runner.py +23 -0
- plugin_runner/sandbox.py +1 -0
- protobufs/canvas_generated/messages/effects.proto +8 -0
- protobufs/canvas_generated/messages/events.proto +8 -0
- {canvas-0.53.2.dist-info → canvas-0.54.0.dist-info}/WHEEL +0 -0
- {canvas-0.53.2.dist-info → canvas-0.54.0.dist-info}/entry_points.txt +0 -0
canvas_sdk/caching/plugins.py
CHANGED
|
@@ -3,18 +3,21 @@ from __future__ import annotations
|
|
|
3
3
|
from typing import TYPE_CHECKING, Any
|
|
4
4
|
|
|
5
5
|
from canvas_sdk.caching.client import get_cache as get_cache_client
|
|
6
|
-
from canvas_sdk.utils.plugins import
|
|
6
|
+
from canvas_sdk.utils.plugins import plugin_context
|
|
7
7
|
from settings import CANVAS_SDK_CACHE_TIMEOUT_SECONDS
|
|
8
8
|
|
|
9
9
|
if TYPE_CHECKING:
|
|
10
10
|
from canvas_sdk.caching.base import Cache
|
|
11
11
|
|
|
12
12
|
|
|
13
|
-
@
|
|
13
|
+
@plugin_context
|
|
14
14
|
def get_cache(**kwargs: Any) -> Cache:
|
|
15
15
|
"""Get the cache client for plugins."""
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
return get_cache_client(
|
|
17
|
+
driver="plugins",
|
|
18
|
+
prefix=kwargs["plugin_name"],
|
|
19
|
+
max_timeout_seconds=CANVAS_SDK_CACHE_TIMEOUT_SECONDS,
|
|
20
|
+
)
|
|
18
21
|
|
|
19
22
|
|
|
20
23
|
__exports__ = ("get_cache",)
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
from enum import StrEnum
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
from canvas_sdk.effects import EffectType, _BaseEffect
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class PaymentProcessorMetadata(_BaseEffect):
|
|
8
|
+
"""PaymentProcessorInfo effect class."""
|
|
9
|
+
|
|
10
|
+
class Meta:
|
|
11
|
+
effect_type = EffectType.REVENUE__PAYMENT_PROCESSOR__METADATA
|
|
12
|
+
|
|
13
|
+
class PaymentProcessorType(StrEnum):
|
|
14
|
+
"""Enum for payment processor types."""
|
|
15
|
+
|
|
16
|
+
CARD = "card"
|
|
17
|
+
|
|
18
|
+
identifier: str
|
|
19
|
+
type: PaymentProcessorType
|
|
20
|
+
|
|
21
|
+
@property
|
|
22
|
+
def values(self) -> dict[str, Any]:
|
|
23
|
+
"""Return the values of the PaymentProcessorMetadata."""
|
|
24
|
+
return {
|
|
25
|
+
"identifier": self.identifier,
|
|
26
|
+
"type": self.type.value,
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class PaymentProcessorForm(_BaseEffect):
|
|
31
|
+
"""PaymentProcessorForm effect class."""
|
|
32
|
+
|
|
33
|
+
class Meta:
|
|
34
|
+
effect_type = EffectType.REVENUE__PAYMENT_PROCESSOR__FORM
|
|
35
|
+
|
|
36
|
+
intent: str
|
|
37
|
+
content: str
|
|
38
|
+
|
|
39
|
+
@property
|
|
40
|
+
def values(self) -> dict[str, Any]:
|
|
41
|
+
"""Return the values of the PaymentProcessorMetadata."""
|
|
42
|
+
return {
|
|
43
|
+
"intent": self.intent,
|
|
44
|
+
"content": self.content,
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class CardTransaction(_BaseEffect):
|
|
49
|
+
"""CardTransaction effect class."""
|
|
50
|
+
|
|
51
|
+
class Meta:
|
|
52
|
+
effect_type = EffectType.REVENUE__PAYMENT_PROCESSOR__CREDIT_CARD_TRANSACTION
|
|
53
|
+
|
|
54
|
+
success: bool
|
|
55
|
+
transaction_id: str | None
|
|
56
|
+
error_code: str | None = None
|
|
57
|
+
api_response: dict
|
|
58
|
+
|
|
59
|
+
@property
|
|
60
|
+
def values(self) -> dict[str, Any]:
|
|
61
|
+
"""Return the values of the CreditCardTransaction."""
|
|
62
|
+
return {
|
|
63
|
+
"success": self.success,
|
|
64
|
+
"transaction_id": self.transaction_id,
|
|
65
|
+
"api_response": self.api_response,
|
|
66
|
+
"error_code": self.error_code,
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class PaymentMethod(_BaseEffect):
|
|
71
|
+
"""PaymentMethod effect class."""
|
|
72
|
+
|
|
73
|
+
class Meta:
|
|
74
|
+
effect_type = EffectType.REVENUE__PAYMENT_PROCESSOR__PAYMENT_METHOD
|
|
75
|
+
|
|
76
|
+
payment_method_id: str
|
|
77
|
+
card_holder_name: str | None
|
|
78
|
+
brand: str
|
|
79
|
+
postal_code: str | None = None
|
|
80
|
+
country: str | None = None
|
|
81
|
+
expiration_year: int
|
|
82
|
+
expiration_month: int
|
|
83
|
+
card_last_four_digits: str
|
|
84
|
+
|
|
85
|
+
@property
|
|
86
|
+
def values(self) -> dict[str, Any]:
|
|
87
|
+
"""Return the values of the PaymentMethod."""
|
|
88
|
+
return {
|
|
89
|
+
"payment_method_id": self.payment_method_id,
|
|
90
|
+
"card_holder_name": self.card_holder_name,
|
|
91
|
+
"brand": self.brand,
|
|
92
|
+
"postal_code": self.postal_code,
|
|
93
|
+
"country": self.country,
|
|
94
|
+
"expiration_year": self.expiration_year,
|
|
95
|
+
"expiration_month": self.expiration_month,
|
|
96
|
+
"card_last_four_digits": self.card_last_four_digits,
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class AddPaymentMethodResponse(_BaseEffect):
|
|
101
|
+
"""AddPaymentMethodResponse effect class."""
|
|
102
|
+
|
|
103
|
+
class Meta:
|
|
104
|
+
effect_type = EffectType.REVENUE__PAYMENT_PROCESSOR__PAYMENT_METHOD__ADD_RESPONSE
|
|
105
|
+
|
|
106
|
+
success: bool
|
|
107
|
+
|
|
108
|
+
@property
|
|
109
|
+
def values(self) -> dict[str, Any]:
|
|
110
|
+
"""Return the values of the AddPaymentMethodResponse."""
|
|
111
|
+
return {"success": self.success}
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class RemovePaymentMethodResponse(_BaseEffect):
|
|
115
|
+
"""RemovePaymentMethodResponse effect class."""
|
|
116
|
+
|
|
117
|
+
class Meta:
|
|
118
|
+
effect_type = EffectType.REVENUE__PAYMENT_PROCESSOR__PAYMENT_METHOD__REMOVE_RESPONSE
|
|
119
|
+
|
|
120
|
+
success: bool
|
|
121
|
+
|
|
122
|
+
@property
|
|
123
|
+
def values(self) -> dict[str, Any]:
|
|
124
|
+
"""Return the values of the RemovePaymentMethodResponse."""
|
|
125
|
+
return {
|
|
126
|
+
"success": self.success,
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
__exports__ = (
|
|
131
|
+
"PaymentProcessorMetadata",
|
|
132
|
+
"PaymentProcessorForm",
|
|
133
|
+
"CardTransaction",
|
|
134
|
+
"PaymentMethod",
|
|
135
|
+
"AddPaymentMethodResponse",
|
|
136
|
+
"RemovePaymentMethodResponse",
|
|
137
|
+
)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__exports__ = ()
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
from abc import ABC
|
|
3
|
+
|
|
4
|
+
from canvas_sdk.effects import Effect
|
|
5
|
+
from canvas_sdk.effects.payment_processor import PaymentProcessorMetadata
|
|
6
|
+
from canvas_sdk.events import EventType
|
|
7
|
+
from canvas_sdk.handlers import BaseHandler
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class PaymentProcessor(BaseHandler, ABC):
|
|
11
|
+
"""
|
|
12
|
+
Abstract Base class for payment processors.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
RESPONDS_TO = [
|
|
16
|
+
EventType.Name(EventType.REVENUE__PAYMENT_PROCESSOR__LIST),
|
|
17
|
+
EventType.Name(EventType.REVENUE__PAYMENT_PROCESSOR__SELECTED),
|
|
18
|
+
EventType.Name(EventType.REVENUE__PAYMENT_PROCESSOR__CHARGE),
|
|
19
|
+
EventType.Name(EventType.REVENUE__PAYMENT_PROCESSOR__PAYMENT_METHODS__LIST),
|
|
20
|
+
EventType.Name(EventType.REVENUE__PAYMENT_PROCESSOR__PAYMENT_METHODS__ADD),
|
|
21
|
+
EventType.Name(EventType.REVENUE__PAYMENT_PROCESSOR__PAYMENT_METHODS__REMOVE),
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
TYPE: PaymentProcessorMetadata.PaymentProcessorType
|
|
25
|
+
|
|
26
|
+
@property
|
|
27
|
+
def identifier(self) -> str:
|
|
28
|
+
"""The application identifier."""
|
|
29
|
+
identifier = f"{self.__class__.__module__}:{self.__class__.__qualname__}"
|
|
30
|
+
|
|
31
|
+
return base64.b64encode(identifier.encode("utf-8")).decode("utf-8")
|
|
32
|
+
|
|
33
|
+
def compute(self) -> list[Effect]:
|
|
34
|
+
"""Compute the effects to be applied."""
|
|
35
|
+
if self.event.type == EventType.REVENUE__PAYMENT_PROCESSOR__LIST and (
|
|
36
|
+
"payment_type" not in self.event.context
|
|
37
|
+
or self.event.context["payment_type"] == self.TYPE
|
|
38
|
+
):
|
|
39
|
+
return [self.metadata().apply()]
|
|
40
|
+
elif self.event.type == EventType.REVENUE__PAYMENT_PROCESSOR__SELECTED:
|
|
41
|
+
effects = self._on_payment_processor_selected()
|
|
42
|
+
return effects
|
|
43
|
+
elif self.event.type == EventType.REVENUE__PAYMENT_PROCESSOR__CHARGE:
|
|
44
|
+
effect = self._charge()
|
|
45
|
+
return [effect] if effect else []
|
|
46
|
+
elif self.event.type == EventType.REVENUE__PAYMENT_PROCESSOR__PAYMENT_METHODS__LIST:
|
|
47
|
+
# This event is used to list payment methods, which may not be applicable for all processors.
|
|
48
|
+
# Subclasses should override this method if they support listing payment methods.
|
|
49
|
+
return self._payment_methods()
|
|
50
|
+
elif self.event.type == EventType.REVENUE__PAYMENT_PROCESSOR__PAYMENT_METHODS__ADD:
|
|
51
|
+
effect = self._add_payment_method()
|
|
52
|
+
return [effect] if effect else []
|
|
53
|
+
elif self.event.type == EventType.REVENUE__PAYMENT_PROCESSOR__PAYMENT_METHODS__REMOVE:
|
|
54
|
+
effect = self._remove_payment_method()
|
|
55
|
+
return [effect] if effect else []
|
|
56
|
+
|
|
57
|
+
return []
|
|
58
|
+
|
|
59
|
+
def metadata(self) -> PaymentProcessorMetadata:
|
|
60
|
+
"""Return information about the payment processor."""
|
|
61
|
+
return PaymentProcessorMetadata(identifier=self.identifier, type=self.TYPE)
|
|
62
|
+
|
|
63
|
+
def _on_payment_processor_selected(self) -> list[Effect]:
|
|
64
|
+
"""Handle the event when a payment processor is selected."""
|
|
65
|
+
# This method should be overridden by subclasses to handle specific logic
|
|
66
|
+
# when a payment processor is selected.
|
|
67
|
+
return []
|
|
68
|
+
|
|
69
|
+
def _charge(self) -> Effect | None:
|
|
70
|
+
"""Handle the event when a charge is made."""
|
|
71
|
+
# This method should be overridden by subclasses to handle specific logic
|
|
72
|
+
# when a charge is made.
|
|
73
|
+
return None
|
|
74
|
+
|
|
75
|
+
def _payment_methods(self) -> list[Effect]:
|
|
76
|
+
"""List payment methods for the processor."""
|
|
77
|
+
# This method should be overridden by subclasses if they support listing payment methods.
|
|
78
|
+
return []
|
|
79
|
+
|
|
80
|
+
def _add_payment_method(self) -> Effect | None:
|
|
81
|
+
"""Handle the event when a payment method is added."""
|
|
82
|
+
# This method should be overridden by subclasses to handle specific logic
|
|
83
|
+
# when a payment method is added.
|
|
84
|
+
return None
|
|
85
|
+
|
|
86
|
+
def _remove_payment_method(self) -> Effect | None:
|
|
87
|
+
"""Handle the event when a payment method is removed."""
|
|
88
|
+
# This method should be overridden by subclasses to handle specific logic
|
|
89
|
+
# when a payment method is removed.
|
|
90
|
+
return None
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
__exports__ = ("PaymentProcessor",)
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
from decimal import Decimal
|
|
3
|
+
from enum import StrEnum
|
|
4
|
+
|
|
5
|
+
from canvas_sdk.effects import Effect
|
|
6
|
+
from canvas_sdk.effects.payment_processor import (
|
|
7
|
+
AddPaymentMethodResponse,
|
|
8
|
+
CardTransaction,
|
|
9
|
+
PaymentMethod,
|
|
10
|
+
PaymentProcessorForm,
|
|
11
|
+
PaymentProcessorMetadata,
|
|
12
|
+
RemovePaymentMethodResponse,
|
|
13
|
+
)
|
|
14
|
+
from canvas_sdk.handlers.payment_processors.base import PaymentProcessor
|
|
15
|
+
from canvas_sdk.v1.data import Patient
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class CardPaymentProcessor(PaymentProcessor, ABC):
|
|
19
|
+
"""Base Card Payment Processor Handler."""
|
|
20
|
+
|
|
21
|
+
TYPE = PaymentProcessorMetadata.PaymentProcessorType.CARD
|
|
22
|
+
|
|
23
|
+
class PaymentIntent(StrEnum):
|
|
24
|
+
"""Enum for payment actions."""
|
|
25
|
+
|
|
26
|
+
ADD_CARD = "add_card"
|
|
27
|
+
PAY = "pay"
|
|
28
|
+
|
|
29
|
+
def _on_payment_processor_selected(self) -> list[Effect]:
|
|
30
|
+
"""Handle the event when a payment processor is selected."""
|
|
31
|
+
if self.event.context.get("identifier") == self.identifier:
|
|
32
|
+
intent = self.event.context.get("intent")
|
|
33
|
+
effects = self.on_payment_processor_selected(intent=intent)
|
|
34
|
+
return [effect.apply() for effect in effects]
|
|
35
|
+
|
|
36
|
+
return []
|
|
37
|
+
|
|
38
|
+
def _charge(self) -> Effect | None:
|
|
39
|
+
"""Handle the event when a charge is made."""
|
|
40
|
+
if self.event.context.get("identifier") == self.identifier:
|
|
41
|
+
patient = (
|
|
42
|
+
Patient.objects.get(id=self.event.context.get("patient", {}).get("id"))
|
|
43
|
+
if self.event.context.get("patient")
|
|
44
|
+
else None
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
amount = Decimal(str(self.event.context.get("amount")))
|
|
48
|
+
token = str(self.event.context.get("token"))
|
|
49
|
+
effect = self.charge(amount=amount, token=token, patient=patient)
|
|
50
|
+
|
|
51
|
+
return effect.apply() if effect else None
|
|
52
|
+
|
|
53
|
+
return None
|
|
54
|
+
|
|
55
|
+
def _payment_methods(self) -> list[Effect]:
|
|
56
|
+
"""List payment methods for the card payment processor."""
|
|
57
|
+
if self.event.context.get("identifier") == self.identifier:
|
|
58
|
+
patient = (
|
|
59
|
+
Patient.objects.get(id=self.event.context.get("patient", {}).get("id"))
|
|
60
|
+
if self.event.context.get("patient")
|
|
61
|
+
else None
|
|
62
|
+
)
|
|
63
|
+
effects = self.payment_methods(patient=patient)
|
|
64
|
+
return [effect.apply() for effect in effects]
|
|
65
|
+
|
|
66
|
+
return []
|
|
67
|
+
|
|
68
|
+
def _add_payment_method(self) -> Effect | None:
|
|
69
|
+
"""Handle the event when a card is added."""
|
|
70
|
+
if self.event.context.get("identifier") == self.identifier:
|
|
71
|
+
patient = (
|
|
72
|
+
Patient.objects.get(id=self.event.context.get("patient", {}).get("id"))
|
|
73
|
+
if self.event.context.get("patient")
|
|
74
|
+
else None
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
if not patient:
|
|
78
|
+
return None
|
|
79
|
+
|
|
80
|
+
token = str(self.event.context.get("token"))
|
|
81
|
+
effect = self.add_payment_method(token=token, patient=patient)
|
|
82
|
+
return effect.apply() if effect else None
|
|
83
|
+
|
|
84
|
+
return None
|
|
85
|
+
|
|
86
|
+
def _remove_payment_method(self) -> Effect | None:
|
|
87
|
+
"""Handle the event when a payment method is removed."""
|
|
88
|
+
if self.event.context.get("identifier") == self.identifier:
|
|
89
|
+
patient = (
|
|
90
|
+
Patient.objects.get(id=self.event.context.get("patient", {}).get("id"))
|
|
91
|
+
if self.event.context.get("patient")
|
|
92
|
+
else None
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
if not patient:
|
|
96
|
+
return None
|
|
97
|
+
|
|
98
|
+
token = str(self.event.context.get("token"))
|
|
99
|
+
effect = self.remove_payment_method(token=token, patient=patient)
|
|
100
|
+
return effect.apply() if effect else None
|
|
101
|
+
|
|
102
|
+
return None
|
|
103
|
+
|
|
104
|
+
def on_payment_processor_selected(self, intent: str | None) -> list[PaymentProcessorForm]:
|
|
105
|
+
"""Handle the event when a payment processor is selected."""
|
|
106
|
+
patient = (
|
|
107
|
+
Patient.objects.get(id=self.event.context.get("patient", {}).get("id"))
|
|
108
|
+
if self.event.context.get("patient")
|
|
109
|
+
else None
|
|
110
|
+
)
|
|
111
|
+
match intent:
|
|
112
|
+
case self.PaymentIntent.ADD_CARD:
|
|
113
|
+
return [self.add_card_form(patient)]
|
|
114
|
+
case self.PaymentIntent.PAY:
|
|
115
|
+
return [self.payment_form(patient)]
|
|
116
|
+
case None:
|
|
117
|
+
return [self.payment_form(patient), self.add_card_form(patient)]
|
|
118
|
+
case _:
|
|
119
|
+
return []
|
|
120
|
+
|
|
121
|
+
@abstractmethod
|
|
122
|
+
def payment_form(self, patient: Patient | None = None) -> PaymentProcessorForm:
|
|
123
|
+
"""Return the payment form for the credit card processor.
|
|
124
|
+
|
|
125
|
+
Args:
|
|
126
|
+
patient (Patient | None): The patient for whom the payment is being processed.
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
PaymentProcessorForm: The form for processing the payment.
|
|
130
|
+
"""
|
|
131
|
+
raise NotImplementedError("Subclasses must implement the payment_form method.")
|
|
132
|
+
|
|
133
|
+
@abstractmethod
|
|
134
|
+
def add_card_form(self, patient: Patient | None = None) -> PaymentProcessorForm:
|
|
135
|
+
"""Return the form for adding a card.
|
|
136
|
+
|
|
137
|
+
Args:
|
|
138
|
+
patient (Patient | None): The patient for whom the add is being added.
|
|
139
|
+
|
|
140
|
+
Returns:
|
|
141
|
+
PaymentProcessorForm: The form for adding a card.
|
|
142
|
+
"""
|
|
143
|
+
raise NotImplementedError("Subclasses must implement the add_card_form method.")
|
|
144
|
+
|
|
145
|
+
@abstractmethod
|
|
146
|
+
def charge(
|
|
147
|
+
self, amount: Decimal, token: str, patient: Patient | None = None
|
|
148
|
+
) -> CardTransaction:
|
|
149
|
+
"""Charge a credit/debit card using the provided token.
|
|
150
|
+
|
|
151
|
+
Args:
|
|
152
|
+
amount (Decimal): The amount to charge.
|
|
153
|
+
token (str): The token representing the credit card.
|
|
154
|
+
patient (Patient | None): The patient for whom the charge is being made.
|
|
155
|
+
|
|
156
|
+
Returns:
|
|
157
|
+
CardTransaction: The result of the card transaction.
|
|
158
|
+
"""
|
|
159
|
+
raise NotImplementedError("Subclasses must implement the charge method.")
|
|
160
|
+
|
|
161
|
+
@abstractmethod
|
|
162
|
+
def payment_methods(self, patient: Patient | None = None) -> list[PaymentMethod]:
|
|
163
|
+
"""List payment methods for the card payment processor.
|
|
164
|
+
|
|
165
|
+
Args:
|
|
166
|
+
patient (Patient | None): The patient for whom the payment methods are being listed.
|
|
167
|
+
|
|
168
|
+
Returns:
|
|
169
|
+
list[PaymentMethod]: A list of payment methods available for the card payment processor.
|
|
170
|
+
"""
|
|
171
|
+
raise NotImplementedError("Subclasses must implement the payment_methods method.")
|
|
172
|
+
|
|
173
|
+
@abstractmethod
|
|
174
|
+
def add_payment_method(self, token: str, patient: Patient) -> AddPaymentMethodResponse:
|
|
175
|
+
"""Add a payment method for the card payment processor.
|
|
176
|
+
|
|
177
|
+
Args:
|
|
178
|
+
token (str): The token representing the payment method.
|
|
179
|
+
patient (Patient): The patient for whom the payment method is being added.
|
|
180
|
+
|
|
181
|
+
Returns:
|
|
182
|
+
AddPaymentMethodResponse: The response indicating the result of the addition operation.
|
|
183
|
+
"""
|
|
184
|
+
raise NotImplementedError("Subclasses must implement the add_payment_method method.")
|
|
185
|
+
|
|
186
|
+
@abstractmethod
|
|
187
|
+
def remove_payment_method(self, token: str, patient: Patient) -> RemovePaymentMethodResponse:
|
|
188
|
+
"""Remove a payment method for the card payment processor.
|
|
189
|
+
|
|
190
|
+
Args:
|
|
191
|
+
token (str): The token representing the payment method to be removed.
|
|
192
|
+
patient (Patient): The patient for whom the payment method is being removed.
|
|
193
|
+
|
|
194
|
+
Returns:
|
|
195
|
+
RemovePaymentMethodResponse: The response indicating the result of the removal operation.
|
|
196
|
+
"""
|
|
197
|
+
raise NotImplementedError("Subclasses must implement the remove_payment_method method.")
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
__exports__ = ("CardPaymentProcessor",)
|
|
@@ -7,7 +7,7 @@ from typing import Any, TypedDict
|
|
|
7
7
|
import yaml
|
|
8
8
|
from jsonschema import Draft7Validator, validators
|
|
9
9
|
|
|
10
|
-
from canvas_sdk.utils.plugins import
|
|
10
|
+
from canvas_sdk.utils.plugins import plugin_context
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
class Response(TypedDict):
|
|
@@ -75,7 +75,7 @@ def extend_with_defaults(validator_class: type[Draft7Validator]) -> type[Draft7V
|
|
|
75
75
|
ExtendedDraft7Validator = extend_with_defaults(Draft7Validator)
|
|
76
76
|
|
|
77
77
|
|
|
78
|
-
@
|
|
78
|
+
@plugin_context
|
|
79
79
|
def from_yaml(questionnaire_name: str, **kwargs: Any) -> QuestionnaireConfig | None:
|
|
80
80
|
"""Load a Questionnaire configuration from a YAML file.
|
|
81
81
|
|
canvas_sdk/templates/utils.py
CHANGED
|
@@ -3,12 +3,14 @@ from typing import Any
|
|
|
3
3
|
|
|
4
4
|
from django.template import Context, Template
|
|
5
5
|
|
|
6
|
-
from canvas_sdk.utils.plugins import
|
|
6
|
+
from canvas_sdk.utils.plugins import plugin_context
|
|
7
7
|
|
|
8
8
|
|
|
9
|
-
@
|
|
9
|
+
@plugin_context
|
|
10
10
|
def render_to_string(
|
|
11
|
-
template_name: str,
|
|
11
|
+
template_name: str,
|
|
12
|
+
context: dict[str, Any] | None = None,
|
|
13
|
+
**kwargs: Any,
|
|
12
14
|
) -> str | None:
|
|
13
15
|
"""Load a template and render it with the given context.
|
|
14
16
|
|
canvas_sdk/utils/plugins.py
CHANGED
|
@@ -8,19 +8,39 @@ from canvas_sdk.utils.metrics import measured
|
|
|
8
8
|
from settings import PLUGIN_DIRECTORY
|
|
9
9
|
|
|
10
10
|
|
|
11
|
+
def find_plugin_ancestor(frame: FrameType | None, max_depth: int = 10) -> FrameType | None:
|
|
12
|
+
"""
|
|
13
|
+
Recurse backwards to find any plugin ancestor of this frame.
|
|
14
|
+
"""
|
|
15
|
+
parent_frame = frame.f_back if frame else None
|
|
16
|
+
|
|
17
|
+
if not parent_frame:
|
|
18
|
+
return None
|
|
19
|
+
|
|
20
|
+
if max_depth == 0:
|
|
21
|
+
return None
|
|
22
|
+
|
|
23
|
+
if "__is_plugin__" in parent_frame.f_globals:
|
|
24
|
+
return parent_frame
|
|
25
|
+
|
|
26
|
+
return find_plugin_ancestor(frame=parent_frame, max_depth=max_depth - 1)
|
|
27
|
+
|
|
28
|
+
|
|
11
29
|
@measured
|
|
12
|
-
def
|
|
30
|
+
def plugin_context(func: Callable[..., Any]) -> Callable[..., Any]:
|
|
13
31
|
"""Decorator to restrict a function's execution to plugins only."""
|
|
14
32
|
|
|
15
33
|
def wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
16
|
-
|
|
17
|
-
caller = current_frame.f_back if current_frame else None
|
|
34
|
+
plugin_frame = find_plugin_ancestor(inspect.currentframe())
|
|
18
35
|
|
|
19
|
-
if not
|
|
20
|
-
|
|
36
|
+
if not plugin_frame:
|
|
37
|
+
raise RuntimeError(
|
|
38
|
+
"Method that expected plugin context was called from outside a plugin."
|
|
39
|
+
)
|
|
21
40
|
|
|
22
|
-
plugin_name =
|
|
41
|
+
plugin_name = plugin_frame.f_globals["__name__"].split(".")[0]
|
|
23
42
|
plugin_dir = Path(PLUGIN_DIRECTORY) / plugin_name
|
|
43
|
+
|
|
24
44
|
kwargs["plugin_name"] = plugin_name
|
|
25
45
|
kwargs["plugin_dir"] = plugin_dir.resolve()
|
|
26
46
|
|
|
@@ -30,24 +50,17 @@ def plugin_only(func: Callable[..., Any]) -> Callable[..., Any]:
|
|
|
30
50
|
|
|
31
51
|
|
|
32
52
|
@measured
|
|
33
|
-
def is_plugin_caller(
|
|
53
|
+
def is_plugin_caller() -> tuple[bool, str | None]:
|
|
34
54
|
"""Check if a function is called from a plugin."""
|
|
35
|
-
|
|
36
|
-
caller = current_frame.f_back if current_frame else None
|
|
37
|
-
|
|
38
|
-
if not caller:
|
|
39
|
-
return False, None
|
|
55
|
+
plugin_frame = find_plugin_ancestor(inspect.currentframe())
|
|
40
56
|
|
|
41
|
-
if
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
else:
|
|
45
|
-
return False, None
|
|
57
|
+
if plugin_frame:
|
|
58
|
+
module = plugin_frame.f_globals.get("__name__")
|
|
59
|
+
qualname = plugin_frame.f_code.co_qualname
|
|
46
60
|
|
|
47
|
-
|
|
48
|
-
qualname = caller.f_code.co_qualname
|
|
61
|
+
return True, f"{module}.{qualname}"
|
|
49
62
|
|
|
50
|
-
return
|
|
63
|
+
return False, None
|
|
51
64
|
|
|
52
65
|
|
|
53
66
|
__exports__ = ()
|
canvas_sdk/v1/data/patient.py
CHANGED
|
@@ -99,6 +99,13 @@ class Patient(Model):
|
|
|
99
99
|
def __str__(self) -> str:
|
|
100
100
|
return f"{self.first_name} {self.last_name}"
|
|
101
101
|
|
|
102
|
+
@property
|
|
103
|
+
def full_name(self) -> str:
|
|
104
|
+
"""Returns the patient's full name."""
|
|
105
|
+
return " ".join(
|
|
106
|
+
n for n in (self.first_name, self.middle_name, self.last_name, self.suffix) if n
|
|
107
|
+
)
|
|
108
|
+
|
|
102
109
|
def age_at(self, time: arrow.Arrow) -> float:
|
|
103
110
|
"""Given a datetime, returns what the patient's age would be at that datetime."""
|
|
104
111
|
age = float(0)
|
|
@@ -279,6 +279,14 @@
|
|
|
279
279
|
"canvas_sdk.effects.patient_profile_configuration": [
|
|
280
280
|
"PatientProfileConfiguration"
|
|
281
281
|
],
|
|
282
|
+
"canvas_sdk.effects.payment_processor": [
|
|
283
|
+
"AddPaymentMethodResponse",
|
|
284
|
+
"CardTransaction",
|
|
285
|
+
"PaymentMethod",
|
|
286
|
+
"PaymentProcessorForm",
|
|
287
|
+
"PaymentProcessorMetadata",
|
|
288
|
+
"RemovePaymentMethodResponse"
|
|
289
|
+
],
|
|
282
290
|
"canvas_sdk.effects.protocol_card": [
|
|
283
291
|
"ProtocolCard",
|
|
284
292
|
"Recommendation"
|
|
@@ -364,6 +372,12 @@
|
|
|
364
372
|
"canvas_sdk.handlers.cron_task": [
|
|
365
373
|
"CronTask"
|
|
366
374
|
],
|
|
375
|
+
"canvas_sdk.handlers.payment_processors.base": [
|
|
376
|
+
"PaymentProcessor"
|
|
377
|
+
],
|
|
378
|
+
"canvas_sdk.handlers.payment_processors.card": [
|
|
379
|
+
"CardPaymentProcessor"
|
|
380
|
+
],
|
|
367
381
|
"canvas_sdk.handlers.simple_api": [
|
|
368
382
|
"APIKeyAuthMixin",
|
|
369
383
|
"APIKeyCredentials",
|
plugin_runner/installation.py
CHANGED
|
@@ -147,6 +147,8 @@ def download_plugin(plugin_package: str) -> Generator[Path, None, None]:
|
|
|
147
147
|
|
|
148
148
|
with open(download_path, "wb") as download_file:
|
|
149
149
|
response = requests.request(method=method, url=f"https://{host}{path}", headers=headers)
|
|
150
|
+
response.raise_for_status()
|
|
151
|
+
|
|
150
152
|
download_file.write(response.content)
|
|
151
153
|
|
|
152
154
|
yield download_path
|
|
@@ -168,7 +170,8 @@ def install_plugin(plugin_name: str, attributes: PluginAttributes) -> None:
|
|
|
168
170
|
|
|
169
171
|
install_plugin_secrets(plugin_name=plugin_name, secrets=attributes["secrets"])
|
|
170
172
|
except Exception as e:
|
|
171
|
-
log.error(f'Failed to install plugin "{plugin_name}", version {attributes["version"]}')
|
|
173
|
+
log.error(f'Failed to install plugin "{plugin_name}", version {attributes["version"]}: {e}')
|
|
174
|
+
|
|
172
175
|
sentry_sdk.capture_exception(e)
|
|
173
176
|
|
|
174
177
|
raise PluginInstallationError() from e
|
plugin_runner/plugin_runner.py
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import base64
|
|
1
2
|
import json
|
|
2
3
|
import os
|
|
3
4
|
import pathlib
|
|
@@ -213,6 +214,28 @@ class PluginRunner(PluginRunnerServicer):
|
|
|
213
214
|
# respond to SimpleAPI request events are not relevant
|
|
214
215
|
plugin_name = event.context["plugin_name"]
|
|
215
216
|
relevant_plugins = [p for p in relevant_plugins if p.startswith(f"{plugin_name}:")]
|
|
217
|
+
elif event_type in {
|
|
218
|
+
EventType.REVENUE__PAYMENT_PROCESSOR__CHARGE,
|
|
219
|
+
EventType.REVENUE__PAYMENT_PROCESSOR__SELECTED,
|
|
220
|
+
EventType.REVENUE__PAYMENT_PROCESSOR__PAYMENT_METHODS__LIST,
|
|
221
|
+
EventType.REVENUE__PAYMENT_PROCESSOR__PAYMENT_METHODS__ADD,
|
|
222
|
+
EventType.REVENUE__PAYMENT_PROCESSOR__PAYMENT_METHODS__REMOVE,
|
|
223
|
+
}:
|
|
224
|
+
# The target plugin's name will be part of the payment processor identifier, so other plugins that
|
|
225
|
+
# respond to payment processor charge events are not relevant
|
|
226
|
+
try:
|
|
227
|
+
plugin_name = (
|
|
228
|
+
base64.b64decode(event.context["identifier"]).decode("utf-8").split(".")[0]
|
|
229
|
+
)
|
|
230
|
+
relevant_plugins = [
|
|
231
|
+
p for p in relevant_plugins if p.startswith(f"{plugin_name}:")
|
|
232
|
+
]
|
|
233
|
+
except Exception as ex:
|
|
234
|
+
log.error(
|
|
235
|
+
f"Failed to decode identifier for event {event_name} with context {event.context}"
|
|
236
|
+
)
|
|
237
|
+
sentry_sdk.capture_exception(ex)
|
|
238
|
+
relevant_plugins = []
|
|
216
239
|
|
|
217
240
|
effect_list = []
|
|
218
241
|
|