-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* Workspace APIs Added * Github action added for tests * Export Settings APIs
- Loading branch information
Showing
20 changed files
with
935 additions
and
32 deletions.
There are no files selected for viewing
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,30 @@ | ||
name: Continuous Integration | ||
|
||
on: | ||
pull_request: | ||
types: [assigned, opened, synchronize, reopened] | ||
|
||
jobs: | ||
pytest: | ||
runs-on: ubuntu-latest | ||
environment: CI Environment | ||
steps: | ||
- uses: actions/checkout@v2 | ||
- name: Bring up Services and Run Tests | ||
run: | | ||
docker-compose -f docker-compose-pipeline.yml build | ||
docker-compose -f docker-compose-pipeline.yml up -d | ||
docker-compose -f docker-compose-pipeline.yml exec -T api pytest tests/ --cov --junit-xml=test-reports/report.xml --cov-report=xml --cov-fail-under=70 | ||
echo "STATUS=$(cat pytest-coverage.txt | grep 'Required test' | awk '{ print $1 }')" >> $GITHUB_ENV | ||
echo "FAILED=$(cat test-reports/report.xml | awk -F'=' '{print $5}' | awk -F' ' '{gsub(/"/, "", $1); print $1}')" >> $GITHUB_ENV | ||
- name: Upload coverage reports to Codecov with GitHub Action | ||
uses: codecov/codecov-action@v3 | ||
- name: Pytest coverage comment | ||
uses: MishaKav/pytest-coverage-comment@main | ||
if: ${{ always() && github.ref != 'refs/heads/master' }} | ||
with: | ||
create-new-comment: true | ||
junitxml-path: ./test-reports/report.xml | ||
- name: Evaluate Coverage | ||
if: ${{ (env.STATUS == 'FAIL') || (env.FAILED > 0) }} | ||
run: exit 1 |
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,52 @@ | ||
import json | ||
import requests | ||
from django.conf import settings | ||
|
||
|
||
def post_request(url, body, refresh_token=None): | ||
""" | ||
Create a HTTP post request. | ||
""" | ||
access_token = None | ||
api_headers = { | ||
'Content-Type': 'application/json', | ||
} | ||
if refresh_token: | ||
access_token = get_access_token(refresh_token) | ||
api_headers['Authorization'] = 'Bearer {0}'.format(access_token) | ||
|
||
response = requests.post( | ||
url, | ||
headers=api_headers, | ||
data=body | ||
) | ||
|
||
if response.status_code == 200: | ||
return json.loads(response.text) | ||
else: | ||
raise Exception(response.text) | ||
|
||
|
||
def get_access_token(refresh_token: str) -> str: | ||
""" | ||
Get access token from fyle | ||
""" | ||
api_data = { | ||
'grant_type': 'refresh_token', | ||
'refresh_token': refresh_token, | ||
'client_id': settings.FYLE_CLIENT_ID, | ||
'client_secret': settings.FYLE_CLIENT_SECRET | ||
} | ||
|
||
return post_request(settings.FYLE_TOKEN_URI, body=json.dumps(api_data))['access_token'] | ||
|
||
|
||
def get_cluster_domain(refresh_token: str) -> str: | ||
""" | ||
Get cluster domain name from fyle | ||
:param refresh_token: (str) | ||
:return: cluster_domain (str) | ||
""" | ||
cluster_api_url = '{0}/oauth/cluster/'.format(settings.FYLE_BASE_URL) | ||
|
||
return post_request(cluster_api_url, {}, refresh_token)['cluster_domain'] |
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,171 @@ | ||
from django.db import models | ||
from django.contrib.auth import get_user_model | ||
|
||
from ms_business_central_api.models.fields import ( | ||
StringNotNullField, | ||
CustomDateTimeField, | ||
StringOptionsField, | ||
TextNotNullField, | ||
StringNullField, | ||
BooleanTrueField | ||
) | ||
|
||
User = get_user_model() | ||
|
||
ONBOARDING_STATE_CHOICES = ( | ||
('CONNECTION', 'CONNECTION'), | ||
('EXPORT_SETTINGS', 'EXPORT_SETTINGS'), | ||
('IMPORT_SETTINGS', 'IMPORT_SETTINGS'), | ||
('ADVANCED_CONFIGURATION', 'ADVANCED_CONFIGURATION'), | ||
('COMPLETE', 'COMPLETE') | ||
) | ||
|
||
|
||
def get_default_onboarding_state(): | ||
return 'EXPORT_SETTINGS' | ||
|
||
|
||
class Workspace(models.Model): | ||
""" | ||
Workspace model | ||
""" | ||
id = models.AutoField(primary_key=True) | ||
name = StringNotNullField(help_text='Name of the workspace') | ||
user = models.ManyToManyField(User, help_text='Reference to users table') | ||
org_id = models.CharField(max_length=255, help_text='org id', unique=True) | ||
last_synced_at = CustomDateTimeField(help_text='Datetime when expenses were pulled last') | ||
ccc_last_synced_at = CustomDateTimeField(help_text='Datetime when ccc expenses were pulled last') | ||
source_synced_at = CustomDateTimeField(help_text='Datetime when source dimensions were pulled') | ||
destination_synced_at = CustomDateTimeField(help_text='Datetime when destination dimensions were pulled') | ||
onboarding_state = StringOptionsField( | ||
max_length=50, choices=ONBOARDING_STATE_CHOICES, default=get_default_onboarding_state, | ||
help_text='Onboarding status of the workspace' | ||
) | ||
ms_business_central_accounts_last_synced_at = CustomDateTimeField(help_text='ms business central accounts last synced at time') | ||
created_at = models.DateTimeField(auto_now_add=True, help_text='Created at datetime') | ||
updated_at = models.DateTimeField(auto_now=True, help_text='Updated at datetime') | ||
|
||
class Meta: | ||
db_table = 'workspaces' | ||
|
||
|
||
class BaseModel(models.Model): | ||
workspace = models.OneToOneField(Workspace, on_delete=models.PROTECT, help_text='Reference to Workspace model') | ||
created_at = models.DateTimeField(auto_now_add=True, help_text='Created at datetime') | ||
updated_at = models.DateTimeField(auto_now=True, help_text='Updated at datetime') | ||
|
||
class Meta: | ||
abstract = True | ||
|
||
|
||
class BaseForeignWorkspaceModel(models.Model): | ||
workspace = models.ForeignKey(Workspace, on_delete=models.PROTECT, help_text='Reference to Workspace model') | ||
created_at = models.DateTimeField(auto_now_add=True, help_text='Created at datetime') | ||
updated_at = models.DateTimeField(auto_now=True, help_text='Updated at datetime') | ||
|
||
class Meta: | ||
abstract = True | ||
|
||
|
||
class FyleCredential(BaseModel): | ||
""" | ||
Table to store Fyle credentials | ||
""" | ||
id = models.AutoField(auto_created=True, primary_key=True, verbose_name='ID', serialize=False) | ||
refresh_token = TextNotNullField(help_text='Fyle refresh token') | ||
cluster_domain = StringNullField(help_text='Fyle cluster domain') | ||
|
||
class Meta: | ||
db_table = 'fyle_credentials' | ||
|
||
|
||
# Reimbursable Expense Choices | ||
REIMBURSABLE_EXPENSE_EXPORT_TYPE_CHOICES = ( | ||
('PURCHASE_INVOICE', 'PURCHASE_INVOICE'), | ||
('JOURNAL_ENTRY', 'JOURNAL_ENTRY') | ||
) | ||
|
||
REIMBURSABLE_EXPENSE_STATE_CHOICES = ( | ||
('PAYMENT_PROCESSING', 'PAYMENT_PROCESSING'), | ||
('CLOSED', 'CLOSED') | ||
) | ||
|
||
REIMBURSABLE_EXPENSES_GROUPED_BY_CHOICES = ( | ||
('REPORT', 'report_id'), | ||
('EXPENSE', 'expense_id') | ||
) | ||
|
||
REIMBURSABLE_EXPENSES_DATE_TYPE_CHOICES = ( | ||
('LAST_SPENT_AT', 'last_spent_at'), | ||
('CREATED_AT', 'created_at'), | ||
('SPENT_AT', 'spent_at') | ||
) | ||
|
||
# Credit Card Expense Choices | ||
CREDIT_CARD_EXPENSE_EXPORT_TYPE_CHOICES = ( | ||
('JOURNAL_ENTRY', 'JOURNAL_ENTRY'), | ||
) | ||
|
||
CREDIT_CARD_EXPENSE_STATE_CHOICES = ( | ||
('APPROVED', 'APPROVED'), | ||
('PAYMENT_PROCESSING', 'PAYMENT_PROCESSING'), | ||
('PAID', 'PAID') | ||
) | ||
|
||
CREDIT_CARD_EXPENSES_GROUPED_BY_CHOICES = ( | ||
('REPORT', 'report_id'), | ||
('EXPENSE', 'expense_id') | ||
) | ||
|
||
CREDIT_CARD_EXPENSES_DATE_TYPE_CHOICES = ( | ||
('LAST_SPENT_AT', 'last_spent_at'), | ||
('POSTED_AT', 'posted_at'), | ||
('CREATED_AT', 'created_at') | ||
) | ||
|
||
|
||
class ExportSetting(BaseModel): | ||
""" | ||
Table to store export settings | ||
""" | ||
# Reimbursable Expenses Export Settings | ||
id = models.AutoField(auto_created=True, primary_key=True, verbose_name='ID', serialize=False) | ||
reimbursable_expenses_export_type = StringOptionsField( | ||
choices=REIMBURSABLE_EXPENSE_EXPORT_TYPE_CHOICES, | ||
) | ||
default_bank_account_name = StringNullField(help_text='Bank account name') | ||
default_back_account_id = StringNullField(help_text='Bank Account ID') | ||
reimbursable_expense_state = StringOptionsField( | ||
choices=REIMBURSABLE_EXPENSE_STATE_CHOICES | ||
) | ||
reimbursable_expense_date = StringOptionsField( | ||
choices=REIMBURSABLE_EXPENSES_DATE_TYPE_CHOICES | ||
) | ||
reimbursable_expense_grouped_by = StringOptionsField( | ||
choices=REIMBURSABLE_EXPENSES_GROUPED_BY_CHOICES | ||
) | ||
# Credit Card Expenses Export Settings | ||
credit_card_expense_export_type = StringOptionsField( | ||
choices=CREDIT_CARD_EXPENSE_EXPORT_TYPE_CHOICES | ||
) | ||
credit_card_expense_state = StringOptionsField( | ||
choices=CREDIT_CARD_EXPENSE_STATE_CHOICES | ||
) | ||
default_reimbursable_account_name = StringNullField(help_text='Reimbursable account name') | ||
default_reimbursable_account_id = StringNullField(help_text='Reimbursable Account ID') | ||
default_ccc_credit_card_account_name = StringNullField(help_text='CCC Credit card account name') | ||
default_ccc_credit_card_account_id = StringNullField(help_text='CCC Credit Card Account ID') | ||
default_reimbursable_credit_card_account_name = StringNullField(help_text='Reimbursable Credit card account name') | ||
default_reimbursable_credit_card_account_id = StringNullField(help_text='Reimbursable Credit card account name') | ||
credit_card_expense_grouped_by = StringOptionsField( | ||
choices=CREDIT_CARD_EXPENSES_GROUPED_BY_CHOICES | ||
) | ||
credit_card_expense_date = StringOptionsField( | ||
choices=CREDIT_CARD_EXPENSES_DATE_TYPE_CHOICES | ||
) | ||
default_vendor_name = StringNullField(help_text='default Vendor Name') | ||
default_vendor_id = StringNullField(help_text='default Vendor Id') | ||
auto_map_employees = BooleanTrueField(help_text='Auto map employees') | ||
|
||
class Meta: | ||
db_table = 'export_settings' |
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,98 @@ | ||
""" | ||
Workspace Serializers | ||
""" | ||
from django.core.cache import cache | ||
from rest_framework import serializers | ||
from fyle_rest_auth.helpers import get_fyle_admin | ||
from fyle_rest_auth.models import AuthToken | ||
|
||
from ms_business_central_api.utils import assert_valid | ||
from apps.workspaces.models import ( | ||
Workspace, | ||
FyleCredential, | ||
ExportSetting | ||
) | ||
from apps.users.models import User | ||
from apps.fyle.helpers import get_cluster_domain | ||
|
||
|
||
class WorkspaceSerializer(serializers.ModelSerializer): | ||
""" | ||
Workspace serializer | ||
""" | ||
class Meta: | ||
model = Workspace | ||
fields = '__all__' | ||
read_only_fields = ('id', 'name', 'org_id', 'created_at', 'updated_at', 'user') | ||
|
||
def create(self, validated_data): | ||
""" | ||
Update workspace | ||
""" | ||
access_token = self.context['request'].META.get('HTTP_AUTHORIZATION') | ||
user = self.context['request'].user | ||
|
||
# Getting user profile using the access token | ||
fyle_user = get_fyle_admin(access_token.split(' ')[1], None) | ||
|
||
# getting name, org_id, currency of Fyle User | ||
name = fyle_user['data']['org']['name'] | ||
org_id = fyle_user['data']['org']['id'] | ||
|
||
# Checking if workspace already exists | ||
workspace = Workspace.objects.filter(org_id=org_id).first() | ||
|
||
if workspace: | ||
# Adding user relation to workspace | ||
workspace.user.add(User.objects.get(user_id=user)) | ||
cache.delete(str(workspace.id)) | ||
else: | ||
workspace = Workspace.objects.create( | ||
name=name, | ||
org_id=org_id, | ||
) | ||
|
||
workspace.user.add(User.objects.get(user_id=user)) | ||
|
||
auth_tokens = AuthToken.objects.get(user__user_id=user) | ||
|
||
cluster_domain = get_cluster_domain(auth_tokens.refresh_token) | ||
|
||
FyleCredential.objects.update_or_create( | ||
refresh_token=auth_tokens.refresh_token, | ||
workspace_id=workspace.id, | ||
cluster_domain=cluster_domain | ||
) | ||
|
||
return workspace | ||
|
||
|
||
class ExportSettingsSerializer(serializers.ModelSerializer): | ||
""" | ||
Export Settings serializer | ||
""" | ||
class Meta: | ||
model = ExportSetting | ||
fields = '__all__' | ||
read_only_fields = ('id', 'workspace', 'created_at', 'updated_at') | ||
|
||
def create(self, validated_data): | ||
""" | ||
Create Export Settings | ||
""" | ||
assert_valid(validated_data, 'Body cannot be null') | ||
workspace_id = self.context['request'].parser_context.get('kwargs').get('workspace_id') | ||
|
||
export_settings, _ = ExportSetting.objects.update_or_create( | ||
workspace_id=workspace_id, | ||
defaults=validated_data | ||
) | ||
|
||
# Update workspace onboarding state | ||
workspace = export_settings.workspace | ||
|
||
if workspace.onboarding_state == 'EXPORT_SETTINGS': | ||
workspace.onboarding_state = 'IMPORT_SETTINGS' | ||
workspace.save() | ||
|
||
return export_settings |
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 | ||
|
||
from apps.workspaces.views import ( | ||
ReadyView, | ||
WorkspaceView, | ||
ExportSettingView | ||
) | ||
|
||
|
||
workspace_app_paths = [ | ||
path('', WorkspaceView.as_view(), name='workspaces'), | ||
path('ready/', ReadyView.as_view(), name='ready'), | ||
path('<int:workspace_id>/export_settings/', ExportSettingView.as_view(), name='export-settings'), | ||
|
||
] | ||
|
||
other_app_paths = [] | ||
|
||
urlpatterns = [] | ||
urlpatterns.extend(workspace_app_paths) | ||
urlpatterns.extend(other_app_paths) |
Oops, something went wrong.