src.offers.offer_service
Offer service for creating and retrieving credential offers.
1"""Offer service for creating and retrieving credential offers.""" 2 3import uuid 4 5from src.access_control.access_control_port import AccessControlPort 6from src.awards.award_service import AwardService 7from src.awards.awards_client_port import ( 8 AwardForbidden, 9 AwardNotFound, 10 AwardsClientError, 11) 12 13from .models import Offer 14from .offers_client_port import OfferNotFound, OffersClientError, OffersClientPort 15from .offers_repository_port import OffersRepositoryPort 16 17 18class PermissionDeniedError(Exception): 19 """Raised when access control denies the requested action.""" 20 21 22class NotFoundError(Exception): 23 """Raised when a requested resource cannot be found. 24 25 The message includes the resource type and identifier so that developers 26 and operators can pinpoint the source, e.g. "Award award-42 not found" 27 or "Offer offer-7 not found". 28 """ 29 30 31class OfferServiceError(Exception): 32 """Raised when an upstream service returns an unexpected error. 33 34 Wraps the original exception message so that the cause is preserved 35 for logging and debugging. 36 """ 37 38 39class OfferService: 40 """Service that orchestrates offer creation.""" 41 42 _access_control: AccessControlPort 43 _offers_client: OffersClientPort 44 _offers_repository: OffersRepositoryPort 45 _award_service: AwardService 46 47 def __init__( 48 self, 49 access_control: AccessControlPort, 50 offers_client: OffersClientPort, 51 offers_repository: OffersRepositoryPort, 52 award_service: AwardService, 53 ) -> None: 54 """Initialise the service with its dependencies. 55 56 Args: 57 access_control: Adapter for checking resource permissions. 58 offers_repository: Adapter for persisting offers. 59 offers_client: Adapter for interacting with oid4vci agent. 60 award_service: Service for fetching awards. 61 """ 62 self._access_control = access_control 63 self._offers_client = offers_client 64 self._offers_repository = offers_repository 65 self._award_service = award_service 66 67 def create_offer(self, award_id: str, bearer_token: str) -> Offer: 68 """Create, persist, and return a new credential offer. 69 70 Args: 71 award_id: The award/achievement to issue. 72 bearer_token: The caller's bearer token used for permission checking. 73 74 Returns: 75 The newly created Offer. 76 77 Raises: 78 PermissionDeniedError: When the caller is not permitted to import the award, 79 or when the awards service denies access. 80 NotFoundError: When the award does not exist in the awards service. 81 OfferServiceError: When an upstream service returns an unexpected error. 82 """ 83 if not self._access_control.may_import( 84 bearer_token, award_id, "Award", "import" 85 ): 86 raise PermissionDeniedError(award_id) 87 88 try: 89 award = self._award_service.get(award_id) 90 except AwardNotFound: 91 raise NotFoundError(f"Award {award_id} not found") 92 except AwardForbidden: 93 raise PermissionDeniedError(award_id) 94 except AwardsClientError as e: 95 raise OfferServiceError(str(e)) from e 96 97 offer_id = str(uuid.uuid4()) 98 99 # TODO: wrap in transaction 100 uri = self._offers_client.create(offer_id, award) 101 offer = Offer(offer_id=offer_id, award_id=award_id, uri=uri) 102 self._offers_repository.store(offer) 103 104 return offer 105 106 def get_offer(self, offer_id: str) -> Offer: 107 """Retrieve an offer by its identifier. 108 109 Args: 110 offer_id: The unique offer identifier. 111 112 Returns: 113 The matching Offer. 114 115 Raises: 116 NotFoundError: When the offer cannot be found in the client or repository. 117 OfferServiceError: When the upstream client returns an unexpected error. 118 """ 119 try: 120 upstream_offer = self._offers_client.get(offer_id) 121 except OfferNotFound: 122 raise NotFoundError(f"Offer {offer_id} not found") 123 except OffersClientError as e: 124 raise OfferServiceError(str(e)) from e 125 126 try: 127 stored_offer = self._offers_repository.get(offer_id) 128 except KeyError: 129 raise NotFoundError(f"Offer {offer_id} not found") 130 131 return Offer( 132 offer_id=offer_id, 133 award_id=stored_offer.award_id, 134 uri=upstream_offer.uri, 135 )
19class PermissionDeniedError(Exception): 20 """Raised when access control denies the requested action."""
Raised when access control denies the requested action.
23class NotFoundError(Exception): 24 """Raised when a requested resource cannot be found. 25 26 The message includes the resource type and identifier so that developers 27 and operators can pinpoint the source, e.g. "Award award-42 not found" 28 or "Offer offer-7 not found". 29 """
Raised when a requested resource cannot be found.
The message includes the resource type and identifier so that developers and operators can pinpoint the source, e.g. "Award award-42 not found" or "Offer offer-7 not found".
32class OfferServiceError(Exception): 33 """Raised when an upstream service returns an unexpected error. 34 35 Wraps the original exception message so that the cause is preserved 36 for logging and debugging. 37 """
Raised when an upstream service returns an unexpected error.
Wraps the original exception message so that the cause is preserved for logging and debugging.
40class OfferService: 41 """Service that orchestrates offer creation.""" 42 43 _access_control: AccessControlPort 44 _offers_client: OffersClientPort 45 _offers_repository: OffersRepositoryPort 46 _award_service: AwardService 47 48 def __init__( 49 self, 50 access_control: AccessControlPort, 51 offers_client: OffersClientPort, 52 offers_repository: OffersRepositoryPort, 53 award_service: AwardService, 54 ) -> None: 55 """Initialise the service with its dependencies. 56 57 Args: 58 access_control: Adapter for checking resource permissions. 59 offers_repository: Adapter for persisting offers. 60 offers_client: Adapter for interacting with oid4vci agent. 61 award_service: Service for fetching awards. 62 """ 63 self._access_control = access_control 64 self._offers_client = offers_client 65 self._offers_repository = offers_repository 66 self._award_service = award_service 67 68 def create_offer(self, award_id: str, bearer_token: str) -> Offer: 69 """Create, persist, and return a new credential offer. 70 71 Args: 72 award_id: The award/achievement to issue. 73 bearer_token: The caller's bearer token used for permission checking. 74 75 Returns: 76 The newly created Offer. 77 78 Raises: 79 PermissionDeniedError: When the caller is not permitted to import the award, 80 or when the awards service denies access. 81 NotFoundError: When the award does not exist in the awards service. 82 OfferServiceError: When an upstream service returns an unexpected error. 83 """ 84 if not self._access_control.may_import( 85 bearer_token, award_id, "Award", "import" 86 ): 87 raise PermissionDeniedError(award_id) 88 89 try: 90 award = self._award_service.get(award_id) 91 except AwardNotFound: 92 raise NotFoundError(f"Award {award_id} not found") 93 except AwardForbidden: 94 raise PermissionDeniedError(award_id) 95 except AwardsClientError as e: 96 raise OfferServiceError(str(e)) from e 97 98 offer_id = str(uuid.uuid4()) 99 100 # TODO: wrap in transaction 101 uri = self._offers_client.create(offer_id, award) 102 offer = Offer(offer_id=offer_id, award_id=award_id, uri=uri) 103 self._offers_repository.store(offer) 104 105 return offer 106 107 def get_offer(self, offer_id: str) -> Offer: 108 """Retrieve an offer by its identifier. 109 110 Args: 111 offer_id: The unique offer identifier. 112 113 Returns: 114 The matching Offer. 115 116 Raises: 117 NotFoundError: When the offer cannot be found in the client or repository. 118 OfferServiceError: When the upstream client returns an unexpected error. 119 """ 120 try: 121 upstream_offer = self._offers_client.get(offer_id) 122 except OfferNotFound: 123 raise NotFoundError(f"Offer {offer_id} not found") 124 except OffersClientError as e: 125 raise OfferServiceError(str(e)) from e 126 127 try: 128 stored_offer = self._offers_repository.get(offer_id) 129 except KeyError: 130 raise NotFoundError(f"Offer {offer_id} not found") 131 132 return Offer( 133 offer_id=offer_id, 134 award_id=stored_offer.award_id, 135 uri=upstream_offer.uri, 136 )
Service that orchestrates offer creation.
48 def __init__( 49 self, 50 access_control: AccessControlPort, 51 offers_client: OffersClientPort, 52 offers_repository: OffersRepositoryPort, 53 award_service: AwardService, 54 ) -> None: 55 """Initialise the service with its dependencies. 56 57 Args: 58 access_control: Adapter for checking resource permissions. 59 offers_repository: Adapter for persisting offers. 60 offers_client: Adapter for interacting with oid4vci agent. 61 award_service: Service for fetching awards. 62 """ 63 self._access_control = access_control 64 self._offers_client = offers_client 65 self._offers_repository = offers_repository 66 self._award_service = award_service
Initialise the service with its dependencies.
Args: access_control: Adapter for checking resource permissions. offers_repository: Adapter for persisting offers. offers_client: Adapter for interacting with oid4vci agent. award_service: Service for fetching awards.
68 def create_offer(self, award_id: str, bearer_token: str) -> Offer: 69 """Create, persist, and return a new credential offer. 70 71 Args: 72 award_id: The award/achievement to issue. 73 bearer_token: The caller's bearer token used for permission checking. 74 75 Returns: 76 The newly created Offer. 77 78 Raises: 79 PermissionDeniedError: When the caller is not permitted to import the award, 80 or when the awards service denies access. 81 NotFoundError: When the award does not exist in the awards service. 82 OfferServiceError: When an upstream service returns an unexpected error. 83 """ 84 if not self._access_control.may_import( 85 bearer_token, award_id, "Award", "import" 86 ): 87 raise PermissionDeniedError(award_id) 88 89 try: 90 award = self._award_service.get(award_id) 91 except AwardNotFound: 92 raise NotFoundError(f"Award {award_id} not found") 93 except AwardForbidden: 94 raise PermissionDeniedError(award_id) 95 except AwardsClientError as e: 96 raise OfferServiceError(str(e)) from e 97 98 offer_id = str(uuid.uuid4()) 99 100 # TODO: wrap in transaction 101 uri = self._offers_client.create(offer_id, award) 102 offer = Offer(offer_id=offer_id, award_id=award_id, uri=uri) 103 self._offers_repository.store(offer) 104 105 return offer
Create, persist, and return a new credential offer.
Args: award_id: The award/achievement to issue. bearer_token: The caller's bearer token used for permission checking.
Returns: The newly created Offer.
Raises: PermissionDeniedError: When the caller is not permitted to import the award, or when the awards service denies access. NotFoundError: When the award does not exist in the awards service. OfferServiceError: When an upstream service returns an unexpected error.
107 def get_offer(self, offer_id: str) -> Offer: 108 """Retrieve an offer by its identifier. 109 110 Args: 111 offer_id: The unique offer identifier. 112 113 Returns: 114 The matching Offer. 115 116 Raises: 117 NotFoundError: When the offer cannot be found in the client or repository. 118 OfferServiceError: When the upstream client returns an unexpected error. 119 """ 120 try: 121 upstream_offer = self._offers_client.get(offer_id) 122 except OfferNotFound: 123 raise NotFoundError(f"Offer {offer_id} not found") 124 except OffersClientError as e: 125 raise OfferServiceError(str(e)) from e 126 127 try: 128 stored_offer = self._offers_repository.get(offer_id) 129 except KeyError: 130 raise NotFoundError(f"Offer {offer_id} not found") 131 132 return Offer( 133 offer_id=offer_id, 134 award_id=stored_offer.award_id, 135 uri=upstream_offer.uri, 136 )
Retrieve an offer by its identifier.
Args: offer_id: The unique offer identifier.
Returns: The matching Offer.
Raises: NotFoundError: When the offer cannot be found in the client or repository. OfferServiceError: When the upstream client returns an unexpected error.