Skip to content

Commit

Permalink
Workspace APIs Added (#6)
Browse files Browse the repository at this point in the history
* Workspace APIs Added

* Github action added for tests

* Export Settings APIs
  • Loading branch information
ruuushhh authored Oct 31, 2023
1 parent f9dc74f commit e11598f
Show file tree
Hide file tree
Showing 20 changed files with 935 additions and 32 deletions.
30 changes: 30 additions & 0 deletions .github/workflows/tests.yml
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
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Pull python base image
FROM python:3.12.0-slim
FROM python:3.11-slim

# set environment variables
ENV PYTHONDONTWRITEBYTECODE 1
Expand Down
52 changes: 52 additions & 0 deletions apps/fyle/helpers.py
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']
171 changes: 171 additions & 0 deletions apps/workspaces/models.py
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'
98 changes: 98 additions & 0 deletions apps/workspaces/serializers.py
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
21 changes: 21 additions & 0 deletions apps/workspaces/urls.py
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)
Loading

0 comments on commit e11598f

Please sign in to comment.