def build_transaction(payload_transaction, assign_status): """Build a transaction and attach to the gift for an email sent. :param payload_transaction: The transaction from the sale. :param assign_status: May be 'Thank You Sent' or 'Receipt Sent'. :return: transaction """ enacted_by_agent = AgentModel.get_agent('Organization', 'name', 'Donate API') enacted_by_agent_id = str(enacted_by_agent.id) transaction = copy.deepcopy(payload_transaction) transaction['gift_id'] = str(payload_transaction['gift_id']) transaction['gift_searchable_id'] = str( payload_transaction['gift_searchable_id']) transaction['date_in_utc'] = datetime.datetime.utcnow().strftime( '%Y-%m-%d %H:%M:%S') transaction['gross_gift_amount'] = payload_transaction['gross_gift_amount'] transaction['status'] = assign_status transaction['type'] = payload_transaction['type'] transaction['enacted_by_agent_id'] = enacted_by_agent_id transaction['notes'] = 'Receipt sent for {}'.format(transaction['type']) return transaction
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 get_agent_id(): """Get the agent ID for the Donate API. :return: agent_id """ agent = AgentModel.get_agent('Organization', 'name', 'Donate API') agent_id = agent.id return agent_id
def build_transaction( transaction_dict, agent_ultsys_id ): """The controller to build a transaction for a gift with searchable ID. :param transaction_dict: The transaction dictionary to use to build the model. :param agent_ultsys_id: The agent ultsys ID to be converted to the Agent ID primary key. :return: A transaction. """ enacted_by_agent = AgentModel.get_agent( 'Staff Member', 'user_id', agent_ultsys_id ) transaction_dict[ 'enacted_by_agent_id' ] = enacted_by_agent.id transaction = create_transaction( transaction_dict ) return transaction
def package_gift_and_transaction(self, data): """This function is run after the dump but before return. Gives us an opportunity to work withBraintree sale fields before returning to the calling code. A context is assigned prior to the dump. This context includes the transaction and gift dictionaries. They are modified here using the data. :param data: The data passed from the schema to the function. :return: """ gift = self.context['gift'] transaction = self.context['transaction'] gift['customer_id'] = self.customer_id transaction['date_in_utc'] = data['date_in_utc'] transaction['status'] = 'Completed' transaction['gross_gift_amount'] = data['gross_gift_amount'] if data['refunded_transaction_id']: transaction['gross_gift_amount'] = -1 * data['gross_gift_amount'] transaction['type'] = 'Refund' transaction['reference_number'] = data['reference_number'] is_account_type = data[ 'payment_instrument_type' ] == 'credit_card' \ or data[ 'payment_instrument_type' ] == 'paypal_account' if data[ 'type' ] == 'sale' \ and is_account_type \ and not data[ 'status' ] == 'voided' \ and not data[ 'refunded_transaction_id' ]: transaction['type'] = 'Gift' elif data['status'] == 'voided': transaction['type'] = 'Void' transaction[ 'gross_gift_amount'] = -1 * transaction['gross_gift_amount'] if not data['fee']: transaction['fee'] = Decimal(0.00) sourced_from_agent = AgentModel.get_agent('Organization', 'name', 'Braintree') gift['sourced_from_agent_id'] = sourced_from_agent.id data['transaction'] = transaction data['gift'] = gift
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
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'} MERCHANT_ID_GIVEN_TO = {'numbersusa': 'NERF', 'numbersusa_action': 'ACTION'} # Get the Agent ID from the model for type Automated: with app.app_context(): AGENT_MODEL = AgentModel.get_agent('Automated', 'type', 'Automated') # pylint: disable=invalid-name AGENT_ID = str(AGENT_MODEL.id) def manage_sales(): """A workbench that you can use to create and manipulate Braintree sales.""" print('\n\n# ******************** MANAGE SALES ******************** #') print(' datetime.now() : {}\n'.format( datetime.now().strftime(MODEL_DATE_STRING_FORMAT))) print(' datetime.utcnow(): {}\n'.format( datetime.utcnow().strftime(MODEL_DATE_STRING_FORMAT))) braintree_id = '5e4vmzcx' secs = 2 set_status_settled_by_id(braintree_id, secs)
def admin_correct_gift(payload): """A function for correcting and or reallocating a gift. The reallocation may be to to a different organization, e.g. NERF, ACTION, or SUPPORT. The reallocation of a gift will look for a subscription, and if one exists move it to the new plan. It will not reconcile any past subscription payments. payload = { "gift": { "gift_searchable_id": "6AE03D8EA2DC48E8874F0A76A1C43D5F", "reallocate_to": "NERF" }, "transaction": { "reference_number": null, "gross_gift_amount": "1000.00", "notes": "An online donation to test receipt sent email." }, "user": { "user_id": 1 }, "agent_ultsys_id": 322156 } :param dict payload: A dictionary that provides information to make the correction and/or reallocation. :return: Boolean for success or failure. """ is_a_reallocation = 'reallocate_to' in payload['gift'] and payload['gift'][ 'reallocate_to'] # Build the transaction to correct the gift. try: # Build what we can from the payload and then pass the models on. enacted_by_agent_model = AgentModel.get_agent( 'Staff Member', 'user_id', payload['agent_ultsys_id']) gift_model = GiftModel.query.filter_by( searchable_id=payload['gift']['searchable_id']).one() if is_a_reallocation: gift_model.given_to = payload['gift']['reallocate_to'] transaction_model = TransactionModel( gross_gift_amount=payload['transaction'] ['corrected_gross_gift_amount'], fee=payload['transaction']['fee'], notes=payload['transaction']['notes']) transaction_model, gift_model = correct_transaction( transaction_model, gift_model, enacted_by_agent_model) except (AdminFindGiftPathError, AdminUpdateSubscriptionPathError, AdminBuildModelsPathError) as error: logging.exception(error.message) return False import ipdb ipdb.set_trace() # Reallocate the subscription if there is one. if gift_model.recurring_subscription_id and is_a_reallocation: try: braintree_subscription = reallocate_subscription( gift_model.recurring_subscription_id, gift_model.given_to) except (AdminFindGiftPathError, AdminUpdateSubscriptionPathError, AdminBuildModelsPathError) as error: logging.exception(error.message) return False import ipdb ipdb.set_trace() try: gift = GiftModel.query.filter_by( searchable_id=payload['gift_searchable_id']).one_or_none() user = find_user(gift) recurring = False if gift.recurring_subscription_id: recurring = True send_admin_email(transaction, user, recurring) except GeneralHelperFindUserPathError as error: logging.exception(error.message) return False except (SendAdminEmailModelError, EmailSendPathError, BuildEmailPayloadPathError, EmailHTTPStatusError) as error: logging.exception(error.message) return False return True
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 make_braintree_sale(payload, app): """Use the form data, front-end payload, to build a Braintree Transaction.sale(). The payload is: payload = { 'user': user, 'transaction': transaction, and 'gift': gift } These will be used to build the 3 models required by the transaction: UserModel, GiftModel, and TransactionModel. The process includes: 1. Categorize donor: new, cage, exists, or caged. 2. Find, and if required create a Braintree customer ( customer_id ). 3. If a subscription is requested create one ( recurring_subscription_id ). 4. Create the Braintree sale ( Braintree transaction ID among other things ) With the categorization and transaction processed the initialized dictionaries are updated. Any errors that may arise during Braintree transactions are handled. When completed, the model dictionaries are returned to the calling function. If there are any errors those are raised. :param dict payload: A dictionary with form data from the front-end. ( See controller for payload content. ) :param app: The current app. :return: Returns a dictionary with model data. :raises SQLAlchemyORMNoResultFoundError: The ORM didn't find the table row. """ merchant_account_id = { 'NERF': app.config['NUMBERSUSA'], 'ACTION': app.config['NUMBERSUSA_ACTION'] } # Attach payment method nonce to user for customer and sale create. payload['user']['payment_method_nonce'] = payload['payment_method_nonce'] # We don't want to do caging up front because it takes too long. Move to end of the sale in controller. # Assign a category: 'queued' and a user ID of -2 ( -1 is used for caged ) payload['user']['category'] = 'queued' payload['gift']['user_id'] = -2 # Setting the user to queued handle the creation of the Braintree customer, for example: # 1. If they are a new donor the Braintree customer has to be created. # 2. If they exist they may or may not have a Braintree customer ID. payload['user']['customer_id'] = '' # The parameter recurring_subscription is a Boolean. recurring_subscription = payload['recurring_subscription'] if isinstance(payload['recurring_subscription'], str): recurring_subscription = json.loads(payload['recurring_subscription']) if recurring_subscription: # Create the customer and pull the payment method token. payment_method_token = get_payment_method_token(payload) # Use this current payment method token to create the subscription. result_subscription = create_braintree_subscription( payment_method_token, merchant_account_id[payload['gift']['given_to']], payload['transaction']['gross_gift_amount'], merchant_account_id[payload['gift']['given_to']]) payload['gift'][ 'recurring_subscription_id'] = result_subscription.subscription.id braintree_sale = result_subscription.subscription.transactions[0] else: # Create Transaction.sale() on Braintree. # Create the customer and pull the payment method token. payment_method_token = get_payment_method_token(payload) result_sale = create_braintree_sale( payment_method_token, payload['user'], payload['transaction']['gross_gift_amount'], merchant_account_id[payload['gift']['given_to']]) # Finish creating dictionaries for deserialization later. braintree_sale = result_sale.transaction # Use BraintreeSaleSchema to populate gift and transaction dictionaries. braintree_schema = BraintreeSaleSchema() braintree_schema.context = { 'gift': payload['gift'], 'transaction': payload['transaction'] } braintree_sale = braintree_schema.dump(braintree_sale) gift = braintree_sale.data['gift'] transaction = braintree_sale.data['transaction'] # Get the sourced from agent ID and if it doesn't exist handle. if gift['method_used'] == 'Admin-Entered Credit Card': sourced_from_agent = AgentModel.get_agent( 'Staff Member', 'user_id', payload['sourced_from_agent_user_id']) gift['sourced_from_agent_id'] = sourced_from_agent.id # Get the enacted by agent ID and if it doesn't exist handle. enacted_by_agent = AgentModel.get_agent('Organization', 'name', 'Braintree') transaction['enacted_by_agent_id'] = enacted_by_agent.id # On success of sale return the model dictionaries. return { 'transactions': [transaction], 'gift': gift, 'user': payload['user'] }
'merchant_account_id', 'date_opened', 'created_at', 'updated_at' ] # If we need to build transactions we need to know the sign to attribute to the sale amount. MULTIPLIER_FOR_TYPE_STATUS = { 'Gift': { 'Completed': 1 }, 'Correction': { 'Completed': 0 }, 'Refund': { 'Completed': -1 }, 'Void': { 'Completed': 1 }, 'Dispute': { 'Won': 0, 'Lost': -1, 'Requested': 1, 'Accepted': 0 }, 'Fine': { 'Completed': -1 } } # Get the Agent ID from the model for type Automated. This is used on both the Gift and Transaction models. with app.app_context(): AGENT_MODEL = AgentModel.get_agent( 'Organization', 'name', 'Donate API' ) # pylint: disable=invalid-name AGENT_ID = str( AGENT_MODEL.id ) # **************************************************************** # # ***** INTERVAL FOR CRON SET HERE ******************************* # # Here is where the interval is set. The cron job executes the script on some repeated time, e.g. every 5 minutes: # */5 * * * * cd /home/apeters/git/DONATE_updater && /home/apeters/git/DONATE_updater/venv/bin/python3 # -c "import jobs.braintree;jobs.braintree.manage_status_updates()" >> # /home/apeters/git/DONATE_updater/cron.log 2>&1 INTERVAL = timedelta( days=30, hours=23, minutes=59, seconds=59, microseconds=999999 ) DATE1 = datetime.utcnow().replace( hour=23, minute=59, second=59, microsecond=999999 ) DATE0 = DATE1 - INTERVAL LOG_INFO = 'Transaction updater cron job: %s ~ %s' % ( DATE0.strftime( MODEL_DATE_STRING_FORMAT ), DATE1.strftime( MODEL_DATE_STRING_FORMAT )
def make_admin_sale(payload): """Use the payload to build an administrative donation. payload = { "gift": { "method_used": "Check", "given_to": "NERF", }, "transaction": { "date_of_method_used": "2018-07-12 00:00:00", "gross_gift_amount": "15.00", "reference_number": "1201", "bank_deposit_number": "<bank-deposit-number>", "type": "Gift", "notes": "A note 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 } Since there is one payload from the front-end, which must include 2 dates and 2 reference numbers, these are both included as separate key-value pairs in the payload. This gives us one submit from front to back-end. :param dict payload: The payload required to update the models as needed. :return dict: Returns transaction and gift dictionaries. """ # We don't want to do caging up front because it takes too long. Move to end of the sale in controller. # Assign a category: 'queued' and a user ID of -2 ( -1 is used for caged ) payload['user']['category'] = 'queued' payload['gift']['user_id'] = -2 # This is not a Braintree transaction and do set the Braintree customer ID to None. payload['user']['customer_id'] = '' sourced_from_agent = AgentModel.get_agent( 'Staff Member', 'user_id', payload['sourced_from_agent_user_id']) enacted_by_agent = sourced_from_agent method_used = MethodUsedModel.get_method_used( 'name', payload['gift']['method_used']) # Create the gift dictionary from admin payload. gift = { 'campaign_id': None, 'method_used_id': method_used.id if method_used else None, 'sourced_from_agent_id': sourced_from_agent.id if sourced_from_agent else None, 'given_to': payload['gift']['given_to'].upper(), 'recurring_subscription_id': None } # Create the transaction dictionary from the administrative payload. # If it is a check or money order add a second transaction to capture the date on the payment. transactions = [] utc_now = datetime.datetime.utcnow() transaction_type = payload['transaction']['type'] transaction_notes = payload['transaction']['notes'] method_used_date_note = 'Date given is date of method used. {}'.format( transaction_notes) fee = 0.00 if 'fee' in payload and payload['transaction']['fee']: fee = payload['transaction']['fee'] is_check_money_order = payload[ 'gift' ][ 'method_used' ] == 'Check' or\ payload[ 'gift' ][ 'method_used' ] == 'Money Order' transactions.append({ 'date_in_utc': payload['transaction']['date_of_method_used'], 'enacted_by_agent_id': enacted_by_agent.id if enacted_by_agent else None, 'type': transaction_type, 'status': 'Completed', 'reference_number': payload['transaction']['reference_number'], 'gross_gift_amount': payload['transaction']['gross_gift_amount'], 'fee': fee, 'notes': method_used_date_note if is_check_money_order else transaction_notes }) if is_check_money_order: bank_agent = AgentModel.get_agent('Organization', 'name', 'Fidelity Bank') bank_agent_id = bank_agent.id transactions.append({ 'date_in_utc': utc_now.strftime('%Y-%m-%d %H:%M:%S'), 'enacted_by_agent_id': bank_agent_id, 'type': 'Deposit to Bank', 'status': 'Completed', 'reference_number': payload['transaction']['bank_deposit_number'], 'gross_gift_amount': payload['transaction']['gross_gift_amount'], 'fee': fee, 'notes': '' }) return { 'transactions': transactions, 'gift': gift, 'user': payload['user'] }
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