def setUp(self): self.app = create_app('TEST') self.app.testing = True self.test_client = self.app.test_client() self.parameters = { 'reference_number': 'braintree_reference_number', 'user_exists_id': 5, 'user_new_id': '67', 'customer_id': 'customer_id', 'method_used': 'Web Form Credit Card', 'given_to': 'NERF', 'transaction_status': 'Completed', 'transaction_type': 'Gift', 'gross_gift_amount': Decimal(25.00), 'fee': Decimal(0.00) } init_braintree_credentials(self.app) with self.app.app_context(): database.reflect() database.drop_all() database.create_all() # Create some ultsys user data for the Ultsys endpoints wrapped in functions for mocking. create_ultsys_users() database.session.add_all(create_method_used()) database.session.commit() self.method_used_id = MethodUsedModel.get_method_used( 'name', self.parameters['method_used']).id
def get_braintree_token(): """Handle token generation for Braintree API. The front-end uses hosted fields and requires a Braintree token to make a submission for a sale. On submission a payment nonce will be returned to the back-end to create the Transaction.sale( {} ). The initial token is created with a call to this endpoint. :return: Braintree token. """ init_braintree_credentials(current_app) return generate_braintree_token()
def setUp(self): self.app = create_app('TEST') self.app.testing = True self.test_client = self.app.test_client() self.parameters = {} init_braintree_credentials(self.app) with self.app.app_context(): database.reflect() database.drop_all() database.create_all() database.session.add_all(create_method_used()) database.session.commit()
def admin_get_braintree_sale_status(transaction_id): """A function for getting the Braintree status of a sale. :param dict transaction_id: The gift searchable ID associated with the Braintree sale. :return: Braintree status. """ init_braintree_credentials(current_app) try: transaction = TransactionModel.query.filter_by(id=transaction_id).one() braintree_id = transaction.reference_number braintree_transaction = get_braintree_transaction(braintree_id) except AdminTransactionModelPathError as error: logging.exception(error.message) return False return braintree_transaction.status
def reallocate_subscription( recurring_subscription_id, reallocate_to ): """A function for reallocating a Braintree gift subscription. :param recurring_subscription_id: The Braintree subscription ID from the gift. :param reallocate_to: The account to reallocate the donation to. :return: Braintree subscription """ # Configure Braintree. init_braintree_credentials( current_app ) merchant_account_id = { 'NERF': current_app.config[ 'NUMBERSUSA' ], 'ACTION': current_app.config[ 'NUMBERSUSA_ACTION' ] } # This is an administrative function and we allow them to grab a default payment method. subscription = braintree.Subscription.find( recurring_subscription_id ) # Getting this far, we can now update the subscription to the new plan and merchant account ID as required. # The original Braintree transaction maintains the same merchant account ID for historical significance. # The original Braintree transaction that is reallocated will have the new subscription plan ID. # New Braintree transactions from the subscription will have new merchant account ID/subscription plan ID. braintree_subscription = braintree.Subscription.update( recurring_subscription_id, { 'id': recurring_subscription_id, 'payment_method_token': subscription.payment_method_token, 'plan_id': merchant_account_id[ reallocate_to ], 'merchant_account_id': merchant_account_id[ reallocate_to ] } ) if not braintree_subscription.is_success: errors = handle_braintree_errors( braintree_subscription ) logging.exception( AdminUpdateSubscriptionPathError( errors=errors ).message ) return braintree_subscription
from time import sleep import braintree from application.app import create_app from application.flask_essentials import database from application.helpers.braintree_api import init_braintree_credentials from application.helpers.model_serialization import from_json from application.models.agent import AgentModel from application.models.transaction import TransactionModel from application.schemas.gift import GiftSchema from application.schemas.transaction import TransactionSchema app = create_app('DEV') # pylint: disable=C0103 init_braintree_credentials(app) MODEL_DATE_STRING_FORMAT = '%Y-%m-%d %H:%M:%S' BRAINTREE_DATE_STRING_FORMAT = '%Y-%m-%dT%H:%M:%SZ' DEFAULT_DATE_IN_UTC = datetime.fromtimestamp(0).strftime( MODEL_DATE_STRING_FORMAT) VALID_CARD_NUMBER = '4111111111111111' DISPUTE_CARD_NUMBER = '4023898493988028' AMOUNT_VALID = '20.00' AMOUNT_GATEWAY_REJECTED = '5001.00' AMOUNT_PROCESSOR_DECLINED = '2000.00' MERCHANT_ACCOUNT_ID = {'NERF': 'numbersusa', 'ACTION': 'numbersusa_action'}
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 post_donation(payload): """ Handle individual donation by calling Braintree API if 'Online', else build the transaction here. Calls the function make_braintree_sale( payload ) or make_admin_sale( payload ), both located in helpers, to handle the donation. The first incorporates the Braintree API to make the sale, the second does not. The Braintree API will create a customer in the vault if needed, register a subscription, and make the sale. It will return errors if any occur. Both sales call a caging function to categorize the donor. Once the sale is made gift, transaction, and user dictionaries are returned and the model updates managed in the present function. payload = { "gift": { "method_used": "Web Form Credit Card", "date_of_method_used": datetime, "given_to": "NERF" }, "transaction": { "gross_gift_amount": "10.00", "notes": "Some notes for the transaction." }, "user": { "user_id": null, "user_address": { "user_first_name": "Ralph", "user_last_name": "Kramden", "user_zipcode": "11214", "user_address": "328 Chauncey St", "user_city": "Bensonhurst", "user_state": "NY", "user_email_address": "*****@*****.**", "user_phone_number": "9172307441" }, "billing_address": { "billing_first_name": "Ralph", "billing_last_name": "Kramden", "billing_zipcode": "11214", "billing_address": "7001 18th Ave", "billing_city": "Bensonhurst", "billing_state": "NY", "billing_email_address": "*****@*****.**", "billing_phone_number": "9172307441" } }, "payment_method_nonce": "fake-valid-visa-nonce", "recurring_subscription": false } If this is an administrative donation then there needs to be 3 additional fields on the payload: payload[ 'transaction' ][ 'date_of_method_used' ] = 2018-10-04 00:00:00, e.g. the date on the check. payload[ "transaction" ][ "reference_number" ] = "101" payload[ "transaction" ][ "bank_deposit_number" ] = "<bank_deposit_number>" The agent ID is extracted from the JWT token and placed on the payload if applicable. :param dict payload: Dictionary of needed information to make a donation. :return: Boolean for success or failure. """ init_braintree_credentials(current_app) # This is a fix to a mismatch between what the back-end expects and what the front-end is passing. user = payload.pop('user') user = validate_user_payload(user) payload['user'] = user # If method used is Web Form Credit Card or Admin-Entered Credit Card then make a Braintree sale, otherwise is one # of the other administrative requests, e.g. Check, Money Order, Stock, Cash, Other. braintree_sale = payload[ 'gift' ][ 'method_used' ].lower() == 'web form credit card' or \ payload[ 'gift' ][ 'method_used' ].lower() == 'web form paypal' or \ payload[ 'gift' ][ 'method_used' ].lower() == 'admin-entered credit card' # See if we need to make a Braintree sale, that is Online or Credit Card call Braintree API. # Since merchant account ID is not set for Support a donation to that cannot be made ( raises an error ). # Otherwise make the administrative sale, e.g. check, stock, wire transfer, etc. # If the code fails in the conditional the database is not modified and the exception is handled by # app.errorhandler(). if braintree_sale and payload['gift']['given_to'].lower() != 'support': donation = make_braintree_sale(payload, current_app) elif braintree_sale and payload['gift']['given_to'].lower() == 'support': raise ModelGiftImproperFieldError else: donation = make_admin_sale(payload) # Getting ready to save to the database, and want to prevent orphaned gifts/transactions. # The response sent back will have redis job ID, status, and the gift searchable ID. response = {} try: # Call build_models_sale() and if an exception occurs while building gift/transaction roll back and quit. # If the gift/transaction is made then see about the Thank You letter and the email. If either fails # log and move on. build_models_sale(donation['user'], donation['gift'], donation['transactions']) # If the gift amount >= $100 ( current threshold ), add to gift_thank_you_letter table if Decimal( donation[ 'transactions' ][ 0 ][ 'gross_gift_amount' ] ) \ >= Decimal( current_app.config[ 'THANK_YOU_LETTER_THRESHOLD' ] ): try: database.session.add( GiftThankYouLetterModel( gift_id=donation['transactions'][0]['gift_id'])) database.session.commit() except: database.session.rollback() logging.exception(DonateBuildModelPathError().message) try: recurring = False if 'recurring_subscription_id' in donation['gift'] and donation[ 'gift']['recurring_subscription_id']: recurring = True send_admin_email(donation['transactions'][0], donation['user'], recurring) except (SendAdminEmailModelError, EmailSendPathError, BuildEmailPayloadPathError, EmailHTTPStatusError) as error: logging.exception(error.message) response['gift_searchable_id'] = str( donation['user']['gift_searchable_id']) except BuildModelsGiftTransactionsPathError as error: logging.exception(error.message) # Build the queued donor model with whatever information we have from above. # It can still help construct what happened if there is an error higher up. try: build_model_queued_donor(donation['user']) except BuildModelsQueuedDonorPathError as error: logging.exception(error.message) # Once on the queue it is out of our hands, but may fail on arguments to queue(). job = None try: job = redis_queue_caging.queue(donation['user'], donation['transactions'], current_app.config['ENV']) except: pass # If the redis job fails quickly, e.g. the ENV is incorrect, the status will be 'failed'. # Otherwise, if it takes more time to fail, the status will be 'queued', 'started', etc. # We get what status we can and pass it back. response['job_id'] = None response['job_status'] = None if job: response['job_id'] = job.get_id() response['job_status'] = job.get_status() logging.debug('REDIS CAGING: %s', response) return response
def refund_transaction(payload): """A function for refunding a Braintree transaction on a gift. Find the transaction in the database and get the Braintree transaction number. Configure the Braintree API and make the refund through Braintree. payload = { "transaction_id": 1, "amount": "0.01", "transaction_notes": "Some transaction notes." } :param dict payload: A dictionary that provides information to make the refund. :return: :raises MarshmallowValidationError: If Marshmallow throws a validation error. :raises SQLAlchemyError: General SQLAlchemy error. :raises SQLAlchemyORMNoResultFoundError: The ORM didn't find the table row. """ # Retrieve the transaction that is to be refunded and raise an exception if not found. # Don't try to continue to Braintree without a valid reference number. try: transaction_model = TransactionModel.query.filter_by( id=payload['transaction_id']).one() except: raise AdminTransactionModelPathError(where='parent') transactions = TransactionModel.query.filter_by( gift_id=transaction_model.gift_id).all() current_balance = transactions[-1].gross_gift_amount # If the model cannot be built do not continue to Braintree. # Raise an exception. try: # Build transaction dictionary for schema. braintree_id = transaction_model.reference_number transaction_data = to_json(TransactionSchema(), transaction_model) transaction_json = transaction_data.data transaction_json['gift_id'] = transaction_model.gift_id transaction_json['notes'] = payload['transaction_notes'] except: raise AdminBuildModelsPathError() # Configure Braintree so we can generate a refund. init_braintree_credentials(current_app) # Function make_braintree_refund() returns: a Braintree refund transaction. transaction_refund = make_braintree_refund(braintree_id, payload['amount'], current_balance) # Need to attach the user who is doing the reallocation. 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_refund.transaction) transaction_json = braintree_sale.data['transaction'] transaction_json.pop('id') 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_refund_model = from_json(TransactionSchema(), transaction_json) database.session.add(transaction_refund_model.data) database.session.commit() database.session.flush() transaction_json['id'] = transaction_refund_model.data.id except: raise AdminBuildModelsPathError() return transaction_json