def transfer_token(sender: User, recipient: User, amount: float, reason_id: Optional[int] = None, transfer_subtype: Optional[TransferSubTypeEnum]=TransferSubTypeEnum.STANDARD): sent_token = default_token(sender) received_token = default_token(recipient) transfer_use = None if reason_id is not None: transfer_use = str(int(reason_id)) transfer = make_payment_transfer(amount, token=sent_token, send_user=sender, receive_user=recipient, transfer_use=transfer_use, is_ghost_transfer=True, require_sender_approved=False, require_recipient_approved=False, transfer_subtype=transfer_subtype, transfer_mode=TransferModeEnum.USSD) exchanged_amount = None if sent_token.id != received_token.id: exchange = Exchange() exchange.exchange_from_amount(user=recipient, from_token=sent_token, to_token=received_token, from_amount=amount, prior_task_uuids=[transfer.blockchain_task_uuid], transfer_mode=TransferModeEnum.USSD) exchanged_amount = exchange.to_transfer.transfer_amount return exchanged_amount
def exchange_success_sms(message_key: str, user: User, other_user: User, own_amount: float, other_amount: float, tx_time: datetime, balance: float): rounded_own_amount_dollars = rounded_dollars(own_amount) rounded_other_amount_dollars = rounded_dollars(other_amount) rounded_balance_dollars = rounded_dollars(balance) TokenProcessor.send_sms( user, message_key, own_amount=rounded_own_amount_dollars, other_amount=rounded_other_amount_dollars, own_token_name=default_token(user).symbol, other_token_name=default_token(other_user).symbol, other_user=other_user.user_details(), date=tx_time.strftime('%d/%m/%Y'), time=tx_time.strftime('%I:%M %p'), balance=rounded_balance_dollars)
def fetch_exchange_rate(user: User): from_token = default_token(user) default_limit, limit_amount = TokenProcessor.get_default_limit( user, from_token) exchange_rate_full_precision = TokenProcessor.get_exchange_rate( user, from_token) exchange_limit = rounded_dollars(limit_amount) exchange_rate = round_to_sig_figs(exchange_rate_full_precision, 3) exchange_sample_value = round(exchange_rate_full_precision * float(1000)) if exchange_limit: TokenProcessor.send_sms( user, "exchange_rate_can_exchange_sms", token_name=from_token.symbol, exchange_rate=exchange_rate, exchange_limit=exchange_limit, exchange_sample_value=exchange_sample_value, limit_period=default_limit.time_period_days) else: TokenProcessor.send_sms( user, "exchange_rate_sms", token_name=from_token.symbol, exchange_rate=exchange_rate, exchange_sample_value=exchange_sample_value, )
def send_balance_sms(user: User): token_balances_dollars, token_exchanges = TokenProcessor._get_token_balances( user) if token_exchanges in ["\n", '']: TokenProcessor.send_sms(user, "send_balance_sms", token_balances=token_balances_dollars) return default_limit, limit_amount = TokenProcessor.get_default_limit( user, default_token(user)) if default_limit: TokenProcessor.send_sms( user, "send_balance_exchange_limit_sms", token_balances=token_balances_dollars, token_exchanges=token_exchanges, limit_period=default_limit.time_period_days) return TokenProcessor.send_sms(user, "send_balance_exchange_sms", token_balances=token_balances_dollars, token_exchanges=token_exchanges)
def send_success_sms(message_key: str, user: User, other_user: User, amount: float, reason: str, tx_time: datetime, balance: float): amount_dollars = rounded_dollars(amount) rounded_balance_dollars = rounded_dollars(balance) TokenProcessor.send_sms(user, message_key, amount=amount_dollars, token_name=default_token(user).symbol, other_user=other_user.user_details(), date=tx_time.strftime('%d/%m/%Y'), reason=reason, time=tx_time.strftime('%I:%M %p'), balance=rounded_balance_dollars)
def process_account_creation_request(self, user_input): try: attach_transfer_account_to_user(self.user) disbursement_amount = cents_to_dollars( config.SELF_SERVICE_WALLET_INITIAL_DISBURSEMENT) self.send_sms(self.user.phone, "account_creation_success_sms", disbursement_amount=disbursement_amount, token_name=default_token(self.user).name) except Exception as e: self.send_sms(self.user.phone, "account_creation_error_sms") raise Exception('Account creation failed. Error: ', e)
def upsell_unregistered_recipient(self, user_input): recipient_phone = proccess_phone_number(user_input) self.send_sms( self.user.phone, 'upsell_message_sender', recipient_phone=recipient_phone, ) self.send_sms( recipient_phone, 'upsell_message_recipient', first_name=self.user.first_name, last_name=self.user.last_name, token_name=default_token(self.user).name )
def send_success_sms(message_key: str, user: User, other_user: User, amount: float, tx_time: datetime, balance: float): amount_dollars = rounded_dollars(amount) rounded_balance_dollars = rounded_dollars(balance) user_details = user.user_details().replace('+254', '0') other_user_details = other_user.user_details().replace('+254', '0') TokenProcessor.send_sms(user=user, message_key=message_key, amount=int(float(amount_dollars)), token_name=default_token(user).symbol, user_details=user_details, other_user_details=other_user_details, date=tx_time.strftime('%d/%m/%Y'), time=tx_time.strftime('%I:%M %p'), balance=int(float(rounded_balance_dollars)))
def get_directory_listing_users(self): token_id = default_token(self.recipient).id """ SEARCH CRITERIA: - users matching recipient provided business category - users with the recipient's token - users who are opted in to market [custom_attribute 'market_enabled' has value true] - order query by transaction count and limit to top 5 transacting users """ count_list = (db.session.query( User.id, func.count(CreditTransfer.id).label('count') ).execution_options(show_all=True).join(User.credit_receives).join( TransferAccount, TransferAccount.id == User.default_transfer_account_id).group_by( User.id).filter(TransferAccount.token_id == token_id).filter( User.is_market_enabled == True).filter( User.business_usage_id == self.selected_business_category.id).filter( CreditTransfer.transfer_status == 'COMPLETE'). limit(NUMBER_OF_DIRECTORY_LISTING_RESULTS).all()) user_ids = list(map(lambda x: x[0], count_list)) count_based_users = list( map(lambda x: User.query.execution_options(show_all=True).get(x), user_ids)) shortfall = NUMBER_OF_DIRECTORY_LISTING_RESULTS - len( count_based_users) matching_category_users = [] if shortfall > 0: matching_category_users = (User.query.execution_options( show_all=True).join( TransferAccount, TransferAccount.id == User.default_transfer_account_id).filter( User.id.notin_(user_ids)).filter( TransferAccount.token_id == token_id).filter( User.is_market_enabled == True).filter( User.business_usage_id == self.selected_business_category.id).limit( shortfall).all()) return count_based_users + matching_category_users
def send_token(sender: User, recipient: User, amount: float, reason_str: str, reason_id: int): try: exchanged_amount = TokenProcessor.transfer_token( sender, recipient, amount, reason_id) sender_tx_time = pendulum.now(sender.default_organisation.timezone) recipient_tx_time = pendulum.now( recipient.default_organisation.timezone) sender_balance = TokenProcessor.get_balance(sender) recipient_balance = TokenProcessor.get_balance(recipient) if exchanged_amount is None: TokenProcessor.send_success_sms("send_token_sender_sms", sender, recipient, amount, reason_str, sender_tx_time, sender_balance) TokenProcessor.send_success_sms("send_token_recipient_sms", recipient, sender, amount, reason_str, recipient_tx_time, recipient_balance) else: TokenProcessor.exchange_success_sms( "exchange_token_sender_sms", sender, recipient, amount, exchanged_amount, sender_tx_time, sender_balance) TokenProcessor.exchange_success_sms("exchange_token_agent_sms", recipient, sender, exchanged_amount, amount, recipient_tx_time, recipient_balance) except InsufficientBalanceError as e: token_balances_dollars, token_exchanges = TokenProcessor._get_token_balances( sender) TokenProcessor.send_sms(sender, "insufficient_balance_sms", amount=cents_to_dollars(amount), token_name=default_token(sender).name, recipient=recipient.user_details(), token_balances=token_balances_dollars) except TransferAmountLimitError as e: # Use a different message here from that used for exchange, # because the issue is caused by KYC (ie something the user can change by providing), rather than # our price-stability controls TokenProcessor.send_sms(sender, "transfer_amount_error_sms", amount=rounded_dollars( e.transfer_amount_limit), token=e.token, limit_period=e.limit_time_period_days) except Exception as e: # TODO: SLAP? all the others take input in cents TokenProcessor.send_sms(sender, "send_token_error_sms", amount=cents_to_dollars(amount), token_name=default_token(sender).name, recipient=recipient.user_details())
def custom_display_text(menu: UssdMenu, ussd_session: UssdSession) -> str: """ Many USSD responses include user-specific data that is stored inside the USSD session. This function extracts the appropriate session data based on the current menu name and then inserts them as keywords in the i18n function. :param menu: The USSD menu to create a text response for :param ussd_session: The ussd session containing user data :return: raw ussd menu text string """ user = ussd_session.user if menu.name == 'about_my_business': bio = next( filter(lambda x: x.name == 'bio', user.custom_attributes), None) if bio: bio_text = bio.value.strip('"') else: bio_text = None if bio_text is None or '': return i18n_for(user, "{}.none".format(menu.display_key)) else: return i18n_for(user, "{}.bio".format(menu.display_key), user_bio=bio_text) if menu.name == 'send_token_confirmation': recipient = get_user_by_phone( ussd_session.get_data('recipient_phone'), should_raise=True) recipient_phone = recipient.user_details() token = default_token(user) transaction_amount = ussd_session.get_data('transaction_amount') transaction_reason = ussd_session.get_data( 'transaction_reason_i18n') return i18n_for( user, menu.display_key, recipient_phone=recipient_phone, token_name=token.symbol, transaction_amount=cents_to_dollars(transaction_amount), transaction_reason=transaction_reason) if menu.name == 'exchange_token_confirmation': agent = get_user_by_phone(ussd_session.get_data('agent_phone'), should_raise=True) agent_phone = agent.user_details() token = default_token(user) exchange_amount = ussd_session.get_data('exchange_amount') return i18n_for(user, menu.display_key, agent_phone=agent_phone, token_name=token.symbol, exchange_amount=cents_to_dollars(exchange_amount)) # in matching is scary since it might pick up unintentional ones if 'exit' in menu.name or 'help' == menu.name: return i18n_for(user, menu.display_key, support_phone='+254757628885') # in matching is scary since it might pick up unintentional ones if 'pin_authorization' in menu.name or 'current_pin' == menu.name: if user.failed_pin_attempts is not None and user.failed_pin_attempts > 0: return i18n_for(user, "{}.retry".format(menu.display_key), remaining_attempts=3 - user.failed_pin_attempts) else: return i18n_for(user, "{}.first".format(menu.display_key)) if menu.name == 'directory_listing' or menu.name == 'send_token_reason': blank_template = i18n_for(user, menu.display_key, options='') blank_len = len(blank_template) most_relevant_usages = ussd_session.get_data( 'transfer_usage_mapping') options = UssdProcessor.fit_usages(ussd_session, most_relevant_usages, blank_len, user, 0, [0]) # current_usages = most_relevant_usages[:ITEMS_PER_MENU] return i18n_for(user, menu.display_key, options=options) if menu.name == 'directory_listing_other' or menu.name == 'send_token_reason_other': most_relevant_usages = ussd_session.get_data( 'transfer_usage_mapping') usage_menu_nr = ussd_session.get_data('usage_menu') usage_stack = ussd_session.get_data('usage_index_stack') or [0] start_of_list = usage_stack[usage_menu_nr] total_usages = len(most_relevant_usages) # First see if we can fit remaining usages onto the one page if start_of_list + ITEMS_PER_MENU > total_usages: part = 'first' if start_of_list == 0 else 'last' current_usages = most_relevant_usages[ start_of_list:total_usages] menu_options = UssdProcessor.create_usages_list( current_usages, user) translated_menu = i18n_for(user, "{}.{}".format( menu.display_key, part), other_options=menu_options) if len(translated_menu) <= USSD_MAX_LENGTH: return translated_menu # Oh well, guess we just have to fit as many as possible then part = 'first' if start_of_list == 0 else 'middle' blank_template = i18n_for(user, "{}.{}".format(menu.display_key, part), other_options='') blank_len = len(blank_template) options = UssdProcessor.fit_usages(ussd_session, most_relevant_usages, blank_len, user, start_of_list, usage_stack) # current_usages = most_relevant_usages[:ITEMS_PER_MENU] return i18n_for(user, "{}.{}".format(menu.display_key, part), other_options=options) return i18n_for(user, menu.display_key)
def custom_display_text(menu: UssdMenu, ussd_session: UssdSession) -> str: """ Many USSD responses include user-specific data that is stored inside the USSD session. This function extracts the appropriate session data based on the current menu name and then inserts them as keywords in the i18n function. :param menu: The USSD menu to create a text response for :param ussd_session: The ussd session containing user data :return: raw ussd menu text string """ user = ussd_session.user if menu.name == 'about_me': bio = next(filter(lambda x: x.name == 'bio', user.custom_attributes), None) first_name = user.first_name last_name = user.last_name gender = next(filter(lambda x: x.name == 'gender', user.custom_attributes), None) location = user.location if bio and gender: bio_text = bio.value.strip('"') gender_text = gender.value.strip('"') else: bio_text = None gender_text = None # translations absent_value_placeholder = "missing" if user.preferred_language == "sw": absent_value_placeholder = 'hakuna' if gender_text == 'male': gender_text = 'mwanaume' elif gender_text == 'female': gender_text = 'mwanamke' if first_name == 'Unknown first name': first_name = None if last_name == 'Unknown last name': last_name = None if bio_text == 'Unknown business': bio_text = None if gender_text == 'Unknown gender': gender_text = None if location == 'Unknown location': location = None # define final values to show in menu first_name = first_name or absent_value_placeholder last_name = last_name or absent_value_placeholder bio_text = bio_text or absent_value_placeholder gender_text = gender_text or absent_value_placeholder location = location or absent_value_placeholder full_name = "{} {}".format(first_name, last_name) if first_name == absent_value_placeholder and last_name == absent_value_placeholder: full_name = absent_value_placeholder return i18n_for(user, "{}.profile".format(menu.display_key), full_name=full_name, gender=gender_text, location=location, user_bio=bio_text) if menu.name == 'send_token_pin_authorization': recipient = get_user_by_phone(ussd_session.get_data('recipient_phone'), 'KE', True) other_user_details = recipient.user_details() user_details = user.user_details() token = default_token(user) transaction_amount = ussd_session.get_data('transaction_amount') if user.failed_pin_attempts > 0: return i18n_for( user=user, key="{}.{}".format(menu.display_key, 'retry'), remaining_attempts=3 - user.failed_pin_attempts ) else: return i18n_for( user=user, key="{}.{}".format(menu.display_key, 'first'), transaction_amount=cents_to_dollars(transaction_amount), token_name=token.symbol, other_user_details=other_user_details, user_details=user_details ) if menu.name == 'exit_successful_send_token': recipient = get_user_by_phone(ussd_session.get_data('recipient_phone'), 'KE', True) other_user_details = recipient.user_details() user_details = user.user_details() token = default_token(user) transaction_amount = ussd_session.get_data('transaction_amount') return i18n_for( user=user, key="{}".format(menu.display_key, 'exit_successful_send_token'), transaction_amount=cents_to_dollars(transaction_amount), token_name=token.symbol, other_user_details=other_user_details, user_details=user_details ) if menu.name == 'exchange_token_confirmation': agent = get_user_by_phone(ussd_session.get_data('agent_phone'), 'KE', True) agent_phone = agent.user_details() token = default_token(user) exchange_amount = ussd_session.get_data('exchange_amount') return i18n_for( user, menu.display_key, agent_phone=agent_phone, token_name=token.symbol, exchange_amount=cents_to_dollars(exchange_amount) ) # in matching is scary since it might pick up unintentional ones if 'exit' in menu.name or 'help' == menu.name: return i18n_for( user, menu.display_key, support_phone='+254757628885' ) # in matching is scary since it might pick up unintentional ones if 'pin_authorization' in menu.name or 'current_pin' == menu.name: if user.failed_pin_attempts is not None and user.failed_pin_attempts > 0: return i18n_for( user, "{}.retry".format(menu.display_key), remaining_attempts=3 - user.failed_pin_attempts ) else: return i18n_for(user, "{}.first".format(menu.display_key)) if menu.name == 'directory_listing' or menu.name == 'send_token_reason': blank_template = i18n_for( user, menu.display_key, options='' ) blank_len = len(blank_template) most_relevant_usages = ussd_session.get_data('transfer_usage_mapping') options = KenyaUssdProcessor.fit_usages( ussd_session, most_relevant_usages, blank_len, user, 0, [0] ) # current_usages = most_relevant_usages[:ITEMS_PER_MENU] return i18n_for( user, menu.display_key, options=options ) if menu.name == 'directory_listing_other' or menu.name == 'send_token_reason_other': most_relevant_usages = ussd_session.get_data('transfer_usage_mapping') usage_menu_nr = ussd_session.get_data('usage_menu') usage_stack = ussd_session.get_data('usage_index_stack') or [0] start_of_list = usage_stack[usage_menu_nr] total_usages = len(most_relevant_usages) # First see if we can fit remaining usages onto the one page if start_of_list + ITEMS_PER_MENU > total_usages: part = 'first' if start_of_list == 0 else 'last' current_usages = most_relevant_usages[start_of_list:total_usages] menu_options = KenyaUssdProcessor.create_usages_list(current_usages, user) translated_menu = i18n_for( user, "{}.{}".format(menu.display_key, part), other_options=menu_options ) if len(translated_menu) <= USSD_MAX_LENGTH: return translated_menu # Oh well, guess we just have to fit as many as possible then part = 'first' if start_of_list == 0 else 'middle' blank_template = i18n_for( user, "{}.{}".format(menu.display_key, part), other_options='' ) blank_len = len(blank_template) options = KenyaUssdProcessor.fit_usages( ussd_session, most_relevant_usages, blank_len, user, start_of_list, usage_stack) # current_usages = most_relevant_usages[:ITEMS_PER_MENU] return i18n_for( user, "{}.{}".format(menu.display_key, part), other_options=options ) return i18n_for(user, menu.display_key)