def test_transaction_model(self):
        """A test to ensure that transactions are saved correctly to the database."""

        with self.app.app_context():
            transaction_dict = get_transaction_dict({'gift_id': 1})

            transaction_model = from_json(TransactionSchema(),
                                          transaction_dict,
                                          create=True)
            database.session.add(transaction_model.data)
            database.session.commit()

            transaction_query = TransactionModel.query.filter_by(
                gift_id=transaction_dict['gift_id']).one()
            transaction_session = database.session.query( TransactionModel )\
                .filter_by( gift_id=transaction_dict[ 'gift_id' ] )\
                .one()

            kwargs = {
                'self': self,
                'model_dict': transaction_dict,
                'model': TransactionModel,
                'model_data': transaction_model.data,
                'model_query': transaction_query,
                'model_session': transaction_session
            }
            ensure_query_session_aligned(kwargs)
示例#2
0
def generate_a_transaction(row, transaction_ids, transaction_params):
    """

    :param row: A row from the CSV.
    :param transaction_ids: The transaction IDs.
    :param transaction_params: agent_id, transaction type, notes, and the gift ID.
    :return:
    """

    if 'gift_id' not in transaction_params or not transaction_params['gift_id']:
        gift_id = transaction_ids.get(row['reference_txn_id'])
    else:
        gift_id = transaction_params['gift_id']

    if 'notes' not in transaction_params:
        notes = 'from csv upload'
    else:
        notes = transaction_params['notes']

    transaction_payload = {
        'gift_id': gift_id,
        'date_in_utc': process_date_time(row['date'], row['time']),
        'enacted_by_agent_id': transaction_params['agent_id'],
        'type': transaction_params['type'],
        'status': process_transaction_status(row['status']),
        'reference_number': row['transaction_id'],
        'gross_gift_amount': process_decimal_amount(row['gross']),
        'fee': process_decimal_amount(row['fee']),
        'notes': notes
    }
    transaction_schema = from_json(TransactionSchema(), transaction_payload)
    transaction_model = transaction_schema.data
    return transaction_model
示例#3
0
    def test_get_transactions_get( self ):
        """Transaction endpoint with one ID retrieves the transaction ( methods = [ GET ] )."""

        with self.app.app_context():
            url = '/donation/transactions/{}'
            # Ensure a GET to with no database entries returns nothing.
            response = self.test_client.get( url.format( 2 ), headers=self.headers )
            data_returned = json.loads( response.data.decode( 'utf-8' ) )
            self.assertEqual( len( data_returned ), 0 )

            # Create a set of transactions.
            total_transactions = 5
            transaction_models = create_model_list(
                TransactionSchema(),
                get_transaction_dict( { 'gift_id': 1 } ),
                total_transactions
            )

            database.session.bulk_save_objects( transaction_models )
            database.session.commit()

            # Ensure a GET with one ID returns the correct transaction.
            response = self.test_client.get( url.format( 2 ), headers=self.headers )
            data_returned = json.loads( response.data.decode( 'utf-8' ) )
            self.assertEqual( data_returned[ 'id' ], 2 )
示例#4
0
def build_transaction(transaction_dict):
    """Given a transaction dictionary method builds the model.

    :param transaction_dict: The transaction dictionary.
    :return: transaction.model.data: The Transaction model built from the given dictionary.
    """

    with app.app_context():
        transaction_model = from_json(TransactionSchema(),
                                      transaction_dict,
                                      create=True)
        database.session.add(transaction_model.data)
        database.session.commit()
        print()
        print(' Build TransactionModel')
        print('    transaction_model.id         : {}'.format(
            transaction_model.data.id))
        print('    transaction_model.date_in_utc: {}'.format(
            transaction_model.data.date_in_utc))
        print('    transaction_model.type       : {}'.format(
            transaction_model.data.type))
        print('    transaction_model.status     : {}'.format(
            transaction_model.data.status))
        print()
        return transaction_model.data
def record_bounced_check( payload ):
    """A function for recording the details of a bounced check.

    The payload required for the bounced check has the following keys:

    payload = {
        "gift_id": 1,
        "user_id": 1234,
        "reference_number": "201",
        "amount": "10.00",
        "transaction_notes": "Some transaction notes."
    }

    The reference number will be most likely the check number.

    :param dict payload: A dictionary that provides information to make the reallocation.
    :return:
    """

    gift_searchable_id = payload[ 'gift_searchable_id' ]
    try:
        gift_model = GiftModel.query.filter_by( searchable_id=gift_searchable_id ).one()

        # The first transaction created has the check amount.
        # The last has the current balance.
        gross_gift_amount = \
            gift_model.transactions[ 0 ].gross_gift_amount - gift_model.transactions[ -1 ].gross_gift_amount
    except:
        raise AdminFindGiftPathError()

    # Make sure the gift exists and that it has method_used='Check'.
    # Do not modify the database if method_used is not cCheck. Handle with app.errorhandler().
    method_used = MethodUsedModel.get_method_used( 'name', 'Check' )
    if gift_model.method_used_id != method_used.id:
        raise ModelGiftImproperFieldError

    enacted_by_agent = AgentModel.get_agent( 'Staff Member', 'user_id', payload[ 'user_id' ] )

    try:
        # If gift exists and method_used is a check, record thet the check bounced.
        date_in_utc = datetime.datetime.utcnow().strftime( '%Y-%m-%d %H:%M:%S' )
        transaction_json = {
            'gift_id': gift_model.id,
            'date_in_utc': date_in_utc,
            'enacted_by_agent_id': enacted_by_agent.id,
            'type': 'Bounced',
            'status': 'Completed',
            'reference_number': payload[ 'reference_number' ],
            'gross_gift_amount': gross_gift_amount,
            'fee': Decimal( 0.00 ),
            'notes': payload[ 'transaction_notes' ]
        }

        transaction = from_json( TransactionSchema(), transaction_json )
        database.session.add( transaction.data )
        database.session.commit()
    except:
        database.session.rollback()
        raise AdminTransactionModelPathError( where='parent' )
示例#6
0
def create_transaction(transaction_dict):
    """Given a gift searchable ID create a transaction.

    The implementation uses transaction_dict[ 'gross_gift_amount' ] to update the current gross_gift_amount on
    the gift. If it should not be updated leave the field off the transaction_dict or set it to 0.00.

    :param transaction_dict: The transaction to create.
    :return:
    """

    sql_query = query_gift_equal_uuid('id',
                                      transaction_dict['gift_searchable_id'])
    try:
        gift_model = database.session.execute(sql_query).fetchone()

        # Get all the transactions on the gift and grab the current gross_gift_amount.
        transactions = TransactionModel.query.filter_by( gift_id=gift_model.id )\
            .order_by( TransactionModel.date_in_utc.desc() ).all()
        current_gross_gift_amount = Decimal(0.00)
        if transactions:
            current_gross_gift_amount = transactions[0].gross_gift_amount

    except SQLAlchemyORMNoResultFoundError as error:
        raise error

    if gift_model:

        reference_number = None
        transaction = TransactionModel.query \
            .filter_by( gift_id=gift_model.id ).filter_by( type='Gift' ).filter_by( status='Completed' ).one_or_none()
        if transaction:
            reference_number = transaction.reference_number

        scrub_transaction_dict(transaction_dict)

        date_in_utc = datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S')
        transaction_dict['date_in_utc'] = date_in_utc
        transaction_dict['gift_id'] = gift_model.id
        transaction_dict[ 'gross_gift_amount' ] = \
            current_gross_gift_amount + Decimal( transaction_dict[ 'gross_gift_amount' ] )
        transaction_dict['reference_number'] = reference_number

        try:
            transaction_model = from_json(TransactionSchema(),
                                          transaction_dict,
                                          create=True)
            database.session.add(transaction_model.data)
            database.session.commit()
        except MarshmallowValidationError as error:
            raise error
        except SQLAlchemyError as error:
            database.session.rollback()
            raise error

        return transaction_model.data

    raise ModelGiftNotFoundError
    def post( self ):
        """Endpoint returns query against gross gift amounts.

        Can provide transactions for gross gift amounts greater than, or greater than or less than, the specified
        amount or amounts.
        """

        transactions = get_transactions_by_amount( request.json[ 'gross_gift_amount' ] )
        result = TransactionSchema( many=True ).dump( transactions ).data
        return result, 200
示例#8
0
    def test_get_transactions_by_gift( self ):
        """Retrieves all transactions with a specified gift searchable_id ( methods = [ GET ] )."""

        with self.app.app_context():
            # The parameter is for the searchable_id.
            url = '/donation/gifts/{}/transactions'

            # Create 3 gifts to attach transactions to.
            # Create each gift with a reproducible UUID.
            total_gifts = 5
            searchable_ids = get_gift_searchable_ids()
            gift_models = [ ]
            for i in range( 0, total_gifts ):  # pylint: disable=W0612
                gift_json = get_gift_dict()
                del gift_json[ 'id' ]
                gift_json[ 'searchable_id' ] = searchable_ids[ i ]
                gift_model = GiftSchema().load( gift_json ).data
                gift_models.append( gift_model )
            database.session.bulk_save_objects( gift_models )
            database.session.commit()

            # Create 3 transactions attached to the same gift ID = 1, one transaction to 4 and none on 5.
            total_transactions = 5
            transaction_models = []
            for i in range( 0, total_transactions ):  # pylint: disable=W0612
                transaction_json = get_transaction_dict()
                del transaction_json[ 'id' ]
                if i <= 2:
                    transaction_json[ 'gift_id' ] = 1
                    transaction_json[ 'gift_searchable_id' ] = uuid.UUID( searchable_ids[ 0 ] ).hex
                elif i == 3:
                    transaction_json[ 'gift_id' ] = i
                    transaction_json[ 'gift_searchable_id' ] = uuid.UUID( searchable_ids[ i ] ).hex
                else:
                    transaction_json[ 'gift_id' ] = i
                    transaction_json[ 'gift_searchable_id' ] = uuid.UUID( searchable_ids[ i ] ).hex

                transaction_model = TransactionSchema().load( transaction_json ).data
                transaction_models.append( transaction_model )

            database.session.bulk_save_objects( transaction_models )
            database.session.commit()

            # searchable_ids[ 0 ] is gift ID = 1 and will have 3 transactions.
            response = self.test_client.get( url.format( uuid.UUID( searchable_ids[ 0 ] ).hex ), headers=self.headers )
            self.assertEqual( len( json.loads( response.data.decode( 'utf-8' ) ) ), 3 )

            # searchable_ids[ 2 ] is gift ID = 3 and will have 1 transactions.
            response = self.test_client.get( url.format( uuid.UUID( searchable_ids[ 2 ] ).hex ), headers=self.headers )
            self.assertEqual( len( json.loads( response.data.decode( 'utf-8' ) ) ), 1 )

            # searchable_ids[ 4 ] is gift ID = 5 and will have 0 transactions.
            response = self.test_client.get( url.format( uuid.UUID( searchable_ids[ 4 ] ).hex ), headers=self.headers )
            self.assertEqual( len( json.loads( response.data.decode( 'utf-8' ) ) ), 0 )
示例#9
0
def reprocess_queued_donors(payload=None):
    """Reprocess existing queued donors."""

    if payload:
        queued_donors = QueuedDonorModel.query.filter(
            QueuedDonorModel.id.in_(payload['queued_donor_ids']))
    else:
        queued_donors = QueuedDonorModel.query.all()

    jobs = []
    for queued_donor_model in queued_donors:
        queued_donor_dict = to_json(QueuedDonorSchema(),
                                    queued_donor_model).data
        queued_donor_dict['gift_id'] = queued_donor_model.gift_id
        queued_donor_dict['queued_donor_id'] = queued_donor_model.id
        queued_donor_dict['category'] = 'queued'
        queued_donor_dict.pop('id')

        # May be multiple transactions for a gift, e.g. check with a Gift and Deposit to Bank.
        transaction_models = TransactionModel.query.filter_by(
            gift_id=queued_donor_model.gift_id).all()
        transactions = []
        for transaction_model in transaction_models:
            if transaction_model.type in ['Gift', 'Deposit to Bank']:
                transaction_dict = to_json(TransactionSchema(),
                                           transaction_model).data
                transactions.append(transaction_dict)

        # Caging expects a user dictionary that has a user something like: { user_address:{}, 'billing_address':{} }.
        # Put the queued donor dictionary in this form.
        queued_donor_dict = validate_user_payload(queued_donor_dict)

        # Once on the queue it is out of our hands, but may fail on arguments to queue().
        try:
            job = redis_queue_caging.queue(queued_donor_dict, transactions,
                                           current_app.config['ENV'])
            jobs.append((queued_donor_dict['queued_donor_id'], job.get_id()))
        except:
            jobs.append((queued_donor_dict['queued_donor_id'], 'failed'))

    response = None
    if jobs:
        response = {'reprocessed_jobs': jobs}

    return response
    def test_get_gifts_by_date_future(self):
        """Gifts endpoint which retrieves all gifts newer than date, or between 2 dates ( methods = [ POST ] )."""

        with self.app.app_context():
            url = '/donation/gifts/date'

            # Ensure that with no database entries endpoint returns nothing.
            date_in_utc_now = datetime.utcnow()
            response = self.test_client.post(
                url,
                data=json.dumps({'date':
                                 date_in_utc_now.strftime('%Y-%m-%d')}),
                content_type='application/json',
                headers=self.headers)
            data_returned = json.loads(response.data.decode('utf-8'))
            self.assertEqual(len(data_returned), 0)

            # Create some gifts to retrieve.
            total_gifts = 2
            gift_models = create_model_list(GiftSchema(), get_gift_dict(),
                                            total_gifts)

            # Create a set of transactions and attach to a specific gift.
            # Here are the time deltas: { gift 1: [ 0, -2, -4, -6 ], gift 2: [ -8, -10, -12, -14 ] }
            total_transactions = 4
            transaction_models = create_gift_transactions_date(
                TransactionSchema(), get_transaction_dict(),
                total_transactions, total_gifts)

            database.session.bulk_save_objects(gift_models)
            database.session.bulk_save_objects(transaction_models)
            database.session.commit()

            date_in_utc_now = datetime.utcnow()

            # Date in the future should bring back no results.
            date_in_utc = date_in_utc_now + timedelta(days=2)
            response = self.test_client.post(
                url,
                data=json.dumps({'date': date_in_utc.strftime('%Y-%m-%d')}),
                content_type='application/json',
                headers=self.headers)
            data_returned = json.loads(response.data.decode('utf-8'))
            self.assertEqual(len(data_returned), 0)
def build_models_sale(user, gift, transactions):
    """Given the dictionaries for the models go ahead and build them.

    :param dict user: User dictionary with necessary model fields, and may have additional fields.
    :param dict gift: Gift dictionary with necessary model fields, and may have additional fields.
    :param transactions: The list of transactions. If this is a Braintree sale, for example, there will be one
           transaction in the list. On the other hand if this is an administrative sale where the method used is
           a check or money order there will be 2 transactions.
    :return:
    """

    # We are not caging at the front of the sale, and do that at the end.
    # The user is stored in QueuedDonorModel and the gift is given a user_id = -2
    user_id = -2

    # Build the gift model dictionary, and flush to get new auto-incremented gift_id.
    try:
        # Build the gift.
        if not gift['campaign_id']:
            gift['campaign_id'] = None
        elif not get_campaign_by_id(gift['campaign_id']):
            gift['campaign_id'] = get_campaigns_by_type('is_default', 1)[0].id

        gift['user_id'] = user_id
        gift_model = from_json(GiftSchema(), gift)
        database.session.add(gift_model.data)
        database.session.flush()
        gift_id = gift_model.data.id
        user['gift_id'] = gift_id
        user['gift_searchable_id'] = gift_model.data.searchable_id
        user['campaign_id'] = gift_model.data.campaign_id

        # Build the transactions.
        for transaction in transactions:
            transaction['gift_id'] = gift_id
            transaction_model = from_json(TransactionSchema(), transaction)
            database.session.add(transaction_model.data)
            database.session.flush()
            transaction['id'] = transaction_model.data.id
        database.session.commit()
    except:
        database.session.rollback()
        raise BuildModelsGiftTransactionsPathError()
    def post( self ):
        """Endpoint builds a transaction for a specified gift searchable ID.

        :return: A transaction.
        """

        # Authenticate the admin user.
        payload = request.json
        try:
            agent_ultsys_id = get_jwt_claims()[ 'ultsys_id' ]
        except KeyError:
            raise JWTRequestError()

        if not test_hex_string( payload[ 'gift_searchable_id' ] ):
            raise TypeError

        transaction = build_transaction( payload, agent_ultsys_id )

        result = TransactionSchema().dump( transaction ).data
        return result, 200
示例#13
0
def create_gift_and_transaction():
    """A function to create a gift and an attached transaction for a given Braintree sale.

    Sometimes while testing a developer will need to have a specific gift and transaction in the database that
    would have been created for a Braintree sale. This function allows you to specify Braintree reference numbers,
    e.g. transaction sale ID and the subscription reference number, and create the gift and transaction associated
    with that sale.
    """

    with app.app_context():

        # These are the Braintree subscription ID and transaction sale ID to create a gift and transaction for.
        # Change this to suit your needs.
        subscription_id = 'kfwgzr'
        transaction_id = '83afbynd'

        date_start = datetime.utcnow().replace( hour=23, minute=59, second=59, microsecond=9999 )
        utc_dates = [ date_start - timedelta( hours=hours, minutes=30 ) for hours in range( 1, 25 ) ]

        pairs = 1
        for date_in_utc in utc_dates:
            i = 0
            while i < pairs:
                i += 1
                # Create a gift.
                gift_json = get_gift_dict( { 'recurring_subscription_id': subscription_id } )
                gift_json[ 'searchable_id' ] = uuid.uuid4()
                del gift_json[ 'id' ]

                gift_model = GiftSchema().load( gift_json ).data

                database.session.add( gift_model )
                database.session.flush()
                gift_id = gift_model.id

                # Create 4 transactions per each gift.
                transaction_json = get_transaction_dict( { 'gift_id': gift_id } )
                transaction_json[ 'type' ] = 'Gift'
                transaction_json[ 'status' ] = 'Completed'
                del transaction_json[ 'id' ]
                transaction_model = TransactionSchema().load( transaction_json ).data
                transaction_model.reference_number = transaction_id
                transaction_model.date_in_utc = date_in_utc

                transaction_model.reference_number = transaction_id
                transaction_model.notes = '{} : {}'.format( str( 1 ), str( gift_json[ 'searchable_id' ] ) )
                database.session.add( transaction_model )

        database.session.commit()
    def test_get_gifts_with_id(self):
        """Gifts-transaction endpoint with one gift ID retrieves all transactions on gift ( methods = [ GET ] )."""

        with self.app.app_context():
            url = '/donation/gifts/{}/transactions'

            # Ensure that with no database entries endpoint returns nothing.
            response = self.test_client.get(url.format(str(uuid.uuid4())),
                                            headers=self.headers)
            data_returned = json.loads(response.data.decode('utf-8'))
            self.assertEqual(len(data_returned), 0)

            # Create some gifts to retrieve.
            total_gifts = 5
            gift_models = create_model_list(GiftSchema(), get_gift_dict(),
                                            total_gifts)

            # Create a set of transactions and attach to a specific gift.
            total_transactions = 5
            transaction_gift_id = 3

            transaction_models = create_model_list(
                TransactionSchema(),
                get_transaction_dict({'gift_id': transaction_gift_id}),
                total_transactions)

            database.session.bulk_save_objects(gift_models)
            database.session.bulk_save_objects(transaction_models)
            database.session.commit()

            # Build the URL using the searchable_id of the gift.
            gift_3 = GiftModel.query.filter_by(id=3).one_or_none()
            searchable_id = gift_3.searchable_id

            # Ensure GET retrieves the specified gift and all its transactions.
            response = self.test_client.get(url.format(str(searchable_id)),
                                            headers=self.headers)
            data_returned = json.loads(response.data.decode('utf-8'))
            self.assertEqual(len(data_returned), total_transactions)
示例#15
0
def gift_update_note(searchable_id, payload):
    """Update a gift with a new transaction containing the note.

    payload = {
        "enacted_by_agent_id": "5",
        "note": "Add this to the Gift please."
    }

    :param searchable_id: A gift UUID searchable ID.
    :param payload: The note to add to the gift as a separate transaction.
    :return: True if successfully updated, and False otherwise.
    """

    # Use the sql query to return gift ID so we can then use the model to get back all transactions for that ID.
    sql_query = query_gift_equal_uuid('id', searchable_id)
    results = database.session.execute(sql_query).fetchone()
    if results:
        gift_id = results[0]

        enacted_by_agent = AgentModel.get_agent('Staff Member', 'user_id',
                                                payload['agent_ultsys_id'])

        transaction_dict = {
            'gift_id': gift_id,
            'date_in_utc': datetime.utcnow().strftime(DATE_TIME_FORMAT),
            'enacted_by_agent_id': enacted_by_agent.id,
            'type': 'Note',
            'status': 'Completed',
            'gross_gift_amount': Decimal(0.00),
            'fee': Decimal(0.00),
            'notes': payload['note']
        }
        transaction_model = from_json(TransactionSchema(), transaction_dict)
        database.session.add(transaction_model.data)
        database.session.commit()
        return True
    return False
def dispute_assess_fine( transaction_models, sale_or_dispute, history_attributes, gift_id ):
    """Build the transaction for the fine if the dispute is a chargeback.

    The history_status on a dispute does not contain information about fines. The Dispute kind ( dispute.kind )
    indicates whether the dispute is a chargeback and if it is Braintree assesses a $15 fine no matter what. Because
    it is not on the status we have to check the kind and if a chargeback has the fine already been attached to the
    gift. If it hasn't we use the datetime stamp of the Open history status to set the date_in_utc. If that is
    missing we revert to datetime.utcnow().

    :param transaction_models: Transaction models being built for disputes and sales.
    :param sale_or_dispute: The dispute
    :param history_attributes: The history status
    :param gift_id: The gift_id associated with the Braintree reference number
    :return:
    """
    if history_attributes[ 'dispute_kind' ] == BRAINTREE_CHARGEBACK_TYPE:
        transaction_for_fine = TransactionModel.query.filter_by( reference_number=sale_or_dispute.id ) \
            .filter_by( type='Fine' ) \
            .filter_by( status='Completed' ).one_or_none()
        if not transaction_for_fine:
            if 'open' in history_attributes[ 'dispute_history' ]:
                date_in_utc = history_attributes[ 'dispute_history' ][ 'open' ].strftime( MODEL_DATE_STRING_FORMAT )
            else:
                date_in_utc = datetime.utcnow()
            transaction_dict = {
                'gift_id': gift_id,
                'date_in_utc': date_in_utc,
                'enacted_by_agent_id': AGENT_ID,
                'type': 'Fine',
                'status': 'Completed',
                'reference_number': sale_or_dispute.id,
                'gross_gift_amount': Decimal( 0 ),
                'fee': BRAINTREE_CHARGEBACK_FINE_AMOUNT,
                'notes': 'Automated creation of chargeback dispute fine'
            }
            transaction_model = from_json( TransactionSchema(), transaction_dict )
            transaction_models.append( transaction_model.data )
示例#17
0
def create_database_tables():
    """A function to create the DONATE database tables, specifically the GiftModel with UUID.

    The GiftModel is build using Marshmallow schema GiftSchema, which deserializes a dictionary to the model:
    The searchable_id in the gift_json is:
        gift_json[ 'searchable_id' ] = uuid.uuid4()
    This gets passed to the GiftSchema where:
        searchable_id = fields.UUID()
    And so the validation step is passed.
    MySql does not have a UUID type though and there we have ( GiftModel ):
        searchable_id = database.Column( database.BINARY( 16 ), nullable=False, default=uuid.uuid4().bytes )
    The helper model class BinaryUUID in binary_uuid.py handles the serialization in and out.
    """

    with app.app_context():

        type = { 3: 'Gift', 2: 'Deposit to Bank', 1: 'Dispute', 0: 'Refund' }
        # Create 100 gifts.
        for i in range( 0, 100 ):
            gift_json = get_gift_dict()
            del gift_json[ 'id' ]
            gift_json[ 'searchable_id' ] = uuid.uuid4()
            gift_model = GiftSchema().load( gift_json ).data

            # Add the index as a note for debugging Gifts, since they exclude ID.
            gift_model.notes = '{} : {}'.format( str( i + 1 ), str( gift_json[ 'searchable_id' ] ) )

            database.session.add( gift_model )
            database.session.flush()
            gift_id = gift_model.id

            # Create 4 transactions per each gift.
            transactions = []
            start_datetime = datetime.utcnow()
            for j in range( 0, 4 ):  # pylint: disable=unused-variable
                test_datetime = start_datetime - timedelta( days=j )
                test_datetime = test_datetime.strftime( '%Y-%m-%d %H:%M:%S' )
                transaction_json = get_transaction_dict( {
                    'gift_id': gift_id,
                    'date_in_utc': test_datetime,
                    'gross_gift_amount': Decimal( 25 - j ),
                    'type': type[ j ],
                    'status': 'Completed'
                } )
                del transaction_json[ 'id' ]
                transaction_model = TransactionSchema().load( transaction_json ).data
                transactions.append( transaction_model )
            database.session.bulk_save_objects( transactions )

        # Create the agents.
        # agent_jsons = [
        #     { 'name': 'Donate API', 'user_id': None, 'staff_id': None, 'type': 'Automated' },
        #     { 'name': 'Braintree', 'user_id': None, 'staff_id': None, 'type': 'Organization' },
        #     { 'name': 'PayPal', 'user_id': None, 'staff_id': None, 'type': 'Organization' },
        #     { 'name': 'Credit Card Issuer', 'user_id': None, 'staf_id': None, 'type': 'Organization' },
        #     { 'name': 'Unspecified NumbersUSA Staff', 'user_id': None, 'staff_id': None, 'type': 'Staff Member' },
        #     { 'name': 'Dan Marsh', 'user_id': 1234, 'staff_id': 4321, 'type': 'Staff Member' },
        #     { 'name': 'Joshua Turcotte', 'user_id': 7041, 'staff_id': 1407, 'type': 'Staff Member' },
        #     { 'name': 'Donate API', 'user_id': None, 'staff_id': None, 'type': 'Automated' }
        # ]
        # agents = []
        # for agent_json in agent_jsons:
        #     agent_model = AgentSchema().load( agent_json ).data
        #     agents.append( agent_model )
        # database.session.bulk_save_objects( agents )

        database.session.commit()
    def test_get_gift_update_note(self):
        """Gifts endpoint to get a list of notes given a gift_searchable_id ( methods = [ GET ] )."""

        with self.app.app_context():
            # Parameter in URL is for a searchable_id_prefix.
            url = '/donation/gift/{}/notes'

            # Create 3 gifts to attach transactions to.
            # Create each gift with a reproducible UUID.
            total_gifts = 4
            searchable_ids = get_gift_searchable_ids()
            gift_models = []
            for i in range(0, total_gifts):  # pylint: disable=W0612
                gift_json = get_gift_dict()
                del gift_json['id']
                gift_json['searchable_id'] = searchable_ids[i]
                gift_model = GiftSchema().load(gift_json).data
                gift_models.append(gift_model)
            database.session.bulk_save_objects(gift_models)
            database.session.commit()

            # Create 3 transactions attached to the same gift ID = 1 and one transaction to each remaining gift.
            total_transactions = 5
            transaction_models = []
            for i in range(0, total_transactions):  # pylint: disable=W0612
                transaction_json = get_transaction_dict()
                del transaction_json['id']
                if i <= 2:
                    transaction_json['gift_id'] = 1
                    transaction_json['gift_searchable_id'] = uuid.UUID(
                        searchable_ids[0]).hex
                elif i == 3:
                    transaction_json['gift_id'] = i
                    transaction_json['gift_searchable_id'] = uuid.UUID(
                        searchable_ids[i]).hex
                    transaction_json['notes'] = ''
                else:
                    transaction_json['gift_id'] = i
                    transaction_json['gift_searchable_id'] = uuid.UUID(
                        searchable_ids[i]).hex

                transaction_model = TransactionSchema().load(
                    transaction_json).data
                transaction_models.append(transaction_model)

            database.session.bulk_save_objects(transaction_models)
            database.session.commit()

            # searchable_ids[ 0 ] is gift ID = 1 and will have 3 transactions.
            response = self.test_client.get(url.format(
                uuid.UUID(searchable_ids[1]).hex),
                                            headers=self.headers)
            self.assertEqual(len(json.loads(response.data.decode('utf-8'))), 0)

            # searchable_ids[ 1 ] is gift ID = 2 and will have 0 transactions.
            response = self.test_client.get(url.format(
                uuid.UUID(searchable_ids[1]).hex),
                                            headers=self.headers)
            self.assertEqual(len(json.loads(response.data.decode('utf-8'))), 0)

            # searchable_ids[ 2 ] is gift ID = 3 and will have 1 transaction with note = ''.
            response = self.test_client.get(url.format(
                uuid.UUID(searchable_ids[2]).hex),
                                            headers=self.headers)
            self.assertEqual(len(json.loads(response.data.decode('utf-8'))), 0)

            # searchable_ids[ 3 ] is gift ID = 4 and will have 1 transaction with a note != ''.
            response = self.test_client.get(url.format(
                uuid.UUID(searchable_ids[3]).hex),
                                            headers=self.headers)
            self.assertEqual(len(json.loads(response.data.decode('utf-8'))), 1)
    def test_get_gifts_by_date_past(self):
        """Gifts endpoint which retrieves all gifts newer than date, or between 2 dates ( methods = [ POST ] )."""

        with self.app.app_context():
            url = '/donation/gifts/date'

            # To create each gift with a new UUID call get_gift_dict() separately.
            totals = {'gifts': 2, 'transactions': 4}
            gift_models = []
            for i in range(0, totals['gifts']):  # pylint: disable=W0612
                gift_json = get_gift_dict()
                del gift_json['id']
                gift_json['searchable_id'] = uuid.uuid4()
                gift_model = GiftSchema().load(gift_json).data
                gift_models.append(gift_model)
            database.session.bulk_save_objects(gift_models)
            database.session.commit()

            # Create a set of transactions and attach to a specific gift.
            # Here are the time deltas: { gift 1: [ 0, -2, -4, -6 ], gift 2: [ -8, -10, -12, -14 ] }
            transaction_models = create_gift_transactions_date(
                TransactionSchema(), get_transaction_dict(),
                totals['transactions'], totals['gifts'])

            database.session.bulk_save_objects(transaction_models)
            database.session.commit()

            date_in_utc_now = datetime.utcnow().replace(hour=0,
                                                        minute=0,
                                                        second=0,
                                                        microsecond=0)

            # Date in the past on only gift 1.
            date_in_utc = date_in_utc_now - timedelta(days=2)
            response = self.test_client.post(
                url,
                data=json.dumps({'date': date_in_utc.strftime('%Y-%m-%d')}),
                content_type='application/json',
                headers=self.headers)
            data_returned = json.loads(response.data.decode('utf-8'))

            self.assertEqual(len(data_returned), 1)
            self.assertEqual(data_returned[0]['searchable_id'],
                             str(gift_models[0].searchable_id))

            # Date in the past which includes transactions on both gift 1 and gift 2.
            date_in_utc = date_in_utc_now - timedelta(days=10)
            response = self.test_client.post(
                url,
                data=json.dumps({'date': date_in_utc.strftime('%Y-%m-%d')}),
                content_type='application/json',
                headers=self.headers)
            data_returned = json.loads(response.data.decode('utf-8'))

            self.assertEqual(len(data_returned), 2)
            self.assertEqual(data_returned[0]['searchable_id'],
                             str(gift_models[0].searchable_id))
            self.assertEqual(data_returned[1]['searchable_id'],
                             str(gift_models[1].searchable_id))

            # Date range in the past, which includes transactions on both gift 1 and 2.
            date_in_utc_0 = date_in_utc_now - timedelta(days=6)
            date_in_utc_1 = date_in_utc_now - timedelta(days=8)
            response = self.test_client.post(
                url,
                data=json.dumps({
                    'date': [
                        date_in_utc_0.strftime('%Y-%m-%d'),
                        date_in_utc_1.strftime('%Y-%m-%d')
                    ]
                }),
                content_type='application/json',
                headers=self.headers)
            data_returned = json.loads(response.data.decode('utf-8'))

            self.assertEqual(len(data_returned), 2)
            self.assertEqual(data_returned[0]['searchable_id'],
                             str(gift_models[0].searchable_id))
            self.assertEqual(data_returned[1]['searchable_id'],
                             str(gift_models[1].searchable_id))

            # Date in the distant past, should bring back no results.
            date_in_utc = date_in_utc_now - timedelta(days=16)
            response = self.test_client.post(
                url,
                data=json.dumps({'date': date_in_utc.strftime('%Y-%m-%d')}),
                content_type='application/json',
                headers=self.headers)
            data_returned = json.loads(response.data.decode('utf-8'))
            self.assertEqual(len(data_returned), 2)

            # Date in the future, should bring back no results.
            date_in_utc = date_in_utc_now + timedelta(days=16)
            response = self.test_client.post(
                url,
                data=json.dumps({'date': date_in_utc.strftime('%Y-%m-%d')}),
                content_type='application/json',
                headers=self.headers)
            data_returned = json.loads(response.data.decode('utf-8'))
            self.assertEqual(len(data_returned), 0)
    def test_braintree_webhooks(self, mock_init_gateway_function,
                                mock_subscription_function,
                                get_ultsys_user_function):  # pylint: disable=unused-argument
        """Make sure the webhook endpoint receives a payload and makes updates as expected."""

        with self.app.app_context():
            url = '/donation/webhook/braintree/subscription'

            # Create the sourced by agent for the subscription webhook.
            agent_model = from_json(AgentSchema(),
                                    get_agent_jsons()[0],
                                    create=True)
            database.session.add(agent_model.data)
            database.session.commit()

            # Here is the first gift as check.
            gift_dict = get_gift_dict({
                'user_id':
                1,
                'method_used':
                METHOD_USED,
                'sourced_from_agent_id':
                1,
                'recurring_subscription_id':
                'recurring_subscription_id'
            })
            gift_model = from_json(GiftSchema(), gift_dict, create=True)
            database.session.add(gift_model.data)
            database.session.flush()

            # Create a transaction on the gift.
            transaction_dict = get_transaction_dict({
                'gift_id':
                gift_model.data.id,
                'enacted_by_agent_id':
                agent_model.data.id,
                'type':
                'Gift',
                'gross_gift_amount':
                Decimal('1.00')
            })
            transaction_model = from_json(TransactionSchema(),
                                          transaction_dict,
                                          create=True)
            database.session.add(transaction_model.data)

            database.session.commit()

            # Here is the fake POST from Braintree when the subscription webhook is triggered.
            response = self.test_client.post(
                url,
                data={
                    'bt_signature': 'bt_signature',
                    'bt_payload': 'subscription_charged_successfully'
                })

            self.assertEqual(response.status_code, status.HTTP_200_OK)

            method_used_id = MethodUsedModel.get_method_used(
                'name', METHOD_USED).id
            gift = GiftModel.query.filter_by(id=1).one_or_none()
            self.assertEqual(gift.method_used_id, method_used_id)
            self.assertEqual(gift.sourced_from_agent_id, SOURCED_FROM_AGENT)
            self.assertEqual(gift.recurring_subscription_id,
                             RECURRING_SUBSCRIPTION_ID)

            transaction = TransactionModel.query.filter_by(id=1).one_or_none()
            self.assertEqual(transaction.gift_id, 1)
            self.assertEqual(transaction.type, 'Gift')
            self.assertEqual(transaction.status, 'Completed')

            response = self.test_client.post(
                url,
                data={
                    'bt_signature': 'bt_signature',
                    'bt_payload': 'subscription_charged_unsuccessfully'
                })

            self.assertEqual(response.status_code, status.HTTP_200_OK)
            transaction = TransactionModel.query.filter_by(id=3).one_or_none()
            self.assertEqual(transaction.status, 'Declined')

            response = self.test_client.post(url,
                                             data={
                                                 'bt_signature':
                                                 'bt_signature',
                                                 'bt_payload':
                                                 'subscription_went_past_due'
                                             })

            self.assertEqual(response.status_code, status.HTTP_200_OK)

            transaction = TransactionModel.query.filter_by(id=4).one_or_none()
            self.assertEqual(transaction.status, 'Failed')

            response = self.test_client.post(url,
                                             data={
                                                 'bt_signature':
                                                 'bt_signature',
                                                 'bt_payload':
                                                 'subscription_expired'
                                             })

            self.assertEqual(response.status_code, status.HTTP_200_OK)

            transaction = TransactionModel.query.filter_by(id=5).one_or_none()
            self.assertEqual(transaction.status, 'Failed')
    def test_get_gifts_without_id(self):
        """Gifts-transaction endpoint with gift ID's retrieves all transactions on gifts ( methods = [ GET ] )."""

        with self.app.app_context():
            url = '/donation/gifts/transactions'

            # Create some gifts to retrieve.
            total_gifts = 5
            gift_models = create_model_list(GiftSchema(), get_gift_dict(),
                                            total_gifts)

            # Create 2 sets of transactions, each attached to a separate gift.
            total_transactions = 5
            transaction_gift_ids = [2, 4]

            transaction_models_1 = create_model_list(
                TransactionSchema(),
                get_transaction_dict({'gift_id': transaction_gift_ids[0]}),
                total_transactions)

            transaction_models_2 = create_model_list(
                TransactionSchema(),
                get_transaction_dict({'gift_id': transaction_gift_ids[1]}),
                total_transactions)

            database.session.bulk_save_objects(gift_models)
            database.session.bulk_save_objects(transaction_models_1)
            database.session.bulk_save_objects(transaction_models_2)
            database.session.commit()

            gift_2 = GiftModel.query.filter_by(id=2).one_or_none()
            searchable_id_2 = gift_2.searchable_id
            gift_4 = GiftModel.query.filter_by(id=4).one_or_none()
            searchable_id_4 = gift_4.searchable_id

            # Ensure a GET returns all transactions.
            response = self.test_client.get(url, headers=self.headers)
            data_returned = json.loads(response.data.decode('utf-8'))
            self.assertEqual(len(data_returned), 2 * total_transactions)

            # Ensure GET retrieves all transactions attached to the specified gift and the ID is correct.
            response = self.test_client.post(
                url,
                data=json.dumps({'searchable_ids': [str(searchable_id_2)]}),
                content_type='application/json',
                headers=self.headers)
            data_returned = json.loads(response.data.decode('utf-8'))

            self.assertEqual(len(data_returned), 5)
            self.assertEqual(data_returned[0]['gift_searchable_id'],
                             str(searchable_id_2))

            # Ensure GET retrieves all transactions attached to the 2 gifts.
            response = self.test_client.post(
                url,
                data=json.dumps({
                    'searchable_ids':
                    [str(searchable_id_2),
                     str(searchable_id_4)]
                }),
                content_type='application/json',
                headers=self.headers)
            data_returned = json.loads(response.data.decode('utf-8'))
            self.assertEqual(len(data_returned), 10)
示例#22
0
def void_transaction(payload):
    """A function for voiding a Braintree transaction on a gift.

    Find the transaction in the database and get the Braintree transaction number. Configure the Braintree API and
    void the transaction through Braintree.

    payload = {
        "transaction_id": 1,
        "user_id": "1234",
        "transaction_notes": "Some transaction notes."
    }

    :param dict payload: A dictionary that provides information to void the transaction.
    :return:
    """

    # Retrieve the transaction that is to be voided.
    try:
        transaction_model = TransactionModel.query.filter_by(
            id=payload['transaction_id']).one()
    except:
        raise AdminTransactionModelPathError('voided')

    try:
        braintree_id = transaction_model.reference_number
        # Need dictionary for schema, and schema.load will not include the gift_id by design.
        transaction_data = to_json(TransactionSchema(), transaction_model)
        transaction_json = transaction_data[0]
        transaction_json['gift_id'] = transaction_model.gift_id
        transaction_json['notes'] = payload['transaction_notes']
    except:
        raise AdminBuildModelsPathError()

    # Generate Braintree void, where make_braintree_void() returns a Braintree voided transaction.
    init_braintree_credentials(current_app)

    transaction_void = make_braintree_void(braintree_id)

    # Need to attach the user who is doing the void.
    enacted_by_agent = AgentModel.get_agent('Staff Member', 'user_id',
                                            payload['user_id'])
    transaction_json['enacted_by_agent_id'] = enacted_by_agent.id

    try:
        # Use BraintreeSaleSchema to populate gift and transaction dictionaries.
        braintree_schema = BraintreeSaleSchema()
        braintree_schema.context = {
            'gift': {},
            'transaction': transaction_json
        }
        braintree_sale = braintree_schema.dump(transaction_void.transaction)
        transaction_json = braintree_sale.data['transaction']

        gift_model = GiftModel.query.get(transaction_json['gift_id'])
        gross_amount = gift_model.transactions[0].gross_gift_amount
        transaction_json['gross_gift_amount'] += gross_amount

        transaction_void_model = from_json(TransactionSchema(),
                                           transaction_json)
        database.session.add(transaction_void_model.data)
        database.session.commit()
        database.session.flush()
        transaction_json['id'] = transaction_void_model.data.id
    except:
        raise AdminBuildModelsPathError()

    return transaction_json
    def get( self, searchable_id ):
        """Simple endpoint to retrieve one row from table."""

        transaction = get_transactions_by_gifts( searchable_id )
        result = TransactionSchema( many=True ).dump( transaction ).data
        return result, 200
示例#24
0
    def test_get_transactions_by_gifts( self ):
        """Retrieves all transactions or those in a list of gift searchable_id's ( methods = [ GET, POST ] )."""

        with self.app.app_context():
            # The parameter is for the searchable_id.
            url = '/donation/gifts/transactions'

            # Create 3 gifts to attach transactions to.
            # Create each gift with a reproducible UUID.
            total_gifts = 5
            searchable_ids = get_gift_searchable_ids()
            gift_models = [ ]
            for i in range( 0, total_gifts ):  # pylint: disable=W0612
                gift_json = get_gift_dict()
                del gift_json[ 'id' ]
                gift_json[ 'searchable_id' ] = searchable_ids[ i ]
                gift_model = GiftSchema().load( gift_json ).data
                gift_models.append( gift_model )
            database.session.bulk_save_objects( gift_models )
            database.session.commit()

            # Create 3 transactions attached to the same gift ID = 1, one transaction to 4 and none on 5.
            total_transactions = 5
            transaction_models = []
            for i in range( 0, total_transactions ):  # pylint: disable=W0612
                transaction_json = get_transaction_dict()
                del transaction_json[ 'id' ]
                if i <= 2:
                    transaction_json[ 'gift_id' ] = 1
                    transaction_json[ 'gift_searchable_id' ] = uuid.UUID( searchable_ids[ 0 ] ).hex
                elif i == 3:
                    transaction_json[ 'gift_id' ] = i
                    transaction_json[ 'gift_searchable_id' ] = uuid.UUID( searchable_ids[ i ] ).hex
                else:
                    transaction_json[ 'gift_id' ] = i
                    transaction_json[ 'gift_searchable_id' ] = uuid.UUID( searchable_ids[ i ] ).hex

                transaction_model = TransactionSchema().load( transaction_json ).data
                transaction_models.append( transaction_model )

            database.session.bulk_save_objects( transaction_models )
            database.session.commit()

            # Get all transactions in the database.
            response = self.test_client.get( url, headers=self.headers )
            self.assertEqual( len( json.loads( response.data.decode( 'utf-8' ) ) ), 5 )

            # searchable_ids[ 0 ] is gift ID = 1 and will have 3 transactions: test string searchable ID.
            response = self.test_client.post(
                url,
                data=json.dumps( { 'searchable_ids': uuid.UUID( searchable_ids[ 0 ] ).hex } ),
                content_type='application/json',
                headers=self.headers
            )

            self.assertEqual( len( json.loads( response.data.decode( 'utf-8' ) ) ), 3 )

            # Gift ID = 1 and 2 and will have a total of 4 transactions: test list of searchable ID's.
            response = self.test_client.post(
                url,
                data=json.dumps(
                    {
                        'searchable_ids':
                            [ uuid.UUID( searchable_ids[ 0 ] ).hex, uuid.UUID( searchable_ids[ 2 ] ).hex ]
                    }
                ),
                content_type='application/json',
                headers=self.headers
            )

            self.assertEqual( len( json.loads( response.data.decode( 'utf-8' ) ) ), 4 )
    def get( self, transaction_id ):
        """Simple endpoint to retrieve one row from table."""

        transaction = get_transactions_by_ids( transaction_id )
        result = TransactionSchema().dump( transaction ).data
        return result, 200
示例#26
0
def create_database_transactions():
    """Uses Braintree sales during a specified interval to build the initial gift and transaction in the database.

    Very useful for filling the database and then running the transaction updater for testing. The transactions
    created here will have type 'Gift' and status 'Completed'.
    """

    dates = {'month_0': 7, 'day_0': 1, 'month_1': 7, 'day_1': 31}
    date1 = datetime.utcnow().replace(month=dates['month_1'],
                                      day=dates['day_1'],
                                      hour=23,
                                      minute=59,
                                      second=59,
                                      microsecond=9999)
    date0 = datetime.utcnow().replace(month=dates['month_0'],
                                      day=dates['day_0'],
                                      hour=0,
                                      minute=0,
                                      second=0,
                                      microsecond=0)
    print('{} ~ {}'.format(date0.strftime(MODEL_DATE_STRING_FORMAT),
                           date1.strftime(MODEL_DATE_STRING_FORMAT)))

    with app.app_context():

        date_in_utc = datetime.fromtimestamp(0)
        sales_authorized = {}
        search_at(date0, date1, 'authorized_at', sales_authorized)
        for sales_id, sale in sales_authorized.items():  # pylint: disable=unused-variable
            gift_dict = {
                'id': None,
                'searchable_id': uuid.uuid4(),
                'user_id': 999999999,
                'method_used': 'Web Form Credit Card',
                'sourced_from_agent_id': AGENT_ID,
                'given_to': MERCHANT_ID_GIVEN_TO[sale.merchant_account_id],
                'recurring_subscription_id': sale.subscription_id
            }
            gift_model = from_json(GiftSchema(), gift_dict)
            database.session.add(gift_model.data)
            database.session.flush()
            database.session.commit()

            for history_item in sale.status_history:
                date_in_utc = datetime.fromtimestamp(0)
                if history_item.status == 'authorized':
                    date_in_utc = history_item.timestamp.strftime(
                        MODEL_DATE_STRING_FORMAT)
                    break

            transaction_dict = {
                'gift_id':
                gift_model.data.id,
                'date_in_utc':
                date_in_utc,
                'enacted_by_agent_id':
                AGENT_ID,
                'type':
                'Gift',
                'status':
                'Completed',
                'reference_number':
                sale.id,
                'gross_gift_amount':
                sale.amount,
                'fee':
                sale.service_fee_amount
                if sale.service_fee_amount else Decimal(0),
                'notes':
                'Automated creation of transaction.'
            }

            transaction_model = from_json(TransactionSchema(),
                                          transaction_dict)
            database.session.add(transaction_model.data)
            database.session.commit()
    def get( self ):
        """Simple endpoint to retrieve all rows from table."""

        transactions = get_transactions_by_ids( transaction_ids=None )
        result = TransactionSchema( many=True ).dump( transactions ).data
        return result, 200
    def post( self ):
        """Simple endpoint to return several rows from table given a list of ID's."""

        transactions = get_transactions_by_ids( request.json[ 'transaction_ids' ] )
        result = TransactionSchema( many=True ).dump( transactions ).data
        return result, 200
示例#29
0
def handle_thank_you_letter_logic(searchable_ids, enacted_by_agent_id):
    """The code to handle the model for a thank you letter being sent.

    We have from the front-end the following searchable_ids:

    searchable_ids = [ searchable_id_1, searchable_id_2, ... ] or possible and empty list ( [] 0.

    When thank you letters are sent:
      1. Get the agent ID using the Ultsys ID from the JWT in the resource.
      2. Build out gift ID's and user for thank you letters.
      3. Create new transactions with type: Thank You Sent

      Return to resource where thank you is emailed.

    :param searchable_ids: The gift searchable IDs to build thank yous for.
    :param enacted_by_agent_id: The agent processing the batch of thank yous.
    :return: The thank you dictionaries and the URL to the CSV.
    """

    thank_you_dicts = []
    transaction_models = []

    if not searchable_ids:
        searchable_ids_tmp = database.session.query( GiftModel.searchable_id )\
            .join( GiftThankYouLetterModel, GiftModel.id == GiftThankYouLetterModel.gift_id ).all()
        searchable_ids = [
            str(searchable_id[0].hex.upper())
            for searchable_id in searchable_ids_tmp
        ]

    # 2. New transaction with Thank You Sent type
    gift_searchable_ids, gift_ids = build_out_gift_ids(searchable_ids)
    user_data = build_out_user_data(searchable_ids)

    for searchable_id in searchable_ids:
        thank_you_dict = {}
        transaction_model = TransactionModel(
            gift_id=gift_searchable_ids[searchable_id]['gift_id'],
            date_in_utc=datetime.utcnow(),
            enacted_by_agent_id=enacted_by_agent_id,
            type=gift_searchable_ids[searchable_id]['type'],
            status='Thank You Sent',
            reference_number=gift_searchable_ids[searchable_id]
            ['reference_number'],
            gross_gift_amount=gift_searchable_ids[searchable_id]
            ['gross_gift_amount'],
            fee=Decimal(0.00),
            notes='Thank you email sent.')
        transaction_models.append(transaction_model)
        thank_you_dict['transaction'] = to_json(TransactionSchema(),
                                                transaction_model).data
        thank_you_dict['transaction']['gift_id'] = gift_searchable_ids[
            searchable_id]['gift_id']

        thank_you_dict['gift'] = gift_searchable_ids[searchable_id]
        thank_you_dict['user'] = user_data[searchable_id]
        thank_you_dicts.append(thank_you_dict)
    try:
        GiftThankYouLetterModel.query\
            .filter( GiftThankYouLetterModel.gift_id.in_( gift_ids ) )\
            .delete( synchronize_session='fetch' )
        database.session.add_all(transaction_models)
        database.session.flush()
    except SQLAlchemyError as error:
        database.session.rollback()
        raise error

    try:
        database.session.commit()
    except SQLAlchemyError as error:
        database.session.rollback()
        raise error

    # 3. Create a CSV of the results.
    url = build_thank_you_letters_csv(thank_you_dicts)

    return thank_you_dicts, url
def build_transactions(  # pylint: disable=too-many-locals
        sale_or_dispute, history_attributes, gift_id, refunded_transaction_id
):
    """Given a sale or dispute, along with some other data build a transaction for the sale.

    :param sale_or_dispute: This is either a sale or dispute Braintree object.
    :param history_attributes: The parsed history on the sale or dispute.
    :param gift_id: The gift ID the transaction is attached to.
    :param refunded_transaction_id: The parent ID to a refunded transaction.
    :return:
    """

    transaction_models = []

    is_dispute, history_attributes_sorted = get_sorted_history_attributes(
        transaction_models, sale_or_dispute, history_attributes, gift_id
    )

    total_amount = get_total_amount( gift_id )

    for status, timestamp in history_attributes_sorted.items():
        amount = Decimal( 0 )
        fee = Decimal( 0 )
        transaction_status_type = {}
        if is_dispute:
            transaction_status_type[ 'type' ] = 'Dispute'
            transaction_status_type[ 'status' ] = DISPUTE_STATUS_HISTORY[ status ]
            if sale_or_dispute.amount_disputed:
                amount = sale_or_dispute.amount_disputed
        else:
            transaction_status_type = get_transaction_status_type( status, refunded_transaction_id )
            if not transaction_status_type:
                continue
            if sale_or_dispute.amount:
                amount = sale_or_dispute.amount
            if sale_or_dispute.service_fee_amount:
                fee = sale_or_dispute.service_fee_amount

        transaction_type = transaction_status_type[ 'type' ]
        transaction_status = transaction_status_type[ 'status' ]

        # See if a transaction already exists.
        transaction = TransactionModel.query.filter_by( reference_number=sale_or_dispute.id ) \
            .filter_by( type=transaction_type ) \
            .filter_by( status=transaction_status ).one_or_none()

        if not transaction:

            # Increment/decrement the total amount currently on the gift given its type and status.
            total_amount += MULTIPLIER_FOR_TYPE_STATUS[ transaction_type ][ transaction_status ] * amount

            transaction_dict = {
                'gift_id': gift_id,
                'date_in_utc': timestamp.strftime( MODEL_DATE_STRING_FORMAT ),
                'enacted_by_agent_id': AGENT_ID,
                'type': transaction_type,
                'status': transaction_status,
                'reference_number': sale_or_dispute.id,
                'gross_gift_amount': total_amount,
                'fee': fee,
                'notes': 'Automated creation' if not refunded_transaction_id
                         else 'Automated creation: parent ID is {}.'.format( refunded_transaction_id )
            }
            transaction_model = from_json( TransactionSchema(), transaction_dict )
            transaction_models.append( transaction_model.data )

            # If the gift amount >= $100 ( current threshold ), add to gift_thank_you_letter table
            if ( Decimal( transaction_dict[ 'gross_gift_amount' ] ) >= THANK_YOU_LETTER_THRESHOLD ) \
                    and ( type == 'Gift' and status == 'Completed' ):
                database.session.add( GiftThankYouLetterModel( gift_id=transaction_dict[ 'gift_id' ] ) )

    return transaction_models