{ "metadata": { "name": "expiring_stripe_cc" }, "nbformat": 3, "nbformat_minor": 0, "worksheets": [ { "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# working out logic around expiring credit cards" ] }, { "cell_type": "code", "collapsed": false, "input": [ "from regluit.payment.stripelib import StripeClient\n", "from regluit.payment.models import Account\n", "\n", "from django.db.models import Q, F\n" ], "language": "python", "metadata": {}, "outputs": [], "prompt_number": 1 }, { "cell_type": "code", "collapsed": false, "input": [ "# use the localdatetime?\n", "\n", "from regluit.utils import localdatetime\n", "localdatetime.date_today()" ], "language": "python", "metadata": {}, "outputs": [ { "output_type": "pyout", "prompt_number": 2, "text": [ "datetime.date(2013, 8, 5)" ] } ], "prompt_number": 2 }, { "cell_type": "heading", "level": 2, "metadata": {}, "source": [ "expiring, expired, soon to expire cards" ] }, { "cell_type": "code", "collapsed": false, "input": [ "from regluit.payment.models import Account\n", "from django.db.models import Q\n", "import datetime\n", "from dateutil.relativedelta import relativedelta\n", "\n", "\n", "# set the month/year for comparison\n", "\n", "# # http://stackoverflow.com/a/15155212/7782\n", "today = datetime.date.today()\n", "year = today.year\n", "month = today.month\n", "\n", "date_before_month = today + relativedelta(months=-1)\n", "year_last_month = date_before_month.year\n", "month_last_month = date_before_month.month\n", "\n", "\n", "#year = 2013\n", "#month = 2\n", "\n", "# look only at active accounts\n", "\n", "active_accounts = Account.objects.filter(Q(date_deactivated__isnull=True))\n", "\n", "# calculate expired cards\n", "accounts_expired = active_accounts.filter((Q(card_exp_year__lt=year) | Q(card_exp_year=year, card_exp_month__lt = month)))\n", "\n", "# expiring on a given month\n", "\n", "accounts_expiring = active_accounts.filter(card_exp_year=year, card_exp_month = month)\n", "\n", "# yet to expire\n", "\n", "accounts_expiring_later = active_accounts.filter((Q(card_exp_year__gt=year) | Q(card_exp_year=year, card_exp_month__gt = month)))\n", "\n", "print \"number of active accounts\", active_accounts.count()\n", "print \"expired: {0} expiring: {1} expire later: {2}\".format(accounts_expired.count(), accounts_expiring.count(), accounts_expiring_later.count())\n", "\n", "# expiring soon\n", "print \"expiring soon\"\n", "print [(account.user, account.card_exp_month, account.card_exp_year) for account in accounts_expiring]\n", "\n", "# expired\n", "print \"expired\"\n", "print [(account.user, account.card_exp_month, account.card_exp_year) for account in accounts_expired]" ], "language": "python", "metadata": {}, "outputs": [ { "output_type": "stream", "stream": "stdout", "text": [ "number of active accounts 190\n", "expired: 17 expiring: 5 expire later: 168\n", "expiring soon\n", "[(, 8L, 2013L), (, 8L, 2013L), (, 8L, 2013L), (, 8L, 2013L), (, 8L, 2013L)]\n", "expired\n", "[(, 7L, 2013L), (, 2L, 2013L), (, 7L, 2013L), (, 7L, 2013L), (, 6L, 2013L), (, 6L, 2013L), (, 7L, 2013L), (, 1L, 2013L), (, 6L, 2013L), (, 2L, 2013L), (, 2L, 2013L), (, 4L, 2013L), (, 7L, 2013L), (, 7L, 2013L), (, 4L, 2013L), (, 7L, 2013L), (, 6L, 2013L)]\n" ] } ], "prompt_number": 3 }, { "cell_type": "code", "collapsed": false, "input": [ "# looking at filtering Accounts that might expire\n", "\n", "# if the the month of the expiration date == next month or earlier \n", "# accounts with expiration of this month and last month -- and to be more expansive filter: expiration with this month or before\n", "# also Account.objects.filter(Q(date_deactivated__isnull=True))\n", "# \n", "# accounts_expired = active_accounts.filter((Q(card_exp_year__lt=year) | Q(card_exp_year=year, card_exp_month__lt = month)))\n", "\n", "accounts_to_consider_expansive = Account.objects.filter(Q(date_deactivated__isnull=True)).filter((Q(card_exp_year__lt=year) | Q(card_exp_year=year, card_exp_month__lte = month)))\n", "\n", "# this month or last last monthj\n", "accounts_to_consider_narrow = Account.objects.filter(Q(date_deactivated__isnull=True)).filter(Q(card_exp_year=year, card_exp_month = month) | Q(card_exp_year=year_last_month, card_exp_month = month_last_month))\n", "\n", "for account in accounts_to_consider_narrow:\n", " print (account.user, account.card_exp_month, account.card_exp_year) " ], "language": "python", "metadata": {}, "outputs": [ { "output_type": "stream", "stream": "stdout", "text": [ "(, 7L, 2013L)\n", "(, 7L, 2013L)\n", "(, 7L, 2013L)\n", "(, 8L, 2013L)\n", "(, 8L, 2013L)\n", "(, 7L, 2013L)\n", "(, 7L, 2013L)\n", "(, 7L, 2013L)\n", "(, 7L, 2013L)\n", "(, 8L, 2013L)\n", "(, 8L, 2013L)\n", "(, 8L, 2013L)\n" ] } ], "prompt_number": 4 }, { "cell_type": "code", "collapsed": false, "input": [ "from regluit.utils.localdatetime import date_today\n", "\n", "today = date_today()\n", "year = today.year\n", "month = today.month\n", "accounts_to_calc = Account.objects.filter(Q(date_deactivated__isnull=True)).filter((Q(card_exp_year__lt=year) | Q(card_exp_year=year, card_exp_month__lte = month)))\n", "accounts_to_calc" ], "language": "python", "metadata": {}, "outputs": [ { "output_type": "pyout", "prompt_number": 5, "text": [ "[, , , , , , , , , , , , , , , , , , , , '...(remaining elements truncated)...']" ] } ], "prompt_number": 5 }, { "cell_type": "code", "collapsed": false, "input": [ "from regluit.payment import tasks\n", "k = tasks.update_account_status.apply(args=[False])" ], "language": "python", "metadata": {}, "outputs": [], "prompt_number": 6 }, { "cell_type": "code", "collapsed": false, "input": [ "k.get()" ], "language": "python", "metadata": {}, "outputs": [ { "output_type": "pyout", "prompt_number": 7, "text": [ "[]" ] } ], "prompt_number": 7 }, { "cell_type": "code", "collapsed": false, "input": [ "# list any active transactions tied to users w/ expiring and expired CC?\n", "\n", "print [(account.user, account.user.email, [t.campaign for t in account.user.transaction_set.filter(status='ACTIVE')]) for account in accounts_expiring]\n", "print [(account.user, account.user.email, [t.campaign for t in account.user.transaction_set.filter(status='ACTIVE')]) for account in accounts_expired]" ], "language": "python", "metadata": {}, "outputs": [ { "output_type": "stream", "stream": "stdout", "text": [ "[(, u'alewis4722@verizon.net', []), (, u'bookish37@gmail.com', []), (, u'pbelang@uwindsor.ca', []), (, u'ed.summers@gmail.com', []), (, u'lurikfa@gmail.com', [])]\n", "[(, u'benjamin.j.keele@gmail.com', []), (, u'rclaringbole@gmail.com', []), (, u'aprcunningham@gmail.com', []), (, u'zanrosin@gmail.com', []), (, u'rlitwin@gmail.com', []), (, u'nblack726@aol.com', []), (, u'chjones@aleph0.com', []), (, u'ranti.junus@gmail.com', []), (, u'stellans@gmail.com', []), (, u'williamgeorgebrowne@gmail.com', []), (, u'stigkj@gmail.com', []), (, u'akosavic@gmail.com', []), (, u'terry.gammon@gmail.com', []), (, u'kegukeis@syr.edu', []), (, u'dan@coffeecode.net', []), (, u'hillel.arnold@gmail.com', []), (, u'astanton@booklamp.org', [])]\n" ] } ], "prompt_number": 6 }, { "cell_type": "code", "collapsed": false, "input": [ "# more to the point, what cards have expired or will expire by the time we have a hopefully \n", "# successful close for Feeding the City (campaign # 15)?\n", "\n", "from django.contrib.auth.models import User\n", "from regluit.core.models import Campaign\n", "\n", "ftc_campaign = Campaign.objects.get(id=15)\n", "\n", "# get all accounts tied to this campaign....\n", "len(ftc_campaign.supporters())\n", "\n", "ftc_expired_accounts = [User.objects.get(id=supporter_id).profile.account for supporter_id in ftc_campaign.supporters()\n", " if User.objects.get(id=supporter_id).profile.account.status == 'EXPIRED' or \n", " User.objects.get(id=supporter_id).profile.account.status == 'EXPIRING']\n", "ftc_expired_accounts\n" ], "language": "python", "metadata": {}, "outputs": [ { "output_type": "pyout", "prompt_number": 7, "text": [ "[]" ] } ], "prompt_number": 7 }, { "cell_type": "code", "collapsed": false, "input": [ "Account.objects.filter(status='EXPIRED')" ], "language": "python", "metadata": {}, "outputs": [ { "output_type": "pyout", "prompt_number": 8, "text": [ "[, , , , , , , , , , , , , , , , ]" ] } ], "prompt_number": 8 }, { "cell_type": "heading", "level": 1, "metadata": {}, "source": [ "coming up with good notices to send out " ] }, { "cell_type": "code", "collapsed": false, "input": [ "from notification.engine import send_all\n", "from notification import models as notification\n", "\n", "from django.contrib.sites.models import Site" ], "language": "python", "metadata": {}, "outputs": [], "prompt_number": 9 }, { "cell_type": "code", "collapsed": false, "input": [ "from django.contrib.auth.models import User\n", "from django.conf import settings\n", "me = User.objects.get(email = settings.EMAIL_HOST_USER )" ], "language": "python", "metadata": {}, "outputs": [], "prompt_number": 10 }, { "cell_type": "code", "collapsed": false, "input": [ "print me, settings.EMAIL_HOST" ], "language": "python", "metadata": {}, "outputs": [ { "output_type": "stream", "stream": "stdout", "text": [ "eric smtp.gmail.com\n" ] } ], "prompt_number": 11 }, { "cell_type": "code", "collapsed": false, "input": [ "notification.send_now([me], \"account_expiring\", {\n", " 'user': me, \n", " 'site':Site.objects.get_current()\n", " }, True)" ], "language": "python", "metadata": {}, "outputs": [], "prompt_number": 12 }, { "cell_type": "code", "collapsed": false, "input": [ "notification.send_now([me], \"account_expired\", {\n", " 'user': me, \n", " 'site':Site.objects.get_current()\n", " }, True)" ], "language": "python", "metadata": {}, "outputs": [], "prompt_number": 14 }, { "cell_type": "heading", "level": 1, "metadata": {}, "source": [ "accounts with problem transactions" ] }, { "cell_type": "code", "collapsed": false, "input": [ "# easy to figure out the card used for a specific problem transaction?\n", "# want to figure out problem status for a given Account\n", "\n", "from regluit.payment.models import Transaction\n", "\n", "# Account has fingerprint\n", "# transaction doesn't have fingerprint -- will have to calculate fingerprint of card associated with transaction\n", "# w/ error if we store pay_key -- any problem?\n", "\n", "Transaction.objects.filter(host='stripelib', status='Error', approved=True).count()" ], "language": "python", "metadata": {}, "outputs": [] }, { "cell_type": "markdown", "metadata": {}, "source": [ "I am hoping that we can use the API to ask for a list of charge.failed --> but I don't see a way to query charges based upon on the status of the charges -- what you have to iterate through all of the charges and filter based on status. ( maybe I should confirm this fact with people at stripe) -- ok let's do that for now.\n", "\n", "**Note: One needs to have productionstripe keys loaded in database to run following code**\n", "\n", "What script do I run to load these keys? \n", "\n", "`/Volumes/ryvault1/gluejar/stripe/set_stripe_keys.py`" ] }, { "cell_type": "code", "collapsed": false, "input": [ "from regluit.payment.stripelib import StripeClient\n", "from regluit.payment.models import Transaction\n", "\n", "import json\n", "from itertools import islice\n", "\n", "sc = StripeClient()\n", "charges = islice(sc._all_objs('Charge'), None)\n", "\n", "failed_charges = [(c.amount, c.id, c.failure_message, json.loads(c.description)['t.id']) for c in charges if c.failure_message is not None]\n", "print failed_charges\n", "\n", "# look up corresponding Transactions and flag the ones that have not been properly charged\n", "\n", "print [t.status for t in Transaction.objects.filter(id__in = [fc[3] for fc in failed_charges])]\n" ], "language": "python", "metadata": {}, "outputs": [] }, { "cell_type": "heading", "level": 2, "metadata": {}, "source": [ "Work to create an Account.account_status()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "First working out conditions for **ERROR** status" ] }, { "cell_type": "code", "collapsed": false, "input": [ "# acc_with_error = transaction # 773 -- the one with an Error that we wrote off\n", "\n", "acc_with_error = Transaction.objects.get(id=773).user.profile.account\n", "acc_with_error.user\n", "\n", "# trans is all stripe transactions of user associated w/ acc_with_error\n", "\n", "trans = Transaction.objects.filter(host='stripelib', \n", " status='Error', approved=True, user=acc_with_error.user)\n", "\n", "# comparing the transaction payment date with when account created.\n", "# why? \n", "# if account created after transaction payment date, we would want to retry the payment.\n", "\n", "\n", "acc_with_error.date_created, [t.date_payment for t in trans] \n", "\n", "print trans.filter(date_payment__gt=acc_with_error.date_created)" ], "language": "python", "metadata": {}, "outputs": [] }, { "cell_type": "markdown", "metadata": {}, "source": [ "https://github.com/Gluejar/regluit/commit/c3f922e9ba61bc412657cfa18c7d8f8d3df6eb38#L1R341 --> it's made its way into the unglue.it code, at least in the `expiring_cc` branch" ] }, { "cell_type": "code", "collapsed": false, "input": [ "# Given the specific account I would like to cut the status... need to handle expiration as well as declined charges\n", "\n", "from regluit.payment.models import Transaction\n", "from regluit.payment.models import Account\n", "from regluit.utils.localdatetime import now, date_today\n", "\n", "from itertools import islice\n", "\n", "def account_status(account):\n", "\n", "# is it deactivated?\n", "\n", " today = date_today()\n", " transactions_w_error_status_older_account = Transaction.objects.filter(host='stripelib', \n", " status='Error', approved=True, user=account.user)\n", " \n", " if account.date_deactivated is not None:\n", " return 'DEACTIVATED'\n", "\n", "# is it expired?\n", "\n", " elif account.card_exp_year < today.year or (account.card_exp_year == today.year and account.card_exp_month < today.month):\n", " return 'EXPIRED'\n", " \n", "# about to expire? do I want to distinguish from 'ACTIVE'?\n", "\n", " elif (account.card_exp_year == today.year and account.card_exp_month == today.month):\n", " return 'EXPIRING' \n", "\n", "# any transactions w/ errors after the account date?\n", "# Transaction.objects.filter(host='stripelib', status='Error', approved=True).count()\n", "\n", " elif Transaction.objects.filter(host='stripelib', \n", " status='Error', approved=True, user=account.user).filter(date_payment__gt=account.date_created):\n", " return 'ERROR'\n", " else:\n", " return 'ACTIVE'\n", " " ], "language": "python", "metadata": {}, "outputs": [] }, { "cell_type": "code", "collapsed": false, "input": [ "# test out with the account that is currently erroring out\n", "\n", "acc_with_error = Transaction.objects.get(id=773).user.profile.account\n", "print account_status(acc_with_error)\n", "print\n", "acc_with_error.status" ], "language": "python", "metadata": {}, "outputs": [] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# validity of accounts -- need to use real stripe keys if we want to look at production data" ] }, { "cell_type": "code", "collapsed": false, "input": [ "from regluit.payment.stripelib import StripeClient\n", "from django.db.models import Q\n", "\n", "sc = StripeClient()\n", "customers = list(sc._all_objs('Customer'))\n", "\n", "# 3 checks available to us: Address Line 1, zip code, CVC\n", "\n", "[(customer.id, customer.description, customer.active_card.get(\"address_line1_check\"), \n", "customer.active_card.get(\"address_zip_check\"), \n", "customer.active_card.get(\"cvc_check\")) for customer in customers if customer.active_card is not None]\n" ], "language": "python", "metadata": {}, "outputs": [] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# look at only customers that are attached to active Account" ] }, { "cell_type": "code", "collapsed": false, "input": [ "from regluit.payment.stripelib import StripeClient\n", "from regluit.payment.models import Account\n", "\n", "sc = StripeClient()\n", "customers = sc._all_objs('Customer')\n", "\n", "active_accounts = Account.objects.filter(Q(date_deactivated__isnull=True))\n", "\n", "active_customer_ids = set([account.account_id for account in active_accounts])\n" ], "language": "python", "metadata": {}, "outputs": [] }, { "cell_type": "code", "collapsed": false, "input": [ "[(customer.active_card[\"address_line1_check\"], \n", "customer.active_card[\"address_zip_check\"], \n", "customer.active_card[\"cvc_check\"]) for customer in customers if customer.id in active_customer_ids]" ], "language": "python", "metadata": {}, "outputs": [] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# handling campaign totals properly based on account statuses\n", "\n", "**Will we need to start marking accounts as expired explicitly?** \n", "\n", "add a manager method?\n" ] }, { "cell_type": "code", "collapsed": false, "input": [ "# calculate which active transactions not tied to an active account w/ unexpired CC\n" ], "language": "python", "metadata": {}, "outputs": [] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# should we delete stripe accounts associated with deactivated accounts -- I think yes\n", "\n", "How to do? \n", "\n", "* clean up Customer associated with current deactivated accounts\n", "* build logic in to delete Customer once the correspending account are deactivated and we safely have a new Account/Customer in place -- maybe a good task to put into the webhook handler" ] }, { "cell_type": "code", "collapsed": false, "input": [ "len(active_customer_ids)" ], "language": "python", "metadata": {}, "outputs": [] }, { "cell_type": "code", "collapsed": false, "input": [ "# do the users w/ deactivated accounts have active ones?" ], "language": "python", "metadata": {}, "outputs": [] } ], "metadata": {} } ] }