Compare commits

..

No commits in common. "dev-master" and "master" have entirely different histories.

21 changed files with 67 additions and 574 deletions

View File

@ -1,7 +1,7 @@
from django.contrib.auth.models import Group from django.contrib.auth.models import Group
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from rest_framework import serializers from rest_framework import serializers
from qrtr_account.models import Account, BankAccount, Institution, Transaction, Slice, Rule, SubscriptionPlan from qrtr_account.models import Account, Bank, Institution, Transaction, Slice, Rule
from user.models import User from user.models import User
from connection.models import Connection, ConnectionType from connection.models import Connection, ConnectionType
from connection.serializers import ConnectionTypeSerializer, ConnectionSerializer from connection.serializers import ConnectionTypeSerializer, ConnectionSerializer
@ -42,20 +42,13 @@ class UserSerializer(serializers.HyperlinkedModelSerializer):
class GroupSerializer(serializers.HyperlinkedModelSerializer): class GroupSerializer(serializers.HyperlinkedModelSerializer):
class Meta: class Meta:
model = Group model = Group
fields = ['pk', 'url', 'name'] fields = ['url', 'name']
class SubscriptionPlanSerializer(serializers.HyperlinkedModelSerializer): class BankSerializer(serializers.HyperlinkedModelSerializer):
class Meta: class Meta:
model = SubscriptionPlan model = Bank
fields = ['pk', 'name', 'status']
class BankAccountSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = BankAccount
fields = [ fields = [
'pk',
'url', 'url',
'qrtr_account', 'qrtr_account',
'connection', 'connection',
@ -64,10 +57,6 @@ class BankAccountSerializer(serializers.HyperlinkedModelSerializer):
'balance', 'balance',
'ac_type', 'ac_type',
'ac_subtype', 'ac_subtype',
'connection_type',
'institution_name',
'plan',
'plan_name'
] ]
extra_kwargs = { extra_kwargs = {
'balance': {'read_only': True}, 'balance': {'read_only': True},
@ -78,10 +67,10 @@ class BankAccountSerializer(serializers.HyperlinkedModelSerializer):
} }
class BankAccountSerializerPOST(BankAccountSerializer): class BankSerializerPOST(BankSerializer):
"""Separate Serializer for POST requests to create a new bank. This adds """Separate Serializer for POST requests to create a new bank. This adds
a new field called connection_details that is used to create a new a new field called connection_details that is used to create a new
connection record to go with the new BankAccount. This field is only allowed on connection record to go with the new Bank. This field is only allowed on
POST because we don't want to expose this information to the user, or allow POST because we don't want to expose this information to the user, or allow
them to change it b/c that could lead to an integrity problem, breaking them to change it b/c that could lead to an integrity problem, breaking
their bank functionality. their bank functionality.
@ -94,9 +83,8 @@ class BankAccountSerializerPOST(BankAccountSerializer):
# "credentials": {}}) # "credentials": {}})
class Meta: class Meta:
model = BankAccount model = Bank
fields = [ fields = [
'pk',
'url', 'url',
'qrtr_account', 'qrtr_account',
'connection', 'connection',
@ -119,13 +107,13 @@ class BankAccountSerializerPOST(BankAccountSerializer):
class InstitutionSerializer(serializers.HyperlinkedModelSerializer): class InstitutionSerializer(serializers.HyperlinkedModelSerializer):
class Meta: class Meta:
model = Institution model = Institution
fields = ['pk', 'url', 'name'] fields = ['url', 'name']
class TransactionSerializer(serializers.HyperlinkedModelSerializer): class TransactionSerializer(serializers.HyperlinkedModelSerializer):
class Meta: class Meta:
model = Transaction model = Transaction
fields = ['pk', 'url', 'authorized_date', fields = ['url', 'authorized_date',
'bank', 'name','details','slice','trans_id', 'bank', 'name','details','slice','trans_id',
'updated_at','created_at'] 'updated_at','created_at']
@ -133,15 +121,9 @@ class TransactionSerializer(serializers.HyperlinkedModelSerializer):
class SliceSerializer(serializers.HyperlinkedModelSerializer): class SliceSerializer(serializers.HyperlinkedModelSerializer):
class Meta: class Meta:
model = Slice model = Slice
fields = ['pk', 'url', 'name', 'icon', 'description', 'balance', 'slice_of', 'bank_acc'] fields = ['url', 'name', 'icon', 'budget', 'slice_of']
class SliceTransactionSerializer(serializers.ModelSerializer):
transactions = TransactionSerializer(many=True, read_only=True)
class Meta:
model = Slice
fields = ['pk', 'url', 'name', 'icon', 'description', 'balance', 'slice_of', 'transactions', 'bank_acc']
class RuleSerializer(serializers.HyperlinkedModelSerializer): class RuleSerializer(serializers.HyperlinkedModelSerializer):
class Meta: class Meta:
model = Rule model = Rule

View File

@ -1,150 +0,0 @@
from .abstract import AbstractConnectionClient
from django.conf import settings
import os
import datetime
import plaid
from plaid.api import plaid_api
from plaid.model.link_token_create_request import LinkTokenCreateRequest
from plaid.model.link_token_create_request_user import LinkTokenCreateRequestUser
from plaid.model.item_public_token_exchange_request import ItemPublicTokenExchangeRequest
from plaid.model.transactions_sync_request import TransactionsSyncRequest
from plaid.model.accounts_get_request import AccountsGetRequest
from plaid.model.products import Products
from plaid.model.country_code import CountryCode
def format_error(e):
return {
'error': {
'display_message': e.display_message,
'error_code': e.code,
'error_type': e.type,
'error_message': e.message}}
class Connection(AbstractConnectionClient):
def __init__(self, credentials, account_id=None):
self.credentials = credentials
self.account_id = account_id
# Fill in your Plaid API keys -
# https://dashboard.plaid.com/account/keys
self.PLAID_CLIENT_ID = settings.PLAID_CLIENT_ID
self.PLAID_SECRET = settings.PLAID_SECRET
self.PLAID_PUBLIC_KEY = settings.PLAID_PUBLIC_KEY
# Use 'sandbox' to test with Plaid's Sandbox environment (usplaid-python==9.2.0ername: user_good,
# password: pass_good)
# Use `development` to test with live users and credentials and `production`
# to go live
self.PLAID_ENV = settings.PLAID_ENV
# PLAID_PRODUCTS is a comma-separated list of products to use when initializing
# Link. Note that this list must contain 'assets' in order for the app to be
# able to create and retrieve asset reports.
self.PLAID_PRODUCTS = settings.PLAID_PRODUCTS
# PLAID_COUNTRY_CODES is a comma-separated list of countries for which users
# will be able to select institutions from.
self.PLAID_COUNTRY_CODES = settings.PLAID_COUNTRY_CODES
configuration = plaid.Configuration(
host=self.PLAID_ENV,
api_key={
'clientId': self.PLAID_CLIENT_ID,
'secret': self.PLAID_SECRET,
}
)
self.api_client = plaid.ApiClient(configuration)
self.client = plaid_api.PlaidApi(self.api_client)
# # Create a link_token for the given user
# request = LinkTokenCreateRequest(
# products=[Products("auth")],
# client_name="Qrtr Plaid",
# country_codes=[CountryCode('US')],
# #redirect_uri='https://domainname.com/oauth-page.html',
# language='en',
# webhook='https://webhook.example.com',
# user=LinkTokenCreateRequestUser(
# client_user_id=self.account_id
# )
# )
# response = client.link_token_create(request)
# resp_dict = response.to_dict()
# resp_dict['expiration'] = resp_dict['expiration'].strftime('%s')
# self.credentials.update(resp_dict)
# return self.credentials
def generate_auth_request(self):
# Create a link_token for the given user
request = LinkTokenCreateRequest(
products=[Products("auth")],
client_name="Qrtr Plaid",
country_codes=[CountryCode('US')],
# redirect_uri='https://domainname.com/oauth-page.html',
language='en',
webhook='https://webhook.example.com',
user=LinkTokenCreateRequestUser(
client_user_id=self.account_id
)
)
response = self.client.link_token_create(request)
resp_dict = response.to_dict()
resp_dict['expiration'] = resp_dict['expiration'].strftime('%s')
self.credentials.update(resp_dict)
def get_auth_token(self, public_token):
try:
exchange_request = ItemPublicTokenExchangeRequest(
public_token=public_token
)
exchange_response = self.client.item_public_token_exchange(
exchange_request)
except Exception as e:
print("Error Occurred")
print(e)
return format_error(e)
access_token = exchange_response['access_token']
item_id = exchange_response['item_id']
self.credentials.update({"access_token": access_token, "item_id": item_id})
return {"access_token": access_token, "item_id": item_id}
def get_accounts(self, auth_token=None):
if not auth_token:
auth_token = self.credentials.get('access_token')
if not auth_token:
raise Exception("Missing Auth Token")
try:
acc_request = AccountsGetRequest(access_token=auth_token)
accounts = self.client.accounts_get(acc_request).to_dict()
except Exception as e:
print(e)
accounts = None
return accounts
def get_transactions(
self,
start_date=None,
end_date=None,
auth_token=None):
if not auth_token:
auth_token = self.credentials.get('access_token')
request = TransactionsSyncRequest(
access_token=auth_token,
)
response = self.client.transactions_sync(request)
transactions = response['added']
# the transactions in the response are paginated, so make multiple calls while incrementing the cursor to
# retrieve all transactions
while (response['has_more']):
request = TransactionsSyncRequest(
access_token=auth_token,
cursor=response['next_cursor']
)
response = self.client.transactions_sync(request)
transactions += response['added']
return transactions

View File

@ -1,6 +1,5 @@
from rest_framework import serializers from rest_framework import serializers
from .models import ConnectionType, Connection from .models import ConnectionType, Connection
from qrtr_account.models import BankAccount
class ConnectionTypeSerializer(serializers.HyperlinkedModelSerializer): class ConnectionTypeSerializer(serializers.HyperlinkedModelSerializer):
@ -12,24 +11,12 @@ class ConnectionTypeSerializer(serializers.HyperlinkedModelSerializer):
'filename': {'read_only': True} 'filename': {'read_only': True}
} }
class ConnectionAccountSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = Connection
fields = ['pk', 'url', 'name', 'type']
class BankAccountReadSerializer(serializers.HyperlinkedModelSerializer):
connection = ConnectionAccountSerializer(read_only=True)
class Meta:
model = BankAccount
fields = ['pk', 'url', 'connection']
class ConnectionSerializer(serializers.HyperlinkedModelSerializer): class ConnectionSerializer(serializers.HyperlinkedModelSerializer):
bank_accounts = BankAccountReadSerializer(read_only=True, source='*')
class Meta: class Meta:
model = Connection model = Connection
fields = ['url', 'name', 'type', 'credentials', 'bank_accounts'] fields = ['url', 'name', 'type', 'credentials']
extra_kwargs = { extra_kwargs = {
'type': {'write_only': True}, 'type': {'write_only': True},
'credentials': {'write_only': True} 'credentials': {'write_only': True}
} }

View File

@ -1,4 +1,4 @@
from qrtr_account.models import Transaction, BankAccount, Account from qrtr_account.models import Transaction, Bank, Account
from connection.models import Connection from connection.models import Connection
@ -15,7 +15,7 @@ def get_and_save_transactions(connection, start_date=None, end_date=None):
defaults={"authorized_date": trns.get("authorized_date"), defaults={"authorized_date": trns.get("authorized_date"),
"trans_id": trns.get("transaction_id"), "trans_id": trns.get("transaction_id"),
"details": trns, "details": trns,
"bank": BankAccount.objects.get(acc_id=trns.get("account_id")), "bank": Bank.objects.get(acc_id=trns.get("account_id")),
"name": trns.get("name")}) "name": trns.get("name")})
return True return True

View File

@ -11,9 +11,6 @@ import importlib
import json import json
from .serializers import ConnectionSerializer, ConnectionTypeSerializer from .serializers import ConnectionSerializer, ConnectionTypeSerializer
from django.db import transaction from django.db import transaction
from qrtr_account.mixins import OwnedAccountsMixin
import traceback
# Create your views here. # Create your views here.
@ -23,7 +20,7 @@ class ConnectionTypeViewSet(viewsets.ModelViewSet):
serializer_class = ConnectionTypeSerializer serializer_class = ConnectionTypeSerializer
class ConnectionViewSet(viewsets.ModelViewSet, OwnedAccountsMixin): class ConnectionViewSet(viewsets.ModelViewSet):
"""API endpoint that allows connections to be seen or created """API endpoint that allows connections to be seen or created
""" """
permission_classes = [IsAuthenticated] permission_classes = [IsAuthenticated]
@ -36,67 +33,18 @@ class ConnectionViewSet(viewsets.ModelViewSet, OwnedAccountsMixin):
'delete', 'delete',
'options'] 'options']
def get_queryset(self):
return Connection.objects.filter(
account__in=self.accessible_accounts().values_list('id'))
@action(detail=False, methods=['post'], url_path='plaid/exchange_public_token')
def exchange_public_token(self, request):
print(f"REQUEST: {request.data}")
name = request.data.get("name", "dummyName")
account_id = request.data.get("account")
public_token = request.data.get("public_token")
user = request.user
if request.user.is_anonymous:
accounts = (Account.objects.filter(pk=1))
else:
accounts = (Account.objects.filter(pk=account_id, owner=user) |
Account.objects.filter(pk=account_id,
admin_users__in=[user]))
if not accounts:
return Response(
status=status.HTTP_400_BAD_REQUEST,
data="ERROR: Account ID not found")
else:
print(f"Account Found: {accounts[0]}")
account = accounts[0]
print(request)
plaid_conn = importlib.import_module(f"connection.connections.plaid_client_v2")
conn_type = ConnectionType.objects.get(name="Plaid")
try:
plaid_client = plaid_conn.Connection(request.data.dict(), account_id=account_id)
token = plaid_client.get_auth_token(public_token)
except ValueError:
return Response(status=status.HTTP_503,
data="ERROR: Invalid public_token")
with transaction.atomic():
conn, created = Connection.objects \
.get_or_create(name=name, type=conn_type,
defaults={
"credentials": request.data,
"account": account
})
conn.credentials = plaid_client.credentials
print(f"CREDS: {plaid_client.credentials}")
conn.save()
serializer = self.get_serializer(conn, context={'request':request})
print("DATA:")
print(serializer.data)
return Response(serializer.data)
@action(detail=False, methods=['post'], url_path='plaid') @action(detail=False, methods=['post'], url_path='plaid')
def authenticate(self, request): def authenticate(self, request):
print(request.data) print(request.data)
print(request.data.keys()) print(request.data.keys())
# public_token = request.data.get("public_token") public_token = request.data.get("public_token")
name = request.data.get("name", "dummyName") name = request.data.get("name", "dummyName")
account_id = request.data.get("account") account_id = request.data.get("account")
print(f"Account ID Detected: {account_id}") print(f"Account ID Detected: {account_id}")
# if public_token is None: if public_token is None:
# return Response( return Response(
# status=status.HTTP_400_BAD_REQUEST, status=status.HTTP_400_BAD_REQUEST,
# data="ERROR: missing public_token") data="ERROR: missing public_token")
if account_id is None: if account_id is None:
return Response( return Response(
status=status.HTTP_400_BAD_REQUEST, status=status.HTTP_400_BAD_REQUEST,
@ -104,9 +52,6 @@ class ConnectionViewSet(viewsets.ModelViewSet, OwnedAccountsMixin):
user = request.user user = request.user
# Filter out any accounts with the right id, but the given user # Filter out any accounts with the right id, but the given user
# is not an owner or admin on that account. # is not an owner or admin on that account.
if request.user.is_anonymous:
accounts = (Account.objects.filter(pk=1))
else:
accounts = (Account.objects.filter(pk=account_id, owner=user) | accounts = (Account.objects.filter(pk=account_id, owner=user) |
Account.objects.filter(pk=account_id, Account.objects.filter(pk=account_id,
admin_users__in=[user])) admin_users__in=[user]))
@ -118,17 +63,15 @@ class ConnectionViewSet(viewsets.ModelViewSet, OwnedAccountsMixin):
print(f"Account Found: {accounts[0]}") print(f"Account Found: {accounts[0]}")
account = accounts[0] account = accounts[0]
print(request) print(request)
plaid_conn = importlib.import_module(f"connection.connections.plaid_client_v2") plaid_conn = importlib.import_module(f"connection.connections.plaid_client")
conn_type = ConnectionType.objects.get(name="Plaid") conn_type = ConnectionType.objects.get(name="Plaid")
try: try:
plaid_client = plaid_conn.Connection(request.data.dict(), account_id=account_id) plaid_client = plaid_conn.Connection(request.data)
plaid_client.generate_auth_request()
except ValueError: except ValueError:
return Response(status=status.HTTP_503, return Response(status=status.HTTP_503,
data="ERROR: Invalid public_token") data="ERROR: Invalid public_token")
except Exception as e: except Exception as e:
print(e) print(e)
print(traceback.format_exc())
return Response(status=status.HTTP_500_INTERNAL_SERVER_ERROR, return Response(status=status.HTTP_500_INTERNAL_SERVER_ERROR,
data="ERROR: Unable to contact Plaid") data="ERROR: Unable to contact Plaid")
with transaction.atomic(): with transaction.atomic():
@ -139,9 +82,8 @@ class ConnectionViewSet(viewsets.ModelViewSet, OwnedAccountsMixin):
"account": account "account": account
}) })
conn.credentials = plaid_client.credentials conn.credentials = plaid_client.credentials
print(f"CREDS: {plaid_client.credentials}")
conn.save() conn.save()
return Response(plaid_client.credentials) return Response(plaid_client.get_accounts())
@action(detail=False, methods=['post'], url_path='plaid-webhook', @action(detail=False, methods=['post'], url_path='plaid-webhook',
permission_classes=[AllowAny]) permission_classes=[AllowAny])

View File

@ -12,7 +12,6 @@ https://docs.djangoproject.com/en/2.2/ref/settings/
import os import os
import smtplib import smtplib
from datetime import timedelta
try: try:
from .local import * from .local import *
@ -123,20 +122,6 @@ REST_FRAMEWORK = {
REST_USE_JWT = True REST_USE_JWT = True
SIMPLE_JWT = {
'ACCESS_TOKEN_LIFETIME': timedelta(days=1),
'REFRESH_TOKEN_LIFETIME': timedelta(days=30),
'ROTATE_REFRESH_TOKENS': True,
}
REST_AUTH = {
"USE_JWT": True,
'JWT_AUTH_RETURN_EXPIRATION': True,
'JWT_AUTH_HTTPONLY': False,
}
AUTH_USER_MODEL = 'user.User' AUTH_USER_MODEL = 'user.User'
# Password validation # Password validation

View File

@ -12,7 +12,6 @@ DATABASES = {"default": DEFAULT_CONNECTION, }
# SECURITY WARNING: keep the secret key used in production secret! # SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = os.environ.get("SECRET_KEY") SECRET_KEY = os.environ.get("SECRET_KEY")
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
# SECURITY WARNING: don't run with debug turned on in production! # SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True DEBUG = True

View File

@ -27,12 +27,12 @@ from user.views import (UserViewSet,
) )
from qrtr_account.views import (AccountViewSet, from qrtr_account.views import (AccountViewSet,
BankAccountViewSet, BankViewSet,
InstitutionViewSet, InstitutionViewSet,
TransactionViewSet, TransactionViewSet,
SliceViewSet, SliceTransactionViewSet, SliceViewSet,
FacebookLogin, FacebookLogin,
TwitterLogin, SubscriptionPlanViewSet) TwitterLogin)
from connection.views import ConnectionViewSet, ConnectionTypeViewSet from connection.views import ConnectionViewSet, ConnectionTypeViewSet
@ -59,15 +59,12 @@ router = routers.DefaultRouter()
router.register(r'users', UserViewSet) router.register(r'users', UserViewSet)
router.register(r'groups', GroupViewSet) router.register(r'groups', GroupViewSet)
router.register(r'accounts', AccountViewSet) router.register(r'accounts', AccountViewSet)
router.register(r'bank-accounts', BankAccountViewSet) router.register(r'banks', BankViewSet)
router.register(r'institutions', InstitutionViewSet) router.register(r'institutions', InstitutionViewSet)
router.register(r'transactions', TransactionViewSet) router.register(r'transactions', TransactionViewSet)
router.register(r'slices', SliceViewSet) router.register(r'slices', SliceViewSet)
router.register(r'slices/(?P<slice_pk>\d+)/transactions', #router.register(r'connections',ConnectionViewSet)
SliceTransactionViewSet, basename='slices')
router.register(r'connections',ConnectionViewSet)
router.register(r'connectiontypes', ConnectionTypeViewSet) router.register(r'connectiontypes', ConnectionTypeViewSet)
router.register(r'plans', SubscriptionPlanViewSet)
# Wire up our API using automatic URL routing. # Wire up our API using automatic URL routing.
# Additionally, we include login URLs for the browsable API. # Additionally, we include login URLs for the browsable API.
@ -86,7 +83,6 @@ apipatterns = [
urlpatterns = [ urlpatterns = [
path('admin/', admin.site.urls), path('admin/', admin.site.urls),
path('api/v1/', include(apipatterns), name='api'), path('api/v1/', include(apipatterns), name='api'),
path('api-auth/', include('rest_framework.urls')),
# path('api/v1/schema/', SpectacularAPIView.as_view(), name='schema'), # path('api/v1/schema/', SpectacularAPIView.as_view(), name='schema'),
path('api/v1/docs', schema_view.with_ui('swagger', cache_timeout=0), name='schema-swagger-ui'), path('api/v1/docs', schema_view.with_ui('swagger', cache_timeout=0), name='schema-swagger-ui'),
path('api/v1/schema/redoc/', schema_view.with_ui('redoc', cache_timeout=0), name='schema-redoc'), path('api/v1/schema/redoc/', schema_view.with_ui('redoc', cache_timeout=0), name='schema-redoc'),

View File

@ -1,5 +1,5 @@
from django.contrib import admin from django.contrib import admin
from .models import Account, Institution, BankAccount, Transaction, Slice, SubscriptionPlan from .models import Account, Institution, Bank, Transaction, Slice
@admin.register(Account) @admin.register(Account)
@ -12,8 +12,8 @@ class InstitutionAdmin(admin.ModelAdmin):
pass pass
@admin.register(BankAccount) @admin.register(Bank)
class BankAccountAdmin(admin.ModelAdmin): class BankAdmin(admin.ModelAdmin):
pass pass
@ -25,7 +25,3 @@ class TransactionAdmin(admin.ModelAdmin):
@admin.register(Slice) @admin.register(Slice)
class SliceAdmin(admin.ModelAdmin): class SliceAdmin(admin.ModelAdmin):
pass pass
@admin.register(SubscriptionPlan)
class SubscriptionPlanAdmin(admin.ModelAdmin):
pass

View File

@ -1,9 +1,9 @@
from qrtr_account.models import Account, Institution, BankAccount from qrtr_account.models import Account, Institution, Bank
from connection.models import Connection from connection.models import Connection
dummy_ac = Account.objects.all()[0] dummy_ac = Account.objects.all()[0]
dummy_isnt = Institution.objects.all()[0] dummy_isnt = Institution.objects.all()[0]
dummy_bank = BankAccount.objects.all()[0] dummy_bank = Bank.objects.all()[0]
conn_dummy = Connection.objects.all()[0] conn_dummy = Connection.objects.all()[0]
@ -142,5 +142,5 @@ for account in accounts:
"balance":account.get("balances",{}).get("current"), "balance":account.get("balances",{}).get("current"),
"balance_limit":account.get("balances",{}).get("limit") "balance_limit":account.get("balances",{}).get("limit")
} }
BankAccount.objects.update_or_create(qrtr_account=dummy_ac, acc_id=account.get("account_id"), Bank.objects.update_or_create(qrtr_account=dummy_ac, acc_id=account.get("account_id"),
defaults=fields) defaults=fields)

View File

@ -669,11 +669,11 @@ transactions = [
print(len(transactions)) print(len(transactions))
for transaction in transactions: for transaction in transactions:
bank = BankAccount.objects.filter(acc_id=transaction.get("account_id",[None]))[0] bank = Bank.objects.filter(acc_id=transaction.get("account_id",[None]))[0]
print(bank) print(bank)
if bank: if bank:
fields = { fields = {
"datetime":datetime.strptime(transaction.get("date"),"%Y-%m-%d"), "datetime":datetime.strptime(transaction.get("date"),"%Y-%m-%d"),
"details":transaction "details":transaction
} }
Transaction.objects.update_or_create(BankAccount=bank,defaults=fields) Transaction.objects.update_or_create(Bank=bank,defaults=fields)

View File

@ -1,29 +0,0 @@
# Generated by Django 3.2.3 on 2023-07-20 01:05
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('connection', '0003_auto_20201125_2242'),
('qrtr_account', '0013_auto_20211229_1935'),
]
operations = [
migrations.RenameModel(
old_name='Bank',
new_name='BankAccount',
),
migrations.RenameField(
model_name='slice',
old_name='budget',
new_name='balance',
),
migrations.AlterField(
model_name='transaction',
name='slice',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='transactions', to='qrtr_account.slice'),
),
]

View File

@ -1,19 +0,0 @@
# Generated by Django 3.2.3 on 2023-07-20 01:15
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('qrtr_account', '0014_auto_20230720_0105'),
]
operations = [
migrations.AlterField(
model_name='bankaccount',
name='institution',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='bank_accounts', to='qrtr_account.institution'),
),
]

View File

@ -1,19 +0,0 @@
# Generated by Django 3.2.3 on 2023-08-31 02:45
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('qrtr_account', '0015_alter_bankaccount_institution'),
]
operations = [
migrations.AddField(
model_name='slice',
name='bank_acc',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='qrtr_account.bankaccount'),
),
]

View File

@ -1,18 +0,0 @@
# Generated by Django 3.2.3 on 2023-08-31 03:55
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('qrtr_account', '0016_slice_bank_acc'),
]
operations = [
migrations.AlterField(
model_name='slice',
name='balance',
field=models.DecimalField(blank=True, decimal_places=3, max_digits=100, null=True),
),
]

View File

@ -1,27 +0,0 @@
# Generated by Django 3.2.3 on 2024-01-18 03:19
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('qrtr_account', '0017_alter_slice_balance'),
]
operations = [
migrations.CreateModel(
name='SubscriptionPlan',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=250)),
('status', models.CharField(choices=[('active', 'Active'), ('inactive', 'Inactive')], max_length=10)),
],
),
migrations.AddField(
model_name='bankaccount',
name='plan',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='qrtr_account.subscriptionplan'),
),
]

View File

@ -1,19 +0,0 @@
# Generated by Django 3.2.3 on 2024-08-01 00:55
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('qrtr_account', '0018_auto_20240118_0319'),
]
operations = [
migrations.AddField(
model_name='rule',
name='bank_acc',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='qrtr_account.bankaccount'),
),
]

View File

@ -1,15 +0,0 @@
from django.db.models import Q
from qrtr_account.models import Account
class OwnedAccountsMixin():
"""Mixin to help getting a list of accounts
the given user is authorized to see
"""
def accessible_accounts(self):
usr = self.request.user
accs = Account.objects.filter(Q(owner=usr) |
Q(id__in=usr.admin_accounts.all().values_list('id')) |
Q(id__in=usr.view_accounts.all().values_list('id')))
return accs

View File

@ -20,12 +20,6 @@ class Account(models.Model):
def __str__(self): def __str__(self):
return f"{self.name}" return f"{self.name}"
class SubscriptionPlan(models.Model):
name = models.CharField(max_length=250)
status = models.CharField(choices=[('active','Active'), ('inactive', 'Inactive')], max_length=10)
def __str__(self):
return f"{self.name}"
class Institution(models.Model): class Institution(models.Model):
@ -39,13 +33,12 @@ class Institution(models.Model):
return f"{self.name}" return f"{self.name}"
class BankAccount(models.Model): class Bank(models.Model):
qrtr_account = models.ForeignKey(Account, on_delete=models.CASCADE) qrtr_account = models.ForeignKey(Account, on_delete=models.CASCADE)
connection = models.ForeignKey('connection.Connection', connection = models.ForeignKey('connection.Connection',
on_delete=models.CASCADE) on_delete=models.CASCADE)
institution = models.ForeignKey(Institution, on_delete=models.CASCADE, institution = models.ForeignKey(Institution, on_delete=models.CASCADE,
related_name="bank_accounts") related_name="banks")
plan = models.ForeignKey(SubscriptionPlan, on_delete=models.SET_NULL, null=True, blank=True)
acc_id = models.CharField(max_length=250, primary_key=True) acc_id = models.CharField(max_length=250, primary_key=True)
nickname = models.CharField(max_length=250) nickname = models.CharField(max_length=250)
official_name = models.CharField(max_length=250,blank=True, null=True) official_name = models.CharField(max_length=250,blank=True, null=True)
@ -55,18 +48,6 @@ class BankAccount(models.Model):
ac_subtype = models.CharField(max_length=250, blank=True) ac_subtype = models.CharField(max_length=250, blank=True)
mask = models.CharField(max_length=4,blank=True) mask = models.CharField(max_length=4,blank=True)
@property
def connection_type(self):
return self.connection.type.name
@property
def plan_name(self):
return self.plan.name
@property
def institution_name(self):
return self.institution.name
@property @property
def qid(self): def qid(self):
return f"B{self.pk}" return f"B{self.pk}"
@ -78,7 +59,7 @@ class BankAccount(models.Model):
class Slice(models.Model): class Slice(models.Model):
name = models.CharField(max_length=250) name = models.CharField(max_length=250)
icon = models.CharField(max_length=250) icon = models.CharField(max_length=250)
balance = models.DecimalField(decimal_places=3, max_digits=100, null=True, blank=True) budget = models.DecimalField(decimal_places=3, max_digits=100)
description = models.TextField(max_length=255, null=True, blank=True) description = models.TextField(max_length=255, null=True, blank=True)
avail_parents = models.Q( avail_parents = models.Q(
app_label='qrtr_account', app_label='qrtr_account',
@ -90,7 +71,6 @@ class Slice(models.Model):
limit_choices_to=avail_parents, limit_choices_to=avail_parents,
on_delete=models.CASCADE, null=True, blank=True) on_delete=models.CASCADE, null=True, blank=True)
parent_id = models.PositiveIntegerField(null=True) parent_id = models.PositiveIntegerField(null=True)
bank_acc = models.ForeignKey(BankAccount, on_delete=models.CASCADE, null=True, blank=True)
is_unsliced = models.BooleanField(default=False) is_unsliced = models.BooleanField(default=False)
slice_of = GenericForeignKey('parent_type', 'parent_id') slice_of = GenericForeignKey('parent_type', 'parent_id')
@ -108,7 +88,6 @@ class Schedule(models.Model):
class Rule(models.Model): class Rule(models.Model):
bank_acc = models.ForeignKey(BankAccount, on_delete=models.CASCADE, null=True, blank=True)
kinds = [("refill", "Refill"), ("increase", "Increase"), ("goal", "Goal")] kinds = [("refill", "Refill"), ("increase", "Increase"), ("goal", "Goal")]
kind = models.CharField(choices=kinds, max_length=255) kind = models.CharField(choices=kinds, max_length=255)
when_to_run = models.ForeignKey(Schedule, on_delete=models.CASCADE) when_to_run = models.ForeignKey(Schedule, on_delete=models.CASCADE)
@ -135,11 +114,11 @@ class Rule(models.Model):
class Transaction(models.Model): class Transaction(models.Model):
authorized_date = models.DateField(null=True) authorized_date = models.DateField(null=True)
bank = models.ForeignKey(BankAccount, on_delete=models.CASCADE, bank = models.ForeignKey(Bank, on_delete=models.CASCADE,
related_name='transactions') related_name='transactions')
name = models.CharField(max_length=255) name = models.CharField(max_length=255)
details = models.JSONField() details = models.JSONField()
slice = models.ForeignKey(Slice, on_delete=models.SET_NULL, null=True, related_name='transactions') slice = models.ForeignKey(Slice, on_delete=models.SET_NULL, null=True)
trans_id = models.CharField(max_length=255) trans_id = models.CharField(max_length=255)
updated_at = models.DateTimeField(auto_now=True) updated_at = models.DateTimeField(auto_now=True)
created_at = models.DateTimeField(auto_now_add=True) created_at = models.DateTimeField(auto_now_add=True)

View File

@ -1,23 +1,22 @@
from django.shortcuts import render from django.shortcuts import render
from rest_framework import viewsets, mixins from rest_framework import viewsets, mixins
from .models import Account, BankAccount, Institution, Transaction, Slice, Rule, SubscriptionPlan from .models import Account, Bank, Institution, Transaction, Slice, Rule
from rest_framework.permissions import IsAuthenticated from rest_framework.permissions import IsAuthenticated
from rest_framework.decorators import action from rest_framework.decorators import action
from connection.models import Connection, ConnectionType from connection.models import Connection, ConnectionType
from api.serializers import (AccountReadSerializer, AccountWriteSerializer, from api.serializers import (AccountReadSerializer, AccountWriteSerializer,
BankAccountSerializer, BankAccountSerializerPOST, BankSerializer, BankSerializerPOST,
InstitutionSerializer, InstitutionSerializer,
TransactionSerializer, TransactionSerializer,
ConnectionSerializer, ConnectionSerializer,
ConnectionTypeSerializer, ConnectionTypeSerializer,
SliceSerializer, SliceTransactionSerializer, SliceSerializer,
RuleSerializer, SubscriptionPlanSerializer) RuleSerializer)
from allauth.socialaccount.providers.facebook.views import FacebookOAuth2Adapter from allauth.socialaccount.providers.facebook.views import FacebookOAuth2Adapter
from dj_rest_auth.registration.views import SocialLoginView from dj_rest_auth.registration.views import SocialLoginView
from allauth.socialaccount.providers.twitter.views import TwitterOAuthAdapter from allauth.socialaccount.providers.twitter.views import TwitterOAuthAdapter
from dj_rest_auth.social_serializers import TwitterLoginSerializer from dj_rest_auth.social_serializers import TwitterLoginSerializer
from api.mixins import ReadWriteSerializerMixin from api.mixins import ReadWriteSerializerMixin
from qrtr_account.mixins import OwnedAccountsMixin
class TwitterLogin(SocialLoginView): class TwitterLogin(SocialLoginView):
@ -29,93 +28,51 @@ class FacebookLogin(SocialLoginView):
adapter_class = FacebookOAuth2Adapter adapter_class = FacebookOAuth2Adapter
class AccountViewSet(ReadWriteSerializerMixin, viewsets.ModelViewSet, OwnedAccountsMixin): class AccountViewSet(ReadWriteSerializerMixin, viewsets.ModelViewSet):
"""API endpoint that allows accounts to be viewed or edited """API endpoint that allows accounts to be viewed or edited
""" """
permission_classes = [IsAuthenticated]
queryset = Account.objects.all() queryset = Account.objects.all()
read_serializer_class = AccountReadSerializer read_serializer_class = AccountReadSerializer
write_serializer_class = AccountWriteSerializer write_serializer_class = AccountWriteSerializer
def get_queryset(self):
return self.accessible_accounts()
class BankViewSet(viewsets.ModelViewSet):
class BankAccountViewSet(viewsets.ModelViewSet, OwnedAccountsMixin): """API endpoint that allows Banks to be viewed or edited
"""API endpoint that allows BankAccounts to be viewed or edited
""" """
permission_classes = [IsAuthenticated] queryset = Bank.objects.all()
# serializer_class = BankSerializer
queryset = BankAccount.objects.all()
# serializer_class = BankAccountSerializer
def get_serializer_class(self): def get_serializer_class(self):
if self.action == 'create': if self.action == 'create':
return BankAccountSerializerPOST return BankSerializerPOST
return BankAccountSerializer return BankSerializer
def get_queryset(self):
return BankAccount.objects.filter(
qrtr_account__in=self.accessible_accounts().values_list('id'))
class SliceViewSet(viewsets.ModelViewSet, OwnedAccountsMixin): class SliceViewSet(viewsets.ModelViewSet):
"""API endpoint that allows BankAccounts to be viewed. """API endpoint that allows Banks to be viewed.
""" """
permission_classes = [IsAuthenticated]
queryset = Slice.objects.all() queryset = Slice.objects.all()
serializer_class = SliceSerializer serializer_class = SliceSerializer
filterset_fields = {
'id': ['exact', 'lte', 'gte'],
'name': ['exact',],
'balance': ['exact', 'lte', 'gte'],
'bank_acc': ['exact'],
# 'slice_of': ['exact']
}
def get_queryset(self):
return Slice.objects.select_related('bank_acc').filter(
bank_acc__qrtr_account__in=self.accessible_accounts().values_list('id')
)
class SubscriptionPlanViewSet(viewsets.ModelViewSet):
queryset = SubscriptionPlan.objects.all()
serializer_class = SubscriptionPlanSerializer
class InstitutionViewSet(viewsets.ReadOnlyModelViewSet): class InstitutionViewSet(viewsets.ReadOnlyModelViewSet):
"""API endpoint that allows BankAccounts to be viewed. """API endpoint that allows Banks to be viewed.
""" """
permission_classes = [IsAuthenticated]
queryset = Institution.objects.all() queryset = Institution.objects.all()
serializer_class = InstitutionSerializer serializer_class = InstitutionSerializer
class TransactionViewSet(viewsets.ModelViewSet, OwnedAccountsMixin): class TransactionViewSet(viewsets.ModelViewSet):
"""API endpoint that allows BankAccounts to be viewed. """API endpoint that allows Banks to be viewed.
""" """
permission_classes = [IsAuthenticated]
queryset = Transaction.objects.filter(is_split=False) queryset = Transaction.objects.filter(is_split=False)
serializer_class = TransactionSerializer serializer_class = TransactionSerializer
search_fields = ['name', 'slice__name', 'bank__nickname',
'bank__official_name']
filterset_fields = { filterset_fields = {
'slice__id': ['exact',],
'slice__name': ['exact', ],
'authorized_date': ['exact', 'lte', 'gte', 'isnull'], 'authorized_date': ['exact', 'lte', 'gte', 'isnull'],
'updated_at': ['exact', 'lte', 'gte', 'isnull'], 'updated_at': ['exact', 'lte', 'gte', 'isnull'],
'created_at': ['exact', 'lte', 'gte', 'isnull'], 'created_at': ['exact', 'lte', 'gte', 'isnull'],
'trans_id': ['exact', 'lte', 'gte'], 'trans_id': ['exact', 'lte', 'gte', 'isnull'],
'id': ['exact', 'lte', 'gte'],
'bank': ['exact']
} }
@action(detail=True, methods=['post'], url_path='split') @action(detail=True, methods=['post'], url_path='split')
@ -134,43 +91,9 @@ class TransactionViewSet(viewsets.ModelViewSet, OwnedAccountsMixin):
child2 = Transaction.objects.create(**base_information) child2 = Transaction.objects.create(**base_information)
child2.name = f"{child1.name}.split2" child2.name = f"{child1.name}.split2"
def get_queryset(self):
return Transaction.objects.select_related('bank').filter(
bank__qrtr_account__in=self.accessible_accounts().values_list('id')
).filter(is_split=False)
class RuleViewSet(viewsets.ReadOnlyModelViewSet):
class SliceTransactionViewSet(viewsets.ModelViewSet, OwnedAccountsMixin): """API endpoint that allows Banks to be viewed.
permission_classes = [IsAuthenticated]
serializer_class = SliceTransactionSerializer
queryset = Slice.objects.all()
filterset_fields = {
'id': ['exact', 'lte', 'gte'],
'name': ['exact',],
'balance': ['exact', 'lte', 'gte'],
'bank_acc': ['exact'],
# 'slice_of': ['exact']
}
def get_queryset(self):
return Slice.objects.select_related('bank_acc').filter(
bank_acc__qrtr_account__in=self.accessible_accounts().values_list('id')
)
# def get_queryset(self):
# return Transaction.objects.filter(slice__pk=self.kwargs.get('slice_pk'))
class RuleViewSet(viewsets.ReadOnlyModelViewSet, OwnedAccountsMixin):
"""API endpoint that allows BankAccounts to be viewed.
""" """
permission_classes = [IsAuthenticated]
queryset = Rule.objects.all() queryset = Rule.objects.all()
serializer_class = RuleSerializer serializer_class = RuleSerializer
def get_queryset(self):
return Rule.objects.select_related('bank_acc').filter(
bank_acc__qrtr_account__in=self.accessible_accounts().values_list('id')
)

View File

@ -5,10 +5,10 @@ chardet==4.0.0
cryptography==3.4.7 cryptography==3.4.7
defusedxml==0.7.1 defusedxml==0.7.1
dj-database-url==0.5.0 dj-database-url==0.5.0
dj-rest-auth==3.0.0 dj-rest-auth==2.1.5
django-rest-swagger==2.2.0 django-rest-swagger==2.2.0
Django==3.2.3 Django==3.2.3
django-allauth==0.50.0 django-allauth==0.44.0
django-cors-headers==3.7.0 django-cors-headers==3.7.0
django-filter==2.4.0 django-filter==2.4.0
djangorestframework==3.12.4 djangorestframework==3.12.4
@ -16,8 +16,8 @@ djangorestframework-simplejwt==4.6.0
drf-yasg==1.20.0 drf-yasg==1.20.0
idna==2.10 idna==2.10
oauthlib==3.1.0 oauthlib==3.1.0
plaid_python==14.0.0 plaid-python==9.2.0
psycopg2-binary==2.8.6 psycopg2==2.8.6
pycparser==2.20 pycparser==2.20
PyJWT==2.1.0 PyJWT==2.1.0
python3-openid==3.2.0 python3-openid==3.2.0