-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(api): add api to invoice models
- Loading branch information
1 parent
38bcaf1
commit d375343
Showing
22 changed files
with
464 additions
and
52 deletions.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
from rest_framework.versioning import BaseVersioning | ||
from rest_framework.exceptions import NotFound | ||
|
||
SUPPORTED_VERSIONS = [ | ||
'1.0' | ||
] | ||
|
||
class HeaderVersioning(BaseVersioning): | ||
def determine_version(self, request, *args, **kwargs): | ||
# Get the version from the 'X-API-Version' header | ||
accept_header = request.META.get('HTTP_X_API_VERSION', '') | ||
|
||
# Check if the version header contains 'v' | ||
if 'v' in accept_header: | ||
version = accept_header.split('v')[-1].split('+')[0] | ||
else: | ||
version = None | ||
|
||
# If no version is found, default to the latest version | ||
if not version: | ||
return SUPPORTED_VERSIONS[-1] # Latest version | ||
|
||
# If version is not supported, raise an error | ||
if version not in SUPPORTED_VERSIONS: | ||
raise NotFound(f"API version '{version}' is not supported.") | ||
|
||
return version |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
from .exception import ErrorHandlingMixin |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,92 @@ | ||
import logging | ||
import uuid | ||
from django.utils.timezone import now | ||
from rest_framework.views import exception_handler | ||
from rest_framework.exceptions import ValidationError, NotFound, APIException, NotAcceptable | ||
from rest_framework import status | ||
from rest_framework.response import Response | ||
|
||
# Get the logger instance | ||
logger = logging.getLogger(__name__) | ||
|
||
class ErrorHandlingMixin: | ||
""" | ||
Mixin to provide consistent error handling across APIs, | ||
including structured error responses, logging, and trace IDs. | ||
""" | ||
|
||
def handle_exception(self, exc): | ||
""" | ||
Override the default DRF exception handling to provide | ||
a structured error response with logging and trace IDs. | ||
""" | ||
# Call the default DRF exception handler first to get the default response | ||
response = exception_handler(exc, self.get_exception_handler_context()) | ||
|
||
# Generate a unique trace ID for this request | ||
trace_id = str(uuid.uuid4()) | ||
|
||
# Path from the current request | ||
path = self.request.path if hasattr(self, 'request') else 'unknown' | ||
|
||
# Default error structure | ||
error_response = { | ||
"code": "SERVER_ERROR", | ||
"message": "An unexpected error occurred.", | ||
"details": [], | ||
"path": path, | ||
"traceId": trace_id, | ||
"timestamp": now().isoformat() | ||
} | ||
|
||
# Handle specific exceptions for better detail | ||
if isinstance(exc, ValidationError): | ||
error_response['code'] = "VALIDATION_ERROR" | ||
error_response['message'] = "Invalid input parameters" | ||
error_response['details'] = [ | ||
{"field": field, "message": msg[0] if isinstance(msg, list) else msg} | ||
for field, msg in exc.detail.items() | ||
] | ||
status_code = status.HTTP_400_BAD_REQUEST | ||
|
||
elif isinstance(exc, NotFound): | ||
error_response['code'] = "NOT_FOUND" | ||
error_response['message'] = str(exc) | ||
status_code = status.HTTP_404_NOT_FOUND | ||
|
||
elif isinstance(exc, NotAcceptable): | ||
error_response['code'] = "NOT_ACCEPTABLE" | ||
error_response['message'] = str(exc) | ||
status_code = status.HTTP_406_NOT_ACCEPTABLE | ||
|
||
elif isinstance(exc, APIException): | ||
error_response['code'] = exc.get_codes() if exc.get_codes() else "API_ERROR" | ||
error_response['message'] = str(exc) | ||
status_code = exc.status_code | ||
|
||
else: | ||
# If response is None, this means DRF didn't handle the exception, so we do it ourselves. | ||
status_code = status.HTTP_500_INTERNAL_SERVER_ERROR | ||
|
||
# If DRF handled the exception, override its response data with the structured error | ||
if response is not None: | ||
response.data = error_response | ||
response.status_code = status_code | ||
else: | ||
# Create a custom response if DRF couldn't handle the exception | ||
response = Response(error_response, status=status_code) | ||
|
||
# Log the error details with trace ID for correlation | ||
logger.error(f"Error occurred [traceId={trace_id}] at {path}: {exc}", exc_info=True) | ||
|
||
return response | ||
|
||
def get_exception_handler_context(self): | ||
""" | ||
This method returns the context that will be passed to the DRF exception handler. | ||
It's useful to provide additional request-related context (like view, request, args). | ||
""" | ||
return { | ||
'view': self, | ||
'request': self.request, | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
from .expense import ExpenseSerializer | ||
from .category import CategorySerializer | ||
from .column import ColumnSerializer | ||
from .item import ItemSerializer | ||
from .invoice import InvoiceSerializer |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
# serializers.py | ||
from rest_framework import serializers | ||
from sage_invoice.models import Category | ||
|
||
|
||
class CategorySerializer(serializers.HyperlinkedModelSerializer): | ||
url = serializers.HyperlinkedIdentityField( | ||
view_name='category-detail', | ||
lookup_field='slug' | ||
) | ||
invoices = serializers.HyperlinkedRelatedField( | ||
many=True, | ||
view_name='invoice-detail', | ||
lookup_field='slug', | ||
read_only=True | ||
) | ||
|
||
class Meta: | ||
model = Category | ||
fields = ( | ||
"url", | ||
"title", | ||
"description", | ||
"invoices" | ||
) | ||
extra_kwargs = { | ||
'url': {'lookup_field': 'slug'}, | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
from rest_framework import serializers | ||
from sage_invoice.models import Column | ||
|
||
|
||
class ColumnSerializer(serializers.HyperlinkedModelSerializer): | ||
class Meta: | ||
model = Column | ||
fields = '__all__' | ||
lookup_field = "id" | ||
extra_kwargs = { | ||
'url': {'lookup_field': 'id'}, | ||
'invoice': {'lookup_field': "slug"}, | ||
'item': {'lookup_field': "id"}, | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
# serializers.py | ||
from rest_framework import serializers | ||
from sage_invoice.models import Expense | ||
|
||
|
||
class ExpenseSerializer(serializers.HyperlinkedModelSerializer): | ||
class Meta: | ||
model = Expense | ||
fields = '__all__' | ||
lookup_field = "id" | ||
extra_kwargs = { | ||
'url': {'lookup_field': 'id'}, | ||
'invoice': {'lookup_field': "slug"} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,80 @@ | ||
from rest_framework import serializers | ||
|
||
from sage_invoice.api.serializers import ItemSerializer, CategorySerializer, ColumnSerializer, ExpenseSerializer | ||
from sage_invoice.models import Invoice, Category | ||
|
||
class ContactFieldSerializer(serializers.Serializer): | ||
""" | ||
Serializer to validate the structure of the contacts field (either phone or email). | ||
""" | ||
phone = serializers.RegexField( | ||
regex=r'^\d{10,15}$', | ||
required=False, | ||
help_text="Phone number should only contain digits and be 10-15 characters long." | ||
) | ||
email = serializers.EmailField( | ||
required=False, | ||
help_text="Valid email format." | ||
) | ||
|
||
def validate(self, data): | ||
""" | ||
Ensure at least one of 'phone' or 'email' is provided. | ||
""" | ||
if not data.get('phone') and not data.get('email'): | ||
raise serializers.ValidationError("Either phone or email must be provided.") | ||
return data | ||
|
||
|
||
class NoteFieldSerializer(serializers.Serializer): | ||
""" | ||
Serializer to validate the structure of the notes field (additional fields). | ||
""" | ||
label = serializers.CharField() | ||
content = serializers.CharField() | ||
|
||
|
||
class InvoiceSerializer(serializers.HyperlinkedModelSerializer): | ||
contacts = ContactFieldSerializer(required=False) | ||
notes = serializers.ListField( | ||
child=serializers.DictField( | ||
child=serializers.CharField() | ||
), | ||
required=False | ||
) | ||
category = CategorySerializer(read_only=True) | ||
items = ItemSerializer(many=True, read_only=True) | ||
columns = ColumnSerializer(many=True, read_only=True) | ||
expense = ExpenseSerializer(read_only=True) | ||
|
||
class Meta: | ||
model = Invoice | ||
fields = [ | ||
'url', | ||
'slug', | ||
'invoice_date', | ||
'customer_name', | ||
'tracking_code', | ||
'contacts', | ||
'status', | ||
'receipt', | ||
'notes', | ||
'currency', | ||
'due_date', | ||
'template_choice', | ||
'logo', | ||
'signature', | ||
'stamp', | ||
'category', | ||
'items', | ||
'columns', | ||
'expense', | ||
] | ||
extra_kwargs = { | ||
'url': {'lookup_field': 'slug'}, | ||
'categories': {'lookup_field': 'slug'}, | ||
'items': {'lookup_field': 'id'}, | ||
'columns': {'lookup_field': 'id'}, | ||
'expense': {'lookup_field': 'id'}, | ||
} | ||
read_only_fields = ['slug'] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
# serializers.py | ||
from rest_framework import serializers | ||
from sage_invoice.models import Item | ||
|
||
|
||
class ItemSerializer(serializers.HyperlinkedModelSerializer): | ||
class Meta: | ||
model = Item | ||
fields = '__all__' | ||
lookup_field = "id" | ||
extra_kwargs = { | ||
'url': {'lookup_field': 'id'}, | ||
'invoice': {'lookup_field': "slug"} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
from django.urls import path, include | ||
from rest_framework.routers import DefaultRouter | ||
|
||
from .views import ( | ||
ExpenseViewSet, | ||
InvoiceViewSet, | ||
ItemViewSet, | ||
CategoryViewSet, | ||
ColumnViewSet | ||
) | ||
|
||
router = DefaultRouter() | ||
router.register(r'expenses', ExpenseViewSet) | ||
router.register(r'invoices', InvoiceViewSet) | ||
router.register(r'items', ItemViewSet) | ||
router.register(r'categories', CategoryViewSet) | ||
router.register(r'columns', ColumnViewSet) | ||
|
||
urlpatterns = [ | ||
path('', include(router.urls)), | ||
] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
from .category import CategoryViewSet | ||
from .column import ColumnViewSet | ||
from .expense import ExpenseViewSet | ||
from .invoice import InvoiceViewSet | ||
from .item import ItemViewSet |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
from rest_framework import viewsets | ||
|
||
from sage_invoice.models import Category | ||
from sage_invoice.api.mixins import ErrorHandlingMixin | ||
from sage_invoice.api.serializers import CategorySerializer | ||
from sage_invoice.api.helpers.versioning import HeaderVersioning | ||
|
||
|
||
class CategoryViewSet(ErrorHandlingMixin, viewsets.ModelViewSet): | ||
queryset = Category.objects.all() | ||
serializer_class = CategorySerializer | ||
|
||
versioning_class = HeaderVersioning | ||
|
||
def get_serializer_class(self): | ||
version = getattr(self.request, 'version', None) | ||
|
||
if version == '1.0': | ||
return CategorySerializer | ||
|
||
return super().get_serializer_class() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
# views.py | ||
from rest_framework import viewsets | ||
from sage_invoice.models import Column | ||
from sage_invoice.api.serializers import ColumnSerializer | ||
|
||
|
||
class ColumnViewSet(viewsets.ModelViewSet): | ||
queryset = Column.objects.all() | ||
serializer_class = ColumnSerializer | ||
lookup_field = "id" | ||
lookup_url_kwarg = "id" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
# views.py | ||
from rest_framework import viewsets | ||
from sage_invoice.models import Expense | ||
from sage_invoice.api.serializers import ExpenseSerializer | ||
|
||
|
||
class ExpenseViewSet(viewsets.ModelViewSet): | ||
queryset = Expense.objects.all() | ||
serializer_class = ExpenseSerializer | ||
lookup_field = "id" | ||
lookup_url_kwarg = "id" |
Oops, something went wrong.