def _validate(self) -> None:
        if self.acct_type not in VALID_ACCT_TYPES:
            raise ValueError('What is this account type? {}\nMust be checking or credit'
                             .format(self.acct_type))
        if self.acct_type == CREDIT:
            self.balance = self.balance * (-1.0)

            if 28 > int(self.payback_date) > 0:
                self.payback_date = int(self.payback_date)

            self.credit_limit = Q_(float(self.credit_limit.replace('$', '')), 'usd')
    def process_tx(self, amount_extractable_obj: Union[AccountInterface, Transaction]) -> None:
        """
        figure out which attribute the amount is stored in
        if its a transaction object use the amount attribute
        if its an account object then use the balance attribute
        """
        if hasattr(amount_extractable_obj, 'amount'):
            amount = amount_extractable_obj.amount
        elif hasattr(amount_extractable_obj, 'balance'):
            amount = amount_extractable_obj.balance

        self.balance += amount
        if self.acct_type == CREDIT:
            bal_credit_ratio = round(abs(self.balance / self.credit_limit) * 100., 1)
            if bal_credit_ratio > 20:
                logger.info("{}\nbe careful, you're debt/limit ratio is {}%\n\
					anything over 20% may hurt your credit score."
                            .format(self, bal_credit_ratio))

        elif self.acct_type == CHECKING:
            if self.balance < Q_(0, 'usd'):
                logger.info("{} has just overdrafted.".format(self))

        # check if balance is an attribute, and update it

        if hasattr(amount_extractable_obj, "balance"):

            # don't just set to 0.0 because future functionality might pay a fraction of balance
            amount_extractable_obj.balance -= amount

            logger.debug("credit account {} was paid off".
                         format(amount_extractable_obj))
    def process_tx(self, amount_extractable_obj: Union[AccountInterface,
                                                       Transaction],
                   simulated_day: datetime) -> None:
        """
        figure out which attribute the amount is stored in
        if its a transaction object use the amount attribute
        if its an account object then use the balance attribute
        """
        if hasattr(amount_extractable_obj, 'amount'):
            amount = amount_extractable_obj.amount
        elif hasattr(amount_extractable_obj, 'balance'):
            amount = amount_extractable_obj.balance

        self.balance += amount
        if self.acct_type == CREDIT:
            bal_credit_ratio = round(
                abs(self.balance / self.credit_limit) * 100., 1)
            if bal_credit_ratio > 25:
                logger.info(
                    f"{self.name}\nbe careful, you're debt/limit ratio is {bal_credit_ratio}%\n\
                    anything over 25% may hurt your credit score.")
                issue = {
                    "ISSUE": DEBT_RATIO,
                    "DATE": simulated_day,
                    "VALUE": bal_credit_ratio
                }
                self.issues.append(issue)
            if abs(self.balance) > self.credit_limit:
                logger.info(
                    f"You've spent more than your credit limit for {self.name}"
                )
                issue = {
                    "ISSUE": OVERCREDIT,
                    "DATE": simulated_day,
                    "VALUE": self.balance
                }
                self.issues.append(issue)

        elif self.acct_type == CHECKING:
            if self.balance < Q_(0, 'usd'):
                logger.info(f"{self} has just overdrafted.")
                issue = {
                    "ISSUE": OVERDRAFT,
                    "DATE": simulated_day,
                    "VALUE": self.balance
                }
                self.issues.append(issue)

        # check if balance is an attribute, and update it

        if hasattr(amount_extractable_obj, "balance"):

            # don't just set to 0.0 because future functionality might pay a fraction of balance
            amount_extractable_obj.balance -= amount

            logger.debug("credit account {} was paid off".format(
                amount_extractable_obj))
    def _parse_attributes(self) -> None:

        self.amount = Q_(float(self.amount.replace('$', '')), 'usd')

        if self.transaction_type.lower() == 'deduction':
            self.amount = self.amount * (-1.0)
        elif self.transaction_type.lower() == 'payment':
            pass

        self.frequency = Q_(
            self.frequency.replace('d', 'day').replace('w', 'week').replace(
                ' ', '+')).to('week')

        self.sample_date = parser.parse(self.sample_date)

        try:
            self.until_date = parser.parse(self.until_date)
        except TypeError:
            self.until_date = FOREVER_RECURRING
Esempio n. 5
0
    def _parse_attributes(self) -> None:

        self.amount = Q_(float(self.amount.replace('$', '')), 'usd')

        if self.transaction_type.lower() == 'deduction':
            self.amount = self.amount * (-1.0)
        elif self.transaction_type.lower() == 'payment':
            pass

        try:

            self.frequency = Q_(
                self.frequency.replace('d',
                                       'day').replace('w', 'week').replace(
                                           ' ', '+')).to('week')

        except ValueError:
            logger.info(
                f'received frequency of {self.frequency} assuming units of days'
            )
            self.frequency = Q_(f'{self.frequency} days')

        try:
            self.sample_date = parser.parse(self.sample_date)
        except ValueError as e:
            logger.error(f'{self.sample_date} is not a valid date')
            raise e

        try:
            self.until_date = parser.parse(self.until_date)
        except (TypeError, ValueError):
            self.until_date = FOREVER_RECURRING
    def __init__(self,
                 name: str,
                 bal: str,
                 acct_type: str,
                 payback_date: Optional[float64] = None,
                 payback_src: Optional[Union[str, float]]=None,
                 credit_limit: Optional[Union[str, float]]=None) -> None:

        self.name = name
        self.balance = Q_(float(bal.replace('$', '')), 'usd')
        self.acct_type = acct_type.upper()
        self.payback_date = payback_date
        self.payback_src = payback_src

        self.credit_limit = credit_limit
        self._validate()
class Account(AccountInterface):

    def __init__(self,
                 name: str,
                 bal: str,
                 acct_type: str,
                 payback_date: Optional[float64] = None,
                 payback_src: Optional[Union[str, float]]=None,
                 credit_limit: Optional[Union[str, float]]=None) -> None:

        self.name = name
        self.balance = Q_(float(bal.replace('$', '')), 'usd')
        self.acct_type = acct_type.upper()
        self.payback_date = payback_date
        self.payback_src = payback_src

        self.credit_limit = credit_limit
        self._validate()

    def __repr__(self) -> str:
        return "{}: {}".format(self.name, self.balance)

    def _validate(self) -> None:
        if self.acct_type not in VALID_ACCT_TYPES:
            raise ValueError('What is this account type? {}\nMust be checking or credit'
                             .format(self.acct_type))
        if self.acct_type == CREDIT:
            self.balance = self.balance * (-1.0)

            if 28 > int(self.payback_date) > 0:
                self.payback_date = int(self.payback_date)

            self.credit_limit = Q_(float(self.credit_limit.replace('$', '')), 'usd')

    def process_tx(self, amount_extractable_obj: Union[AccountInterface, Transaction]) -> None:
        """
        figure out which attribute the amount is stored in
        if its a transaction object use the amount attribute
        if its an account object then use the balance attribute
        """
        if hasattr(amount_extractable_obj, 'amount'):
            amount = amount_extractable_obj.amount
        elif hasattr(amount_extractable_obj, 'balance'):
            amount = amount_extractable_obj.balance

        self.balance += amount
        if self.acct_type == CREDIT:
            bal_credit_ratio = round(abs(self.balance / self.credit_limit) * 100., 1)
            if bal_credit_ratio > 20:
                logger.info("{}\nbe careful, you're debt/limit ratio is {}%\n\
					anything over 20% may hurt your credit score."
                            .format(self, bal_credit_ratio))

        elif self.acct_type == CHECKING:
            if self.balance < Q_(0, 'usd'):
                logger.info("{} has just overdrafted.".format(self))

        # check if balance is an attribute, and update it

        if hasattr(amount_extractable_obj, "balance"):

            # don't just set to 0.0 because future functionality might pay a fraction of balance
            amount_extractable_obj.balance -= amount

            logger.debug("credit account {} was paid off".
                         format(amount_extractable_obj))

    def payoff_credit_acct(self, account_object: AccountInterface) -> None:
        """
        modify the account_object by paying off its balance
        """

        if account_object.acct_type == CREDIT:

            if self.acct_type == CHECKING:
                self.process_tx(account_object)

            else:
                logger.warning("Need to payoff_credt_acct with a checking account.\
					Skipping this operation.")
                return

        else:
            logger.warning("Cannot payoff_credit_acct with {} type acct.\n\
				skipping this operation."
                           .format(account_object.acct_type))
            return
Esempio n. 8
0
class Transaction(object):
    def __init__(self, f: str, a: str, t: str, d: str, sd: str, sc: str,
                 u: Union[str, float]) -> None:
        # change to have the units recognized by pint and the + as a mathematical operation
        self.frequency = f
        self.amount = a
        self.transaction_type = t
        self.description = d
        self.sample_date = sd
        self.source = sc.strip().upper()
        self.until_date = u
        self._parse_attributes()

    def __repr__(self):
        return self.description

    def _parse_attributes(self) -> None:

        self.amount = Q_(float(self.amount.replace('$', '')), 'usd')

        if self.transaction_type.lower() == 'deduction':
            self.amount = self.amount * (-1.0)
        elif self.transaction_type.lower() == 'payment':
            pass

        try:

            self.frequency = Q_(
                self.frequency.replace('d',
                                       'day').replace('w', 'week').replace(
                                           ' ', '+')).to('week')

        except ValueError:
            logger.info(
                f'received frequency of {self.frequency} assuming units of days'
            )
            self.frequency = Q_(f'{self.frequency} days')

        try:
            self.sample_date = parser.parse(self.sample_date)
        except ValueError as e:
            logger.error(f'{self.sample_date} is not a valid date')
            raise e

        try:
            self.until_date = parser.parse(self.until_date)
        except (TypeError, ValueError):
            self.until_date = FOREVER_RECURRING

    def should_payment_occur_today(self,
                                   datetime_object: datetime,
                                   check_cycles: int = 1) -> bool:
        """
        Given a datetime object determine if this transaction
        would have occurred on a given date
        function does some math based on the sample date provided
        and the frequency indicated
        TODO: this is slow and inefficient
        TODO: intelligent update check_cycle based on sample date and datetime_object
        :param check_cycles: number of occurrences (in weeks) to check in either direction from sample date
        """

        cycles = range(check_cycles + 1)
        dtc = datetime_object.day
        mtc = datetime_object.month
        ytc = datetime_object.year

        time_delta = datetime_object - self.sample_date
        """
        if there is more time between the sample date and current simulated day (datetime_obj)
        than would be reachable within the check_cycles of frequency
        then update the sample_date to be further in the future
        """
        # frequency is a quantity with units so update weeks to days before comparing integers
        range_of_time_reachable = (self.frequency * check_cycles).to('days')
        # import ipdb
        # ipdb.set_trace()
        while abs(time_delta.days) >= range_of_time_reachable.magnitude:
            # if time_delta days is positive, then the sample date is too far in the past, step forward
            if time_delta.days > 0:
                self.sample_date += range_of_time_reachable

            # if time_delta days is negative, then the sample date is too far in the future, step backwards
            elif time_delta.days < 0:
                self.sample_date -= range_of_time_reachable
            # update time_delta with new sample date
            time_delta = datetime_object - self.sample_date

        # TODO: this for loop is likely not needed anymore, avoiding refactoring until unit tests are set up
        for occ in cycles:

            forward = self.sample_date + self.frequency * occ
            backward = self.sample_date - self.frequency * occ

            if ((backward.day == dtc) and (backward.month == mtc)
                    and (backward.year == ytc)):
                logger.debug("Found it on {}th occurence".format(occ))
                # update the sample date such that it always stays close to the simulated day
                self.sample_date = backward
                return True
            elif (forward.day == dtc) and (forward.month
                                           == mtc) and (forward.year == ytc):
                logger.debug("Found it on {}th occurence".format(occ))
                # update the sample date such that it always stays close to the simulated day
                self.sample_date = forward
                return True
            else:
                continue

        return False
Esempio n. 9
0
def lambda_handler(event, context):
    '''
    Lambda that triggers on s3 put into the userdata directory
    of backend data
    '''

    now = datetime.datetime.now()

    placement_key = event['Records'][0]['s3']['object']['key']
    userid = placement_key.split('/')[1]
    accounts, transactions = get_data(userid)
    #accts_dict = {}
    accts_list = []
    for account in accounts:
        acctname = account['AccountName']
        balance = account['Balance']
        account_type = account['Type']
        paydate = account['PayoffDay']
        paysrc = account['PayoffSource']
        creditlim = account['CreditLimit']

        accts_list.append(
            Account(name=acctname,
                    bal=balance,
                    acct_type=account_type,
                    payback_date=paydate,
                    payback_src=paysrc,
                    credit_limit=creditlim))

    accts_dict = {acct.name: acct for acct in accts_list}

    txs_list = []
    for tx in transactions:
        desc = tx['Description']
        freq = tx['Occurrence']
        amt = tx['Amount']
        tx_type = tx['Type']
        samp_d = tx['Sample_Date']
        src = tx['Source']
        until = tx['Until']

        tx_obj = Transaction(f=freq,
                             a=amt,
                             t=tx_type,
                             d=desc,
                             sd=samp_d,
                             sc=src,
                             u=until)
        txs_list.append(tx_obj)

    days = range(DAYS_TO_PROJECT)
    aggregate_df = []
    # This the actual simulation running through days
    for day in days:
        txs_occurring_today = []
        # use units to add one day per iteration
        simulated_day = now + Q_(day, 'day')
        # loop through all the available transactions
        for tx in txs_list:
            # determine if a transaction should occur on simulated day
            if tx.should_payment_occur_today(simulated_day):
                logger.debug(
                    f"Paying {tx.description} Today\nSample Date: {tx.sample_date}\nSimulated Day:{simulated_day}\n"
                )

                logger.debug(f"From Acct: {tx.source}")

                # attempt to grab the account that the transactions is coming from or into or both.
                try:
                    acct_obj = accts_dict[tx.source]
                except KeyError:
                    raise KeyError(
                        f"Transaction Source {tx.source} is not an account in {accts_dict.keys()}"
                    )
                # take the money from the account it is coming from
                acct_obj.process_tx(tx, simulated_day)
                # track the transactions that happened today
                txs_occurring_today.append(
                    (tx.description, tx.source, tx.amount.magnitude))

        # update transaction list to only include ones that are forever recurring
        """
        Update transaction list
        This is important bc of the "until_date" attribute.
        If the current simulated day is past the until date,
        then the transaction should no longer be processed in the future.
        TODO: move this logic into transaction object itself.
        A method that takes a datetime object and compares it to
        its until_date attribute and returns a boolean of
        [tx for tx in txs_list if tx.is_active(simulated_day)]
        """
        txs_list = [
            tx for tx in txs_list if tx.until_date == FOREVER_RECURRING
            or tx.until_date > simulated_day
        ]

        # check all the accounts and see if its a payoff date
        for acct_obj in accts_dict.values():
            # only credit accounts can get paid off
            if acct_obj.acct_type == CREDIT and simulated_day.day == acct_obj.payback_date:
                try:
                    payback_src_acct = accts_dict[acct_obj.payback_src]
                except KeyError:
                    raise KeyError(
                        "Credit Acct Payoff Source {} is not an account in {}".
                        format(acct_obj.payback_src, accts_dict.keys()))

                # this function modifies both accounts in place
                payback_src_acct.payoff_credit_acct(acct_obj, simulated_day)

        logger.debug(f"Day: {simulated_day}".format(simulated_day))
        logger.debug(f"Amount: {accts_dict.values()}")

        acct_data = []
        acct_names = []
        for acc in accts_dict.values():
            acct_data.append(acc.balance.magnitude)
            acct_names.append(acc.name)

        # create a row to concatenate to dataframe
        datalist = [simulated_day, txs_occurring_today]
        col_list = ['date', 'transactions']
        # extend modifies list in place
        datalist.extend(acct_data)
        col_list.extend(acct_names)
        # newrow = pd.DataFrame([datalist], columns=col_list)
        # use columns as keys and data as values
        newrow = OrderedDict({k: v for k, v in zip(col_list, datalist)})

        aggregate_df.append(newrow)

    for curract in accts_dict.keys():

        acct_specific_txs = [
            specify_txs(tx, curract)
            for tx in get_column_data(aggregate_df, 'transactions')
        ]

        new_col_name = f'{curract}transactions'
        # aggregate_df[new_col_name] = acct_specific_txs
        aggregate_df = place_column_data(aggregate_df, new_col_name,
                                         acct_specific_txs)

    place_forecasted_data(userid, aggregate_df)
    # take issues from accounts and write to s3
    place_money_warning_data(userid, list(accts_dict.values()))
Esempio n. 10
0
        u=until)
    txs_list.append(tx)

DAYS_TO_PROJECT = args.forecast
now = datetime.datetime.now()
#now = datetime.datetime(month=3, day=25, year=2019)
tings2plot = []
days = range(DAYS_TO_PROJECT)

acct_lines = {}
aggregate_df = pd.DataFrame()
# This the actual simulation running through days
for day in days:
    txs_occurring_today = []
    # use units to add one day per iteration
    simulated_day = now + Q_(day, 'day')
    # loop through all the available transactions
    for tx in txs_list:
        # determine if a transaction should occur on simulated day
        if tx.should_payment_occur_today(simulated_day):
            logger.debug("Paying {} Today\nSample Date: {}\nSimulated Day:{}\n"
                         .format(tx.description, tx.sample_date, simulated_day))

            logger.debug("From Acct: {}".format(tx.source))

            # attempt to grab the account that the transactions is coming from or into or both.
            try:
                acct_obj = accts_dict[tx.source]
            except KeyError:
                raise KeyError("Transaction Source {} is not an account in {}"
                               .format(tx.source, accts_dict.keys()))