Compare commits
42 Commits
master
...
dev-master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dd3eee2a76 | ||
|
|
88f70323b0 | ||
|
|
4e188cd57e | ||
|
|
20aa85bf08 | ||
|
|
1411fbdf53 | ||
|
|
0d150971f7 | ||
|
|
8d15f325bb | ||
|
|
b98ad76080 | ||
|
|
05bdc13fc4 | ||
|
|
9c9a397d83 | ||
|
|
5d35596c12 | ||
|
|
8eb3190972 | ||
|
|
eafa2bbe75 | ||
|
|
93aa10d98d | ||
|
|
0c98606234 | ||
|
|
6ec9065f8e | ||
|
|
8bdbc05c59 | ||
|
|
cca13d4a4f | ||
|
|
64e8ae4bab | ||
|
|
8dc43c0132 | ||
|
|
dc1a60910f | ||
|
|
0640571fba | ||
|
|
b711675a0c | ||
|
|
23764c1886 | ||
|
|
42704350a4 | ||
|
|
e942f1a76e | ||
|
|
cd9a8c4d29 | ||
|
|
5b6c7bde1d | ||
|
|
6b377067e1 | ||
|
|
eee001e32b | ||
|
|
70453bcdcf | ||
|
|
0b32c1d20d | ||
|
|
e3b1060624 | ||
|
|
5e33e6a308 | ||
|
|
88b0f6242b | ||
|
|
25bc7dc347 | ||
|
|
899ea476d6 | ||
|
|
6d13ec9655 | ||
|
|
c6b8f16a99 | ||
|
|
8ff8b000e3 | ||
|
|
64a51b362a | ||
|
|
b6e8c0f108 |
@ -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, Bank, Institution, Transaction, Slice, Rule
|
from qrtr_account.models import Account, BankAccount, Institution, Transaction, Slice, Rule, SubscriptionPlan
|
||||||
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,13 +42,20 @@ class UserSerializer(serializers.HyperlinkedModelSerializer):
|
|||||||
class GroupSerializer(serializers.HyperlinkedModelSerializer):
|
class GroupSerializer(serializers.HyperlinkedModelSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Group
|
model = Group
|
||||||
fields = ['url', 'name']
|
fields = ['pk', 'url', 'name']
|
||||||
|
|
||||||
|
|
||||||
class BankSerializer(serializers.HyperlinkedModelSerializer):
|
class SubscriptionPlanSerializer(serializers.HyperlinkedModelSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Bank
|
model = SubscriptionPlan
|
||||||
|
fields = ['pk', 'name', 'status']
|
||||||
|
|
||||||
|
|
||||||
|
class BankAccountSerializer(serializers.HyperlinkedModelSerializer):
|
||||||
|
class Meta:
|
||||||
|
model = BankAccount
|
||||||
fields = [
|
fields = [
|
||||||
|
'pk',
|
||||||
'url',
|
'url',
|
||||||
'qrtr_account',
|
'qrtr_account',
|
||||||
'connection',
|
'connection',
|
||||||
@ -57,6 +64,10 @@ class BankSerializer(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},
|
||||||
@ -67,10 +78,10 @@ class BankSerializer(serializers.HyperlinkedModelSerializer):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class BankSerializerPOST(BankSerializer):
|
class BankAccountSerializerPOST(BankAccountSerializer):
|
||||||
"""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 Bank. This field is only allowed on
|
connection record to go with the new BankAccount. 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.
|
||||||
@ -83,8 +94,9 @@ class BankSerializerPOST(BankSerializer):
|
|||||||
# "credentials": {}})
|
# "credentials": {}})
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Bank
|
model = BankAccount
|
||||||
fields = [
|
fields = [
|
||||||
|
'pk',
|
||||||
'url',
|
'url',
|
||||||
'qrtr_account',
|
'qrtr_account',
|
||||||
'connection',
|
'connection',
|
||||||
@ -107,13 +119,13 @@ class BankSerializerPOST(BankSerializer):
|
|||||||
class InstitutionSerializer(serializers.HyperlinkedModelSerializer):
|
class InstitutionSerializer(serializers.HyperlinkedModelSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Institution
|
model = Institution
|
||||||
fields = ['url', 'name']
|
fields = ['pk', 'url', 'name']
|
||||||
|
|
||||||
|
|
||||||
class TransactionSerializer(serializers.HyperlinkedModelSerializer):
|
class TransactionSerializer(serializers.HyperlinkedModelSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Transaction
|
model = Transaction
|
||||||
fields = ['url', 'authorized_date',
|
fields = ['pk', 'url', 'authorized_date',
|
||||||
'bank', 'name','details','slice','trans_id',
|
'bank', 'name','details','slice','trans_id',
|
||||||
'updated_at','created_at']
|
'updated_at','created_at']
|
||||||
|
|
||||||
@ -121,9 +133,15 @@ class TransactionSerializer(serializers.HyperlinkedModelSerializer):
|
|||||||
class SliceSerializer(serializers.HyperlinkedModelSerializer):
|
class SliceSerializer(serializers.HyperlinkedModelSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Slice
|
model = Slice
|
||||||
fields = ['url', 'name', 'icon', 'budget', 'slice_of']
|
fields = ['pk', 'url', 'name', 'icon', 'description', 'balance', 'slice_of', 'bank_acc']
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
|||||||
150
connection/connections/plaid_client_v2.py
Executable file
150
connection/connections/plaid_client_v2.py
Executable file
@ -0,0 +1,150 @@
|
|||||||
|
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
|
||||||
@ -1,5 +1,6 @@
|
|||||||
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):
|
||||||
@ -11,12 +12,24 @@ class ConnectionTypeSerializer(serializers.HyperlinkedModelSerializer):
|
|||||||
'filename': {'read_only': True}
|
'filename': {'read_only': True}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class ConnectionAccountSerializer(serializers.HyperlinkedModelSerializer):
|
||||||
class ConnectionSerializer(serializers.HyperlinkedModelSerializer):
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Connection
|
model = Connection
|
||||||
fields = ['url', 'name', 'type', 'credentials']
|
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):
|
||||||
|
bank_accounts = BankAccountReadSerializer(read_only=True, source='*')
|
||||||
|
class Meta:
|
||||||
|
model = Connection
|
||||||
|
fields = ['url', 'name', 'type', 'credentials', 'bank_accounts']
|
||||||
extra_kwargs = {
|
extra_kwargs = {
|
||||||
'type': {'write_only': True},
|
'type': {'write_only': True},
|
||||||
'credentials': {'write_only': True}
|
'credentials': {'write_only': True}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
from qrtr_account.models import Transaction, Bank, Account
|
from qrtr_account.models import Transaction, BankAccount, 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": Bank.objects.get(acc_id=trns.get("account_id")),
|
"bank": BankAccount.objects.get(acc_id=trns.get("account_id")),
|
||||||
"name": trns.get("name")})
|
"name": trns.get("name")})
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|||||||
@ -11,6 +11,9 @@ 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.
|
||||||
|
|
||||||
@ -20,7 +23,7 @@ class ConnectionTypeViewSet(viewsets.ModelViewSet):
|
|||||||
serializer_class = ConnectionTypeSerializer
|
serializer_class = ConnectionTypeSerializer
|
||||||
|
|
||||||
|
|
||||||
class ConnectionViewSet(viewsets.ModelViewSet):
|
class ConnectionViewSet(viewsets.ModelViewSet, OwnedAccountsMixin):
|
||||||
"""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]
|
||||||
@ -33,28 +36,24 @@ class ConnectionViewSet(viewsets.ModelViewSet):
|
|||||||
'delete',
|
'delete',
|
||||||
'options']
|
'options']
|
||||||
|
|
||||||
@action(detail=False, methods=['post'], url_path='plaid')
|
def get_queryset(self):
|
||||||
def authenticate(self, request):
|
return Connection.objects.filter(
|
||||||
print(request.data)
|
account__in=self.accessible_accounts().values_list('id'))
|
||||||
print(request.data.keys())
|
|
||||||
public_token = request.data.get("public_token")
|
@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")
|
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}")
|
public_token = request.data.get("public_token")
|
||||||
if public_token is None:
|
|
||||||
return Response(
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
|
||||||
data="ERROR: missing public_token")
|
|
||||||
if account_id is None:
|
|
||||||
return Response(
|
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
|
||||||
data="ERROR: missing account_id")
|
|
||||||
user = request.user
|
user = request.user
|
||||||
# Filter out any accounts with the right id, but the given user
|
|
||||||
# is not an owner or admin on that account.
|
if request.user.is_anonymous:
|
||||||
accounts = (Account.objects.filter(pk=account_id, owner=user) |
|
accounts = (Account.objects.filter(pk=1))
|
||||||
Account.objects.filter(pk=account_id,
|
else:
|
||||||
admin_users__in=[user]))
|
accounts = (Account.objects.filter(pk=account_id, owner=user) |
|
||||||
|
Account.objects.filter(pk=account_id,
|
||||||
|
admin_users__in=[user]))
|
||||||
if not accounts:
|
if not accounts:
|
||||||
return Response(
|
return Response(
|
||||||
status=status.HTTP_400_BAD_REQUEST,
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
@ -63,15 +62,73 @@ class ConnectionViewSet(viewsets.ModelViewSet):
|
|||||||
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")
|
plaid_conn = importlib.import_module(f"connection.connections.plaid_client_v2")
|
||||||
conn_type = ConnectionType.objects.get(name="Plaid")
|
conn_type = ConnectionType.objects.get(name="Plaid")
|
||||||
try:
|
try:
|
||||||
plaid_client = plaid_conn.Connection(request.data)
|
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')
|
||||||
|
def authenticate(self, request):
|
||||||
|
print(request.data)
|
||||||
|
print(request.data.keys())
|
||||||
|
# public_token = request.data.get("public_token")
|
||||||
|
name = request.data.get("name", "dummyName")
|
||||||
|
account_id = request.data.get("account")
|
||||||
|
print(f"Account ID Detected: {account_id}")
|
||||||
|
# if public_token is None:
|
||||||
|
# return Response(
|
||||||
|
# status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
# data="ERROR: missing public_token")
|
||||||
|
if account_id is None:
|
||||||
|
return Response(
|
||||||
|
status=status.HTTP_400_BAD_REQUEST,
|
||||||
|
data="ERROR: missing account_id")
|
||||||
|
user = request.user
|
||||||
|
# Filter out any accounts with the right id, but the given user
|
||||||
|
# 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) |
|
||||||
|
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)
|
||||||
|
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():
|
||||||
@ -82,8 +139,9 @@ class ConnectionViewSet(viewsets.ModelViewSet):
|
|||||||
"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.get_accounts())
|
return Response(plaid_client.credentials)
|
||||||
|
|
||||||
@action(detail=False, methods=['post'], url_path='plaid-webhook',
|
@action(detail=False, methods=['post'], url_path='plaid-webhook',
|
||||||
permission_classes=[AllowAny])
|
permission_classes=[AllowAny])
|
||||||
|
|||||||
@ -12,6 +12,7 @@ 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 *
|
||||||
@ -122,6 +123,20 @@ 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
|
||||||
|
|||||||
@ -12,6 +12,7 @@ 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
|
||||||
|
|||||||
14
core/urls.py
14
core/urls.py
@ -27,12 +27,12 @@ from user.views import (UserViewSet,
|
|||||||
)
|
)
|
||||||
|
|
||||||
from qrtr_account.views import (AccountViewSet,
|
from qrtr_account.views import (AccountViewSet,
|
||||||
BankViewSet,
|
BankAccountViewSet,
|
||||||
InstitutionViewSet,
|
InstitutionViewSet,
|
||||||
TransactionViewSet,
|
TransactionViewSet,
|
||||||
SliceViewSet,
|
SliceViewSet, SliceTransactionViewSet,
|
||||||
FacebookLogin,
|
FacebookLogin,
|
||||||
TwitterLogin)
|
TwitterLogin, SubscriptionPlanViewSet)
|
||||||
|
|
||||||
from connection.views import ConnectionViewSet, ConnectionTypeViewSet
|
from connection.views import ConnectionViewSet, ConnectionTypeViewSet
|
||||||
|
|
||||||
@ -59,12 +59,15 @@ 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'banks', BankViewSet)
|
router.register(r'bank-accounts', BankAccountViewSet)
|
||||||
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'connections',ConnectionViewSet)
|
router.register(r'slices/(?P<slice_pk>\d+)/transactions',
|
||||||
|
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.
|
||||||
@ -83,6 +86,7 @@ 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'),
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from .models import Account, Institution, Bank, Transaction, Slice
|
from .models import Account, Institution, BankAccount, Transaction, Slice, SubscriptionPlan
|
||||||
|
|
||||||
|
|
||||||
@admin.register(Account)
|
@admin.register(Account)
|
||||||
@ -12,8 +12,8 @@ class InstitutionAdmin(admin.ModelAdmin):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
@admin.register(Bank)
|
@admin.register(BankAccount)
|
||||||
class BankAdmin(admin.ModelAdmin):
|
class BankAccountAdmin(admin.ModelAdmin):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
@ -25,3 +25,7 @@ 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
|
||||||
@ -1,9 +1,9 @@
|
|||||||
from qrtr_account.models import Account, Institution, Bank
|
from qrtr_account.models import Account, Institution, BankAccount
|
||||||
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 = Bank.objects.all()[0]
|
dummy_bank = BankAccount.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")
|
||||||
}
|
}
|
||||||
Bank.objects.update_or_create(qrtr_account=dummy_ac, acc_id=account.get("account_id"),
|
BankAccount.objects.update_or_create(qrtr_account=dummy_ac, acc_id=account.get("account_id"),
|
||||||
defaults=fields)
|
defaults=fields)
|
||||||
|
|||||||
@ -669,11 +669,11 @@ transactions = [
|
|||||||
print(len(transactions))
|
print(len(transactions))
|
||||||
|
|
||||||
for transaction in transactions:
|
for transaction in transactions:
|
||||||
bank = Bank.objects.filter(acc_id=transaction.get("account_id",[None]))[0]
|
bank = BankAccount.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(Bank=bank,defaults=fields)
|
Transaction.objects.update_or_create(BankAccount=bank,defaults=fields)
|
||||||
|
|||||||
29
qrtr_account/migrations/0014_auto_20230720_0105.py
Normal file
29
qrtr_account/migrations/0014_auto_20230720_0105.py
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
# 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'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -0,0 +1,19 @@
|
|||||||
|
# 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'),
|
||||||
|
),
|
||||||
|
]
|
||||||
19
qrtr_account/migrations/0016_slice_bank_acc.py
Normal file
19
qrtr_account/migrations/0016_slice_bank_acc.py
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
# 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'),
|
||||||
|
),
|
||||||
|
]
|
||||||
18
qrtr_account/migrations/0017_alter_slice_balance.py
Normal file
18
qrtr_account/migrations/0017_alter_slice_balance.py
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
# 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),
|
||||||
|
),
|
||||||
|
]
|
||||||
27
qrtr_account/migrations/0018_auto_20240118_0319.py
Normal file
27
qrtr_account/migrations/0018_auto_20240118_0319.py
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
# 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'),
|
||||||
|
),
|
||||||
|
]
|
||||||
19
qrtr_account/migrations/0019_rule_bank_acc.py
Normal file
19
qrtr_account/migrations/0019_rule_bank_acc.py
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
# 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'),
|
||||||
|
),
|
||||||
|
]
|
||||||
15
qrtr_account/mixins.py
Normal file
15
qrtr_account/mixins.py
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
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
|
||||||
@ -20,6 +20,12 @@ 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):
|
||||||
@ -33,12 +39,13 @@ class Institution(models.Model):
|
|||||||
return f"{self.name}"
|
return f"{self.name}"
|
||||||
|
|
||||||
|
|
||||||
class Bank(models.Model):
|
class BankAccount(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="banks")
|
related_name="bank_accounts")
|
||||||
|
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)
|
||||||
@ -48,6 +55,18 @@ class Bank(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}"
|
||||||
@ -59,7 +78,7 @@ class Bank(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)
|
||||||
budget = models.DecimalField(decimal_places=3, max_digits=100)
|
balance = models.DecimalField(decimal_places=3, max_digits=100, null=True, blank=True)
|
||||||
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',
|
||||||
@ -71,6 +90,7 @@ 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')
|
||||||
|
|
||||||
@ -88,6 +108,7 @@ 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)
|
||||||
@ -114,11 +135,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(Bank, on_delete=models.CASCADE,
|
bank = models.ForeignKey(BankAccount, 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)
|
slice = models.ForeignKey(Slice, on_delete=models.SET_NULL, null=True, related_name='transactions')
|
||||||
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)
|
||||||
|
|||||||
@ -1,22 +1,23 @@
|
|||||||
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, Bank, Institution, Transaction, Slice, Rule
|
from .models import Account, BankAccount, Institution, Transaction, Slice, Rule, SubscriptionPlan
|
||||||
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,
|
||||||
BankSerializer, BankSerializerPOST,
|
BankAccountSerializer, BankAccountSerializerPOST,
|
||||||
InstitutionSerializer,
|
InstitutionSerializer,
|
||||||
TransactionSerializer,
|
TransactionSerializer,
|
||||||
ConnectionSerializer,
|
ConnectionSerializer,
|
||||||
ConnectionTypeSerializer,
|
ConnectionTypeSerializer,
|
||||||
SliceSerializer,
|
SliceSerializer, SliceTransactionSerializer,
|
||||||
RuleSerializer)
|
RuleSerializer, SubscriptionPlanSerializer)
|
||||||
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):
|
||||||
@ -28,51 +29,93 @@ class FacebookLogin(SocialLoginView):
|
|||||||
adapter_class = FacebookOAuth2Adapter
|
adapter_class = FacebookOAuth2Adapter
|
||||||
|
|
||||||
|
|
||||||
class AccountViewSet(ReadWriteSerializerMixin, viewsets.ModelViewSet):
|
class AccountViewSet(ReadWriteSerializerMixin, viewsets.ModelViewSet, OwnedAccountsMixin):
|
||||||
"""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):
|
|
||||||
"""API endpoint that allows Banks to be viewed or edited
|
class BankAccountViewSet(viewsets.ModelViewSet, OwnedAccountsMixin):
|
||||||
|
"""API endpoint that allows BankAccounts to be viewed or edited
|
||||||
"""
|
"""
|
||||||
queryset = Bank.objects.all()
|
permission_classes = [IsAuthenticated]
|
||||||
# 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 BankSerializerPOST
|
return BankAccountSerializerPOST
|
||||||
return BankSerializer
|
return BankAccountSerializer
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
return BankAccount.objects.filter(
|
||||||
|
qrtr_account__in=self.accessible_accounts().values_list('id'))
|
||||||
|
|
||||||
|
|
||||||
class SliceViewSet(viewsets.ModelViewSet):
|
class SliceViewSet(viewsets.ModelViewSet, OwnedAccountsMixin):
|
||||||
"""API endpoint that allows Banks to be viewed.
|
"""API endpoint that allows BankAccounts 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 Banks to be viewed.
|
"""API endpoint that allows BankAccounts to be viewed.
|
||||||
"""
|
"""
|
||||||
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
queryset = Institution.objects.all()
|
queryset = Institution.objects.all()
|
||||||
serializer_class = InstitutionSerializer
|
serializer_class = InstitutionSerializer
|
||||||
|
|
||||||
|
|
||||||
class TransactionViewSet(viewsets.ModelViewSet):
|
class TransactionViewSet(viewsets.ModelViewSet, OwnedAccountsMixin):
|
||||||
"""API endpoint that allows Banks to be viewed.
|
"""API endpoint that allows BankAccounts 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', 'isnull'],
|
'trans_id': ['exact', 'lte', 'gte'],
|
||||||
|
'id': ['exact', 'lte', 'gte'],
|
||||||
|
'bank': ['exact']
|
||||||
}
|
}
|
||||||
|
|
||||||
@action(detail=True, methods=['post'], url_path='split')
|
@action(detail=True, methods=['post'], url_path='split')
|
||||||
@ -91,9 +134,43 @@ class TransactionViewSet(viewsets.ModelViewSet):
|
|||||||
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):
|
|
||||||
"""API endpoint that allows Banks to be viewed.
|
class SliceTransactionViewSet(viewsets.ModelViewSet, OwnedAccountsMixin):
|
||||||
|
|
||||||
|
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')
|
||||||
|
)
|
||||||
@ -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==2.1.5
|
dj-rest-auth==3.0.0
|
||||||
django-rest-swagger==2.2.0
|
django-rest-swagger==2.2.0
|
||||||
Django==3.2.3
|
Django==3.2.3
|
||||||
django-allauth==0.44.0
|
django-allauth==0.50.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==9.2.0
|
plaid_python==14.0.0
|
||||||
psycopg2==2.8.6
|
psycopg2-binary==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
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user