diff --git a/openedx_events/enterprise/data.py b/openedx_events/enterprise/data.py index 23c3dd01..915786f8 100644 --- a/openedx_events/enterprise/data.py +++ b/openedx_events/enterprise/data.py @@ -4,8 +4,11 @@ These attributes follow the form of attr objects specified in OEP-49 data pattern. """ +from datetime import datetime +from uuid import UUID import attr +from opaque_keys.edx.keys import CourseKey @attr.s(frozen=True) @@ -22,3 +25,64 @@ class SubsidyRedemption: subsidy_identifier = attr.ib(type=str) content_key = attr.ib(type=str) lms_user_id = attr.ib(type=int) + + +@attr.s(frozen=True) +class LedgerTransactionReversal: + """ + Attributes of an ``openedx_ledger.Reversal`` record. + + Arguments: + uuid (str): Primary identifier of the record. + created (datetime): When the record was created. + modified (datetime): When the record was last modified. + idempotency_key (str): Client-generated unique value to achieve idempotency of operations. + quantity (int): How many units of value this reversal represents (e.g. USD cents). + state (str): Current lifecyle state of the record, one of (created, pending, committed, failed). + metadata (dict): Additional/custom metadata related to this record. + """ + uuid = attr.ib(type=UUID) + created = attr.ib(type=datetime) + modified = attr.ib(type=datetime) + idempotency_key = attr.ib(type=str) + quantity = attr.ib(type=int) + state = attr.ib(type=str) + metadata = attr.ib(type=dict) + + +@attr.s(frozen=True) +class LedgerTransaction: + """ + Attributes of an ``openedx_ledger.Transaction`` record. + + Arguments: + uuid (UUID): Primary identifier of the Transaction. + created (datetime): When the record was created. + modified (datetime): When the record was last modified. + idempotency_key (str): Client-generated unique value to achieve idempotency of operations. + quantity (int): How many units of value this transaction represents (e.g. USD cents). + state (str): Current lifecyle state of the record, one of (created, pending, committed, failed). + metadata (dict): Additional/custom metadata related to this record. + ledger_uuid (UUID): The primary identifier of this Transaction's ledger object. + subsidy_access_policy_uuid (UUID): The primary identifier of the subsidy access policy for this transaction. + lms_user_id (int): The LMS user id of the user associated with this transaction. + content_key (CourseKey): The course (run) key associated with this transaction. + parent_content_key (str): The parent (just course, not run) key for the course key. + fulfillment_identifier (str): The identifier of the subsidized enrollment record for a learner, + generated durning enrollment. + reversal (LedgerTransactionReversal): Any reversal associated with this transaction. + """ + uuid = attr.ib(type=UUID) + created = attr.ib(type=datetime) + modified = attr.ib(type=datetime) + idempotency_key = attr.ib(type=str) + quantity = attr.ib(type=int) + state = attr.ib(type=str) + metadata = attr.ib(type=dict) + ledger_uuid = attr.ib(type=UUID) + subsidy_access_policy_uuid = attr.ib(type=UUID) + lms_user_id = attr.ib(type=int) + content_key = attr.ib(type=CourseKey) + parent_content_key = attr.ib(type=str) + fulfillment_identifier = attr.ib(type=str) + reversal = attr.ib(type=LedgerTransactionReversal) diff --git a/openedx_events/enterprise/signals.py b/openedx_events/enterprise/signals.py index 287a3849..e1a90a68 100644 --- a/openedx_events/enterprise/signals.py +++ b/openedx_events/enterprise/signals.py @@ -8,7 +8,11 @@ docs/decisions/0003-events-payload.rst """ -from openedx_events.enterprise.data import SubsidyRedemption +from openedx_events.enterprise.data import ( + LedgerTransaction, + LedgerTransactionReversal, + SubsidyRedemption, +) from openedx_events.tooling import OpenEdxPublicSignal # .. event_type: org.openedx.enterprise.subsidy.redeemed.v1 @@ -32,3 +36,51 @@ "redemption": SubsidyRedemption, } ) + + +# .. event_type: org.openedx.enterprise_subsidies.ledger_transaction.created.v1 +# .. event_name: LEDGER_TRANSACTION_CREATED +# .. event_description: emitted when an enterprise ledger transaction is created. +# .. event_data: LedgerTransaction +LEDGER_TRANSACTION_CREATED = OpenEdxPublicSignal( + event_type="org.openedx.enterprise_subsidies.ledger_transaction.created.v1", + data={ + "ledger_transaction": LedgerTransaction, + } +) + + +# .. event_type: org.openedx.enterprise_subsidies.ledger_transaction.committed.v1 +# .. event_name: LEDGER_TRANSACTION_COMMITTED +# .. event_description: emitted when an enterprise ledger transaction is committed. +# .. event_data: LedgerTransaction +LEDGER_TRANSACTION_COMMITTED = OpenEdxPublicSignal( + event_type="org.openedx.enterprise_subsidies.ledger_transaction.committed.v1", + data={ + "ledger_transaction": LedgerTransaction, + } +) + + +# .. event_type: org.openedx.enterprise_subsidies.ledger_transaction.failed.v1 +# .. event_name: LEDGER_TRANSACTION_FAILED +# .. event_description: emitted when an enterprise ledger transaction fails. +# .. event_data: LedgerTransaction +LEDGER_TRANSACTION_FAILED = OpenEdxPublicSignal( + event_type="org.openedx.enterprise_subsidies.ledger_transaction.failed.v1", + data={ + "ledger_transaction": LedgerTransaction, + } +) + + +# .. event_type: org.openedx.enterprise_subsidies.ledger_transaction.reversed.v1 +# .. event_name: LEDGER_TRANSACTION_REVERSED +# .. event_description: emitted when an enterprise ledger transaction is reversed. +# .. event_data: LedgerTransaction +LEDGER_TRANSACTION_REVERSED = OpenEdxPublicSignal( + event_type="org.openedx.enterprise_subsidies.ledger_transaction.reversed.v1", + data={ + "ledger_transaction": LedgerTransaction, + } +) diff --git a/openedx_events/event_bus/avro/custom_serializers.py b/openedx_events/event_bus/avro/custom_serializers.py index d41616c2..09570848 100644 --- a/openedx_events/event_bus/avro/custom_serializers.py +++ b/openedx_events/event_bus/avro/custom_serializers.py @@ -4,6 +4,7 @@ """ from abc import ABC, abstractmethod from datetime import datetime +from uuid import UUID from ccx_keys.locator import CCXLocator from opaque_keys.edx.keys import CourseKey, UsageKey @@ -149,6 +150,26 @@ def deserialize(data: str): return LibraryUsageLocatorV2.from_string(data) +class UuidAvroSerializer(BaseCustomTypeAvroSerializer): + """ + CustomTypeAvroSerializer for the UUID class. + https://github.com/cloudevents/spec/blob/v1.0.2/cloudevents/formats/avro-format.md#21-type-system-mapping + """ + + cls = UUID + field_type = PYTHON_TYPE_TO_AVRO_MAPPING[str] + + @staticmethod + def serialize(obj) -> str: + """Serialize obj into string.""" + return str(obj) + + @staticmethod + def deserialize(data: str): + """Deserialize string into obj.""" + return UUID(data) + + DEFAULT_CUSTOM_SERIALIZERS = [ CourseKeyAvroSerializer, CcxCourseLocatorAvroSerializer, @@ -156,4 +177,5 @@ def deserialize(data: str): LibraryLocatorV2AvroSerializer, LibraryUsageLocatorV2AvroSerializer, UsageKeyAvroSerializer, + UuidAvroSerializer, ] diff --git a/openedx_events/event_bus/avro/tests/test_custom_serializers.py b/openedx_events/event_bus/avro/tests/test_custom_serializers.py index ab852fc0..398aec36 100644 --- a/openedx_events/event_bus/avro/tests/test_custom_serializers.py +++ b/openedx_events/event_bus/avro/tests/test_custom_serializers.py @@ -1,9 +1,11 @@ """Test custom servializers""" +from uuid import uuid4, UUID from unittest import TestCase from ccx_keys.locator import CCXLocator from openedx_events.event_bus.avro.custom_serializers import CcxCourseLocatorAvroSerializer +from ..custom_serializers import UuidAvroSerializer class TestCCXLocatorSerailizer(TestCase): @@ -19,7 +21,7 @@ def test_serialize(self): result1 = CcxCourseLocatorAvroSerializer.serialize(obj1) self.assertEqual(result1, expected1) - def test_deseialize(self): + def test_deserialize(self): """ Test case for deserializing CCXLocator object. """ @@ -28,3 +30,27 @@ def test_deseialize(self): expected1 = CCXLocator(org="edx", course="DemoX", run="Demo_course", ccx="1") result1 = CcxCourseLocatorAvroSerializer.deserialize(data1) self.assertEqual(result1, expected1) + + +class TestUuidAvroSerializer(TestCase): + """ + Tests case for Avro UUID de-/serialization. + """ + def test_serialize(self): + """ + Test UUID Avro serialization. + """ + some_uuid = uuid4() + expected_result = str(some_uuid) + actual_result = UuidAvroSerializer.serialize(some_uuid) + self.assertEqual(actual_result, expected_result) + + def test_deserialize(self): + """ + Test UUID Avro de-serialization. + """ + + uuid_str = str(uuid4()) + expected_result = UUID(uuid_str) + actual_result = UuidAvroSerializer.deserialize(uuid_str) + self.assertEqual(actual_result, expected_result) diff --git a/setup.cfg b/setup.cfg index 4c16b98c..4bc0fb68 100644 --- a/setup.cfg +++ b/setup.cfg @@ -13,3 +13,6 @@ multi_line_output = 3 [wheel] universal = 1 + +[flake8] +max-line-length = 120 \ No newline at end of file