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)
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
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 )
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' )
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
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 )
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
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)
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 )
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)
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
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
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
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