def archive(self, request, pk=None, **kwargs): """ Override this method as we don't want to actually delete the goal, just disable it. :param instance: The goal to disable :return: None """ goal = self.get_object() # If I'm an advisor or the goal is unsupervised, # archive the goal immediately. sr = SupportRequest.get_current(request, as_obj=True) # check helped user instead if support request is active user, sr_id = (sr.user, sr.id) if sr else (request.user, None) if not goal.account.supervised or user.is_advisor: check_state(Goal.State(goal.state), [ Goal.State.ACTIVE, Goal.State.ARCHIVE_REQUESTED, Goal.State.INACTIVE ]) Event.ARCHIVE_GOAL.log('{} {}'.format(request.method, request.path), user=request.user, obj=goal, support_request_id=sr_id) # Set the state to archive requested, # as the call to archive() requires it. goal.state = Goal.State.ARCHIVE_REQUESTED.value goal.archive() else: # I'm a client with a supervised goal, just change the status to # ARCHIVE_REQUESTED, and add a notification check_state(Goal.State(goal.state), [Goal.State.ACTIVE, Goal.State.INACTIVE]) Event.ARCHIVE_GOAL_REQUESTED.log('{} {}'.format( request.method, request.path), user=request.user, obj=goal, support_request_id=sr_id) # Flag the goal as archive requested. goal.state = Goal.State.ARCHIVE_REQUESTED.value # TODO: Add a notification to the advisor that the goal is archive requested. goal.save() return Response(serializers.GoalSerializer(goal).data)
def calculate_all_portfolios(self, request, pk=None, **kwargs): """ Called to calculate all the portfolio objects for 100 different risk scores for the supplied settings. """ goal = self.get_object() check_state(Goal.State(goal.state), Goal.State.ACTIVE) setting_str = request.query_params.get('setting', None) if not setting_str: logger.debug( 'setting parameter missing from calculate_all_portfolios query' ) raise ValidationError( "Query parameter 'setting' must be specified and a valid JSON string" ) try: setting = ujson.loads(setting_str) except ValueError: logger.debug( 'setting parameter for calculate_all_portfolios query not valid json' ) raise ValidationError( "Query parameter 'setting' must be a valid json string") # Create the settings from the dict serializer = serializers.GoalSettingStatelessSerializer(data=setting) serializer.is_valid(raise_exception=True) settings = serializer.create_stateless(serializer.validated_data, goal) # Calculate the portfolio try: data_provider = DataProviderDjango() execution_provider = ExecutionProviderDjango() data = [ self.build_portfolio_data(item[1], item[0]) for item in calculate_portfolios(settings=settings, data_provider=data_provider, execution_provider=execution_provider) ] return Response(data) except Unsatisfiable as e: rdata = {'reason': "No portfolio could be found: {}".format(e)} if e.req_funds is not None: rdata['req_funds'] = e.req_funds return Response({'error': rdata}, status=status.HTTP_400_BAD_REQUEST)
def revert_selected(self, request, pk=None, **kwargs): """ Called to revert the current selected-settings to the approved-settings Returns a validation error if there is no approved-settings. """ goal = self.get_object() check_state(Goal.State(goal.state), Goal.State.ACTIVE) if not goal.approved_settings: raise ValidationError("No settings have yet been approved for " "this Goal, cannot revert to last approved.") sr_id = SupportRequest.get_current(request) Event.REVERT_SELECTED_SETTINGS.log('{} {}'.format( request.method, request.path), user=request.user, obj=goal, support_request_id=sr_id) goal.revert_selected() serializer = serializers.GoalSettingSerializer(goal.selected_settings) return Response(serializer.data)
def calculate_portfolio(self, request, pk=None, **kwargs): """ Called to calculate a portfolio object for a set of supplied settings. """ goal = self.get_object() check_state(Goal.State(goal.state), [Goal.State.ACTIVE, Goal.State.INACTIVE]) setting_str = request.query_params.get('setting', None) if not setting_str: raise ValidationError( "Query parameter 'setting' must be specified and a valid JSON string" ) try: setting = ujson.loads(setting_str) except ValueError: raise ValidationError( "Query parameter 'setting' must be a valid json string") # Create the settings object from the dict serializer = serializers.GoalSettingStatelessSerializer(data=setting) serializer.is_valid(raise_exception=True) settings = serializer.create_stateless(serializer.validated_data, goal) try: data = self.build_portfolio_data( calculate_portfolio( settings=settings, data_provider=DataProviderDjango(), execution_provider=ExecutionProviderDjango())) return Response(data) except Unsatisfiable as e: rdata = {'reason': "No portfolio could be found: {}".format(e)} if e.req_funds is not None: rdata['req_funds'] = e.req_funds return Response({'error': rdata}, status=status.HTTP_400_BAD_REQUEST)
def approve_selected(self, request, pk=None, **kwargs): """ Called to make the currently selected settings approved by the advisor, ready to be activated next time the account is processed (rebalance). """ sr = SupportRequest.get_current(request, as_obj=True) # check helped user instead if support request is active user, sr_id = (sr.user, sr.id) if sr else (request.user, None) if not user.is_advisor: raise PermissionDenied('Only an advisor can approve selections.') goal = self.get_object() check_state(Goal.State(goal.state), [Goal.State.ACTIVE, Goal.State.INACTIVE]) Event.APPROVE_SELECTED_SETTINGS.log('{} {}'.format( request.method, request.path), user=request.user, obj=goal, support_request_id=sr_id) goal.approve_selected() serializer = serializers.GoalSettingSerializer(goal.approved_settings) return Response(serializer.data)
def withdraw(self, request, pk=None, **kwargs): goal = self.get_object() check_state(Goal.State(goal.state), Goal.State.ACTIVE) client = goal.account.primary_owner site = get_current_site(request) target_user = client.user sr_id = SupportRequest.get_current(request) if site.config().plaid_enabled: if not hasattr(target_user, 'invitation'): logger.error('Django user missing invitation') raise ValidationError( 'Client missing invitation, needed for withdrawals') else: if not hasattr(target_user.invitation, 'photo_verification'): logger.error( 'Django user missing invitation.photo_verification') raise ValidationError( 'Client missing invitation.photo_verification, needed for withdrawals' ) serializer = serializers.TransactionCreateSerializer(data=request.data) serializer.is_valid(raise_exception=True) # Make sure the total amount for the goal is larger than the pending withdrawal amount. if goal.current_balance + 0.0000001 < serializer.validated_data[ 'amount']: emsg = "Goal's current balance: {} is less than the desired withdrawal amount: {}" raise SystemConstraintError( emsg.format(goal.current_balance, serializer.validated_data['amount'])) if site.config().plaid_enabled: # make sure user has connected a plaid_user plaid_user = getattr(target_user, 'plaid_user', False) if not plaid_user: logger.error( 'Withdrawal attempted by %s but no plaid_user found' % target_user) raise ValidationError('Client has no plaid_user linked') if not serializer.validated_data.get('account_id', False): logger.error( 'No plaid account_id sent with withdrawal request') raise ValidationError( 'No plaid account_id sent with withdrawal request') transaction = serializer.save(from_goal=goal, reason=Transaction.REASON_WITHDRAWAL) if site.config().plaid_enabled: # execute transaction through stripe logger.info('Executing valid withdrawal request from %s for %s' % (target_user, serializer.validated_data['amount'])) stripe_token = get_stripe_account_token( target_user, serializer.validated_data['account_id']) try: transfer = execute_withdrawal( target_user, serializer.validated_data['amount'], stripe_token) if not transfer: logger.error( 'Failed to execute withdrawal, marking Transaction %s failed' % transaction) transaction.status = Transaction.STATUS_FAILED transaction.save() return Response('Failed to charge account', status=status.HTTP_400_BAD_REQUEST) transaction.stripe_id = transfer.id transaction.save() except Exception as e: logger.error( 'Failed to execute withdrawal, marking Transaction %s failed' % transaction) logger.error(e) transaction.status = Transaction.STATUS_FAILED transaction.save() return Response('Failed to charge account', status=status.HTTP_400_BAD_REQUEST) else: # manual transfer, goal cash balance updated directly transaction.execute() Event.GOAL_WITHDRAWAL.log('{} {}'.format(request.method, request.path), request.data, user=request.user, obj=goal, support_request_id=sr_id) headers = self.get_success_headers(serializer.data) return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
def selected_settings(self, request, pk=None, **kwargs): goal = self.get_object() if request.method == 'GET': serializer = serializers.GoalSettingSerializer( goal.selected_settings) return Response(serializer.data) sr_id = SupportRequest.get_current(request) data = request.data roa = None if request.method == 'POST': with transaction.atomic( ): # So both the log and change get committed. check_state(Goal.State(goal.state), [Goal.State.ACTIVE, Goal.State.INACTIVE]) # If this field is specified and it is an update from an advisor, we create a record of advice. if 'record_of_advice' in request.data: roa_data = data.pop('record_of_advice') roa_ser = serializers.RecordOfAdviceSerializer( data=roa_data) roa_ser.initial_data['goal'] = goal.id roa_ser.is_valid(raise_exception=True) roa = roa_ser.save() serializer = serializers.GoalSettingWritableSerializer( data=data, context={'request': request}) serializer.is_valid(raise_exception=True) event = Event.SET_SELECTED_SETTINGS.log( '{} {}'.format(self.request.method, self.request.path), request.data, user=request.user, obj=goal, support_request_id=sr_id) settings = serializer.save(goal=goal) if roa: roa.send_roa_generated_email() # We use the read-only serializer to send the settings object, not the update serializer. serializer = serializers.GoalSettingSerializer(settings) headers = self.get_success_headers(serializer.data) return Response(serializer.data, status=status.HTTP_200_OK, headers=headers) elif request.method == 'PUT': with transaction.atomic( ): # So both the log and change get committed. check_state(Goal.State(goal.state), [Goal.State.ACTIVE, Goal.State.INACTIVE]) # If this field is specified and it is an update from an advisor, we create a record of advice. if 'record_of_advice' in request.data: roa_data = data.pop('record_of_advice') roa_ser = serializers.RecordOfAdviceSerializer( data=roa_data, context={'request': request}) roa_ser.initial_data['goal'] = goal.id roa_ser.is_valid(raise_exception=True) roa = roa_ser.save() settings = goal.selected_settings event = Event.UPDATE_SELECTED_SETTINGS.log( '{} {}'.format(self.request.method, self.request.path), request.data, user=request.user, obj=goal, support_request_id=sr_id) serializer = serializers.GoalSettingWritableSerializer( settings, data=data, partial=True) serializer.is_valid(raise_exception=True) settings = serializer.save(goal=goal) if roa: roa.send_roa_generated_email() # We use the read-only serializer to send the settings object, not the update serializer. serializer = serializers.GoalSettingSerializer(settings) headers = self.get_success_headers(serializer.data) return Response(serializer.data, status=status.HTTP_200_OK, headers=headers)