From 07bb796a5a18699dd51ac1251160713e58a2923c Mon Sep 17 00:00:00 2001 From: DJ Gillespie Date: Mon, 24 Feb 2020 19:41:24 -0700 Subject: [PATCH] refactor for slice and rule functionality. general cleanup --- connection/connections/plaid.py | 79 +++++++++++++++++++ .../migrations/0002_auto_20200106_2335.py | 18 +++++ .../migrations/0003_auto_20200107_0015.py | 26 ++++++ connection/migrations/0004_connection_type.py | 19 +++++ .../0005_remove_connection_type_old.py | 17 ++++ connection/serializers.py | 12 +++ connection/urls.py | 6 ++ qrtr_account/migrations/0005_rule_slice.py | 32 ++++++++ .../migrations/0006_auto_20200225_0155.py | 23 ++++++ .../migrations/0007_auto_20200225_0239.py | 56 +++++++++++++ 10 files changed, 288 insertions(+) create mode 100755 connection/connections/plaid.py create mode 100644 connection/migrations/0002_auto_20200106_2335.py create mode 100644 connection/migrations/0003_auto_20200107_0015.py create mode 100644 connection/migrations/0004_connection_type.py create mode 100644 connection/migrations/0005_remove_connection_type_old.py create mode 100755 connection/serializers.py create mode 100755 connection/urls.py create mode 100644 qrtr_account/migrations/0005_rule_slice.py create mode 100644 qrtr_account/migrations/0006_auto_20200225_0155.py create mode 100644 qrtr_account/migrations/0007_auto_20200225_0239.py diff --git a/connection/connections/plaid.py b/connection/connections/plaid.py new file mode 100755 index 0000000..a91c3d6 --- /dev/null +++ b/connection/connections/plaid.py @@ -0,0 +1,79 @@ +from .abstract import AbstractConnectionClient +import plaid +import os +import datetime + + +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): + self.credentials = credentials + + # Fill in your Plaid API keys - + # https://dashboard.plaid.com/account/keys + self.PLAID_CLIENT_ID = os.getenv('PLAID_CLIENT_ID') + self.PLAID_SECRET = os.getenv('PLAID_SECRET') + self.PLAID_PUBLIC_KEY = os.getenv('PLAID_PUBLIC_KEY') + # Use 'sandbox' to test with Plaid's Sandbox environment (username: user_good, + # password: pass_good) + # Use `development` to test with live users and credentials and `production` + # to go live + self.PLAID_ENV = os.getenv('PLAID_ENV', 'sandbox') + # 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 = os.getenv('PLAID_PRODUCTS', 'transactions') + + # PLAID_COUNTRY_CODES is a comma-separated list of countries for which users + # will be able to select institutions from. + self.PLAID_COUNTRY_CODES = os.getenv( + 'PLAID_COUNTRY_CODES', 'US,CA,GB,FR,ES') + self.client = plaid.Client( + client_id=self.PLAID_CLIENT_ID, + secret=self.PLAID_SECRET, + public_key=self.PLAID_PUBLIC_KEY, + environment=self.PLAID_ENV, + api_version='2019-05-29') + + public_key = self.credentials.get('public_key') + if not self.credentials.get('auth_token') and public_key: + self.credentials['auth_token'] = self.get_auth_token(public_key) + + def get_auth_token(self, public_token): + try: + exchange_response = self.client.Item.public_token.exchange( + public_token) + except plaid.errors.PlaidError as e: + return format_error(e) + access_token = exchange_response['access_token'] + return access_token + + def get_transactions( + self, + start_date=None, + end_date=None, + auth_token=None): + if not auth_token: + auth_token = self.credentials.get('auth_token') + if not auth_token: + raise Exception("Missing Auth Token") + if not start_date: + start_date = '{:%Y-%m-%d}'.format( + datetime.datetime.now() + datetime.timedelta(-30)) + if not end_date: + end_date = '{:%Y-%m-%d}'.format(datetime.datetime.now()) + try: + transactions_resp = self.client.Transactions.get( + auth_token, start_date, end_date) + except plaid.errors.PlaidError as e: + return format_error(e) + return transactions_resp diff --git a/connection/migrations/0002_auto_20200106_2335.py b/connection/migrations/0002_auto_20200106_2335.py new file mode 100644 index 0000000..89acba4 --- /dev/null +++ b/connection/migrations/0002_auto_20200106_2335.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.6 on 2020-01-06 23:35 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('connection', '0001_initial'), + ] + + operations = [ + migrations.RenameField( + model_name='connection', + old_name='connection_path', + new_name='type', + ), + ] diff --git a/connection/migrations/0003_auto_20200107_0015.py b/connection/migrations/0003_auto_20200107_0015.py new file mode 100644 index 0000000..42feedd --- /dev/null +++ b/connection/migrations/0003_auto_20200107_0015.py @@ -0,0 +1,26 @@ +# Generated by Django 2.2.6 on 2020-01-07 00:15 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('connection', '0002_auto_20200106_2335'), + ] + + operations = [ + migrations.CreateModel( + name='ConnectionType', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=255)), + ('filename', models.CharField(max_length=255, unique=True)), + ], + ), + migrations.RenameField( + model_name='connection', + old_name='type', + new_name='type_old', + ), + ] diff --git a/connection/migrations/0004_connection_type.py b/connection/migrations/0004_connection_type.py new file mode 100644 index 0000000..959b40b --- /dev/null +++ b/connection/migrations/0004_connection_type.py @@ -0,0 +1,19 @@ +# Generated by Django 2.2.6 on 2020-01-07 00:17 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('connection', '0003_auto_20200107_0015'), + ] + + operations = [ + migrations.AddField( + model_name='connection', + name='type', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='connection.ConnectionType'), + ), + ] diff --git a/connection/migrations/0005_remove_connection_type_old.py b/connection/migrations/0005_remove_connection_type_old.py new file mode 100644 index 0000000..7485831 --- /dev/null +++ b/connection/migrations/0005_remove_connection_type_old.py @@ -0,0 +1,17 @@ +# Generated by Django 2.2.6 on 2020-01-07 00:20 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('connection', '0004_connection_type'), + ] + + operations = [ + migrations.RemoveField( + model_name='connection', + name='type_old', + ), + ] diff --git a/connection/serializers.py b/connection/serializers.py new file mode 100755 index 0000000..d8115c8 --- /dev/null +++ b/connection/serializers.py @@ -0,0 +1,12 @@ +from .models import Connection +from rest_framework import serializers + + +class ConnectionSerializer(serializers.HyperlinkedModelSerializer): + class Meta: + model = Connection + fields = ['url', 'name', 'type', 'credentials'] + extra_kwargs = { + 'type': {'write_only': True}, + 'credentials': {'write_only': True} + } diff --git a/connection/urls.py b/connection/urls.py new file mode 100755 index 0000000..37f0a59 --- /dev/null +++ b/connection/urls.py @@ -0,0 +1,6 @@ +from rest_framework import routers +from .views import ConnectionViewSet + +ROUTER = routers.SimpleRouter() +ROUTER.register(r'', ConnectionViewSet) +urlpatterns = ROUTER.urls diff --git a/qrtr_account/migrations/0005_rule_slice.py b/qrtr_account/migrations/0005_rule_slice.py new file mode 100644 index 0000000..e3aed96 --- /dev/null +++ b/qrtr_account/migrations/0005_rule_slice.py @@ -0,0 +1,32 @@ +# Generated by Django 2.2.6 on 2020-02-25 01:43 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('contenttypes', '0002_remove_content_type_name'), + ('qrtr_account', '0004_auto_20191126_0231'), + ] + + operations = [ + migrations.CreateModel( + name='Rule', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ], + ), + migrations.CreateModel( + name='Slice', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=250)), + ('icon', models.CharField(max_length=250)), + ('budget', models.DecimalField(decimal_places=3, max_digits=100)), + ('object_id', models.PositiveIntegerField()), + ('content_type', models.ForeignKey(limit_choices_to=models.Q(models.Q(('app_label', 'qrtr_account'), ('model', 'bank')), models.Q(('app_label', 'qrtr_account'), ('model', 'slice')), _connector='OR'), on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType')), + ], + ), + ] diff --git a/qrtr_account/migrations/0006_auto_20200225_0155.py b/qrtr_account/migrations/0006_auto_20200225_0155.py new file mode 100644 index 0000000..70b2521 --- /dev/null +++ b/qrtr_account/migrations/0006_auto_20200225_0155.py @@ -0,0 +1,23 @@ +# Generated by Django 2.2.6 on 2020-02-25 01:55 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('qrtr_account', '0005_rule_slice'), + ] + + operations = [ + migrations.RenameField( + model_name='slice', + old_name='object_id', + new_name='parent_id', + ), + migrations.RenameField( + model_name='slice', + old_name='content_type', + new_name='parent_type', + ), + ] diff --git a/qrtr_account/migrations/0007_auto_20200225_0239.py b/qrtr_account/migrations/0007_auto_20200225_0239.py new file mode 100644 index 0000000..da9e35b --- /dev/null +++ b/qrtr_account/migrations/0007_auto_20200225_0239.py @@ -0,0 +1,56 @@ +# Generated by Django 2.2.6 on 2020-02-25 02:39 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('qrtr_account', '0006_auto_20200225_0155'), + ] + + operations = [ + migrations.CreateModel( + name='Schedule', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=255)), + ], + ), + migrations.AddField( + model_name='rule', + name='amount', + field=models.DecimalField(decimal_places=3, default=0, max_digits=100), + preserve_default=False, + ), + migrations.AddField( + model_name='rule', + name='amount_type', + field=models.CharField(choices=[('quantity', 'Quantity'), ('round', 'Round'), ('percent', 'Percent')], default='quantity', max_length=20), + ), + migrations.AddField( + model_name='rule', + name='destination', + field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, related_name='rule_destination_set', to='qrtr_account.Slice'), + preserve_default=False, + ), + migrations.AddField( + model_name='rule', + name='kind', + field=models.CharField(choices=[('refill', 'Refill'), ('increase', 'Increase'), ('goal', 'Goal')], default='increase', max_length=255), + preserve_default=False, + ), + migrations.AddField( + model_name='rule', + name='source', + field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, related_name='rule_source_set', to='qrtr_account.Slice'), + preserve_default=False, + ), + migrations.AddField( + model_name='rule', + name='when_to_run', + field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to='qrtr_account.Schedule'), + preserve_default=False, + ), + ]