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        )
class PermissionDeniedError(builtins.Exception):
19class PermissionDeniedError(Exception):
20    """Raised when access control denies the requested action."""

Raised when access control denies the requested action.

class NotFoundError(builtins.Exception):
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".

class OfferServiceError(builtins.Exception):
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.

class OfferService:
 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.

def create_offer(self, award_id: str, bearer_token: str) -> src.offers.models.Offer:
 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.

def get_offer(self, offer_id: str) -> src.offers.models.Offer:
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.