Skip to content

Commit

Permalink
Added webhook callback (#142)
Browse files Browse the repository at this point in the history
* Added webhook callback

* modify script to add admin-subscription
  • Loading branch information
Hrishabh17 authored Jun 20, 2024
1 parent abdc6b6 commit bb87062
Show file tree
Hide file tree
Showing 10 changed files with 173 additions and 3 deletions.
54 changes: 52 additions & 2 deletions apps/fyle/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,14 @@
import traceback
from functools import wraps

from fyle.platform.exceptions import NoPrivilegeError, RetryException
from fyle.platform.exceptions import NoPrivilegeError, RetryException, InvalidTokenError as FyleInvalidTokenError
from apps.business_central.exceptions import BulkError
from rest_framework.response import Response
from rest_framework.views import status

from apps.workspaces.models import FyleCredential

from apps.workspaces.models import FyleCredential, BusinessCentralCredentials, Workspace, ExportSetting, AdvancedSetting
from apps.accounting_exports.models import AccountingExport

logger = logging.getLogger(__name__)
logger.level = logging.INFO
Expand Down Expand Up @@ -41,3 +46,48 @@ def wrapper(*args, **kwargs):
logger.exception('Something unexpected happened workspace_id: %s %s', args[0], args[1].detail)

return wrapper


def handle_view_exceptions():
def decorator(func):
def new_fn(*args, **kwargs):
try:
return func(*args, **kwargs)
except AccountingExport.DoesNotExist:
return Response(data={'message': 'Accounting Export not found'}, status=status.HTTP_400_BAD_REQUEST)

except FyleCredential.DoesNotExist:
return Response(data={'message': 'Fyle credentials not found in workspace'}, status=status.HTTP_400_BAD_REQUEST)

except FyleInvalidTokenError as exception:
logger.info('Fyle token expired workspace_id - %s %s', kwargs['workspace_id'], {'error': exception.response})
return Response(data={'message': 'Fyle token expired workspace_id'}, status=status.HTTP_400_BAD_REQUEST)

except NoPrivilegeError as exception:
logger.info('Invalid Fyle Credentials / Admin is disabled for workspace_id%s %s', kwargs['workspace_id'], {'error': exception.response})
return Response(data={'message': 'Invalid Fyle Credentials / Admin is disabled'}, status=status.HTTP_400_BAD_REQUEST)

except Workspace.DoesNotExist:
return Response(data={'message': 'Workspace with this id does not exist'}, status=status.HTTP_400_BAD_REQUEST)

except AdvancedSetting.DoesNotExist:
return Response(data={'message': 'Advanced Settings does not exist in workspace'}, status=status.HTTP_400_BAD_REQUEST)

except ExportSetting.DoesNotExist:
return Response({'message': 'Export Settings does not exist in workspace'}, status=status.HTTP_400_BAD_REQUEST)

except BusinessCentralCredentials.DoesNotExist:
logger.info('BusinessCentral credentials not found in workspace')
return Response(data={'message': 'BusinessCentral credentials not found in workspace'}, status=status.HTTP_400_BAD_REQUEST)

except BulkError as exception:
logger.info('Bulk Error %s', exception.response)
return Response(data={'message': 'Bulk Error'}, status=status.HTTP_400_BAD_REQUEST)

except Exception as exception:
logger.exception(exception)
return Response(data={'message': 'An unhandled error has occurred, please re-try later'}, status=status.HTTP_400_BAD_REQUEST)

return new_fn

return decorator
13 changes: 13 additions & 0 deletions apps/fyle/queue.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

from apps.accounting_exports.models import AccountingExport
from apps.fyle.tasks import import_expenses
from apps.workspaces.models import Workspace


def queue_import_reimbursable_expenses(workspace_id: int, synchronous: bool = False):
Expand Down Expand Up @@ -55,3 +56,15 @@ def queue_import_credit_card_expenses(workspace_id: int, synchronous: bool = Fal
return

import_expenses(workspace_id, accounting_export, 'PERSONAL_CORPORATE_CREDIT_CARD_ACCOUNT', 'CCC')


def async_handle_webhook_callback(body: dict) -> None:
"""
Async'ly import and export expenses
:param body: body
:return: None
"""
if body.get('action') == 'ACCOUNTING_EXPORT_INITIATED' and body.get('data'):
org_id = body['data']['org_id']
workspace = Workspace.objects.get(org_id=org_id)
async_task('apps.workspaces.tasks.run_import_export', workspace.id)
2 changes: 2 additions & 0 deletions apps/fyle/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
FyleFieldsView,
ImportFyleAttributesView,
SkippedExpenseView,
WebhookCallbackView
)

accounting_exports_path = [
Expand All @@ -40,6 +41,7 @@
path('fields/', FyleFieldsView.as_view(), name='fyle-fields'),
path('expense_fields/', CustomFieldView.as_view(), name='fyle-expense-fields'),
path('expenses/', SkippedExpenseView.as_view(), name='expenses'),
path('webhook_callback/', WebhookCallbackView.as_view(), name='webhook-callback')
]

fyle_dimension_paths = [
Expand Down
17 changes: 17 additions & 0 deletions apps/fyle/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
from rest_framework.response import Response
from rest_framework.views import status

from apps.fyle.exceptions import handle_view_exceptions
from apps.fyle.queue import async_handle_webhook_callback

from apps.fyle.helpers import get_exportable_accounting_exports_ids
from apps.fyle.models import Expense, ExpenseFilter
from apps.fyle.queue import queue_import_credit_card_expenses, queue_import_reimbursable_expenses
Expand Down Expand Up @@ -111,3 +114,17 @@ class SkippedExpenseView(generics.ListAPIView):
queryset = Expense.objects.all().order_by("-updated_at")
filter_backends = (DjangoFilterBackend,)
filterset_class = ExpenseSearchFilter


class WebhookCallbackView(generics.CreateAPIView):
"""
Export View
"""
authentication_classes = []
permission_classes = []

@handle_view_exceptions()
def post(self, request, *args, **kwargs):
async_handle_webhook_callback(request.data)

return Response(data={}, status=status.HTTP_200_OK)
2 changes: 2 additions & 0 deletions apps/workspaces/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
Workspace Serializers
"""
from django.core.cache import cache
from django_q.tasks import async_task
from django.db import transaction
from django.db.models import Q
from fyle_accounting_mappings.models import ExpenseAttribute, MappingSetting
Expand Down Expand Up @@ -302,6 +303,7 @@ def create(self, validated_data):
if workspace.onboarding_state == 'ADVANCED_SETTINGS':
workspace.onboarding_state = 'COMPLETE'
workspace.save()
async_task('apps.workspaces.tasks.async_create_admin_subcriptions', workspace.id)

return advanced_setting

Expand Down
19 changes: 19 additions & 0 deletions apps/workspaces/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,18 @@
from typing import List

from django_q.models import Schedule
from django.conf import settings

from apps.accounting_exports.models import AccountingExport, AccountingExportSummary
from apps.business_central.exports.journal_entry.tasks import ExportJournalEntry
from apps.business_central.exports.purchase_invoice.tasks import ExportPurchaseInvoice
from apps.fyle.queue import queue_import_credit_card_expenses, queue_import_reimbursable_expenses
from apps.workspaces.models import AdvancedSetting, ExportSetting, FyleCredential

from fyle_integrations_platform_connector import PlatformConnector

logger = logging.getLogger(__name__)
logger.level = logging.INFO


def async_update_fyle_credentials(fyle_org_id: str, refresh_token: str):
Expand Down Expand Up @@ -186,3 +190,18 @@ def export_to_business_central(workspace_id: int):
accounting_summary.last_exported_at = last_exported_at
accounting_summary.export_mode = 'MANUAL'
accounting_summary.save()


def async_create_admin_subcriptions(workspace_id: int) -> None:
"""
Create admin subscriptions
:param workspace_id: workspace id
:return: None
"""
fyle_credentials = FyleCredential.objects.get(workspace_id=workspace_id)
platform = PlatformConnector(fyle_credentials)
payload = {
'is_enabled': True,
'webhook_url': '{}/workspaces/{}/fyle/webhook_callback/'.format(settings.API_URL, workspace_id)
}
platform.subscriptions.post(payload)
13 changes: 13 additions & 0 deletions scripts/python/001_create_admin_subscriptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Create admin subscriptions for existing workspaces

from apps.workspaces.tasks import async_create_admin_subcriptions
from apps.workspaces.models import Workspace

workspaces = Workspace.objects.filter(onboarding_state='COMPLETE').all()

for workspace in workspaces:
try:
async_create_admin_subcriptions(workspace.id)
except Exception as e:
print('Error while creating admin subscriptions for workspace - {} with ID - {}'.format(workspace.name, workspace.id))
print(e.__dict__)
File renamed without changes.
File renamed without changes.
56 changes: 55 additions & 1 deletion tests/test_workspaces/test_tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,14 @@
async_update_fyle_credentials,
run_import_export,
schedule_sync,
export_to_business_central
export_to_business_central,
async_create_admin_subcriptions
)
from apps.accounting_exports.models import AccountingExport, AccountingExportSummary
from apps.workspaces.models import FyleCredential, AdvancedSetting, ExportSetting
from django_q.models import Schedule
from django.conf import settings
from django.urls import reverse


def test_async_update_fyle_credentials(
Expand Down Expand Up @@ -258,3 +261,54 @@ def test_export_to_business_central_with_reimbursable_expense(
assert accounting_export.fund_source == 'PERSONAL'
assert accounting_summary.export_mode == 'MANUAL'
assert accounting_summary.last_exported_at is not None


def test_async_create_admin_subcriptions(
db,
mocker,
create_temp_workspace,
add_fyle_credentials
):
mock_api = mocker.patch(
'fyle.platform.apis.v1beta.admin.Subscriptions.post',
return_value={}
)
workspace_id = 1
async_create_admin_subcriptions(workspace_id=workspace_id)

payload = {
'is_enabled': True,
'webhook_url': '{}/workspaces/{}/fyle/webhook_callback/'.format(settings.API_URL, workspace_id)
}

assert mock_api.once_called_with(payload)

mock_api.side_effect = Exception('Error')
try:
async_create_admin_subcriptions(workspace_id=workspace_id)
except Exception as e:
assert str(e) == 'Error'


def test_async_create_admin_subcriptions_2(
db,
mocker,
create_temp_workspace,
add_fyle_credentials
):
mock_api = mocker.patch(
'fyle.platform.apis.v1beta.admin.Subscriptions.post',
return_value={}
)
workspace_id = 1
reverse('webhook-callback', kwargs={'workspace_id': workspace_id})

payload = {
'is_enabled': True,
'webhook_url': '{}/workspaces/{}/fyle/webhook_callback/'.format(settings.API_URL, workspace_id)
}

assert mock_api.once_called_with(payload)

mock_api.side_effect = Exception('Error')
reverse('webhook-callback', kwargs={'workspace_id': workspace_id})

0 comments on commit bb87062

Please sign in to comment.