Add a Goal Onboarding


Contents

Overview

This feature allows the user to create their first goals. They have the liberty to set up various types of goals to suit their needs, including:

  • Financial Freedom Goal: A long term system goal to help them plan and save towards achieving financial independence.
  • Emergency Fund Goal: A short term system goal to help them prepare for unexpected expenses by setting aside funds for emergencies.
  • Personal Goal: Custom goals to help them track any personal financial objectives they may have.

Financial Freedom

Selecting financial freedom takes the user to what their App\Models\FinancialFreedom model looks like. Once they get an overview of what the model looks like, they can then proceed to view the results of their financial freedom goal. /api:IaYS-M9j/financial_freedom_results is the endpoint that will be used to get the results of the financial freedom goal and it runs the App\Http\Controllers\Onboarding\OnboardingController@financialFreedomResults method that returns the results of the financial freedom goal.

API Response

{
    "result_1": {
        "id": 1234,
        "users_id": 5305,
        "income": 100000,
        "years": {
            "nper_no_ap": 25.9,
            "nper_ap": 14.9,
            "difference": 11
        },
        "age": {
            "age_ap": 37,
            "age_ap_desc": "37 years old",
            "age_no_ap": 48,
            "age_no_ap_desc": "48 years old",
            "age_today": 21.84931506870732
        },
        "savings": {
            "savings": 55000,
            "savings_rate": 0.55
        },
        "ffnumber": {
            "ffnumber": 30000000,
            "ffnumber_desc": "30,000,000.00"
        },
        "created_at": "2025-01-24T03:24:04.000000Z",
        "updated_at": "2025-03-07T02:52:37.000000Z"
    },
    "response": {
        "income": "100,000",
        "savings": "55,000",
        "savingsrate": 0.55
    }
}

/api:IaYS-M9j/financial_freedom/milestones is also hit to get the milestones of the financial freedom goal. It runs the App\Http\Controllers\Onboarding\OnboardingController@getUserMilestone method that returns the milestones of the financial freedom goal.

API Response

{
    "0": {
        "age": 21,
        "amount": "0.00"
    },
    "25": {
        "age": 25,
        "amount": "7,500,000.00"
    },
    "50": {
        "age": 29,
        "amount": "15,000,000.00"
    },
    "75": {
        "age": 33,
        "amount": "22,500,000.00"
    },
    "100": {
        "age": 37,
        "amount": "30,000,000.00"
    }
}

The next step guides the user in what they need to do to achieve their financial freedom goal. In the hood /api:IaYS-M9j/create_financial_freedom_goal, /api:IaYS-M9j/financial_freedom/config and /api:IaYS-M9j/calculator/financial-freedom?date_of_birth=01%2F05%2F2003&user_income=100000&user_monthly_savings=55000 are hit to setup the financial freedom goal.

  • /api:IaYS-M9j/create_financial_freedom_goal runs the App\Http\Controllers\Onboarding\OnboardingController@createFinancialFreedomGoal method that creates the financial freedom goal from the user's Financial Freedom model.
  • /api:IaYS-M9j/financial_freedom/config runs the App\Http\Controllers\Onboarding\OnboardingController@financialFreedomConfig method that returns the configuration of the financial freedom model by running the App\Actions\FinancialFreedom\GetFinancialFreedomConfigAction action.
  • /api:IaYS-M9j/calculator/financial-freedom?date_of_birth=01%2F05%2F2003&user_income=100000&user_monthly_savings=55000 runs the App\Http\Controllers\Onboarding\OnboardingController@financialFreedomCalculator method that calculates the financial freedom goal by running the App\Actions\Calculators\FinancialFreedomCalculatorAction action.

The user has an potion to either proceed with the set amount to be contributed monthly or update it. This hits the /api:IaYS-M9j/update_stage endpoint that runs the App\Http\Controllers\Onboarding\OnboardingController@updateStage method to update the stage of the user by running the App\Actions\Auth\UpdateUserStage action. The purpose of updating the stage is to keep track of the user's progress in the onboarding process. This will allow them to get directly to the dashboard when they log in next time.

Emergency Fund

Selecting emergency fund has two steps. The first asks the user how much they have saved so far. Submitting this hits /api:IaYS-M9j/emergency_fund that runs the App\Http\Controllers\Goals\GoalsController@emergencyFund

public function emergencyFund(Request $request) {

    $validator = Validator::make($request->all(), [
        'savings_user' => ['required', 'numeric'],
        'timeline' => ['sometimes', 'numeric']
    ]);

    if ($validator->fails()) {
        return RespondWithError::make()->handle(
            message: $validator->errors()->first(),
            payload: $validator->errors()->all()
        );
    }
    $user_current_savings = floatval($request->input('savings_user'));
    $timeline = intval($request->input('timeline', 6));
    $monthly_allocation = GetUserMonthlyGoalsAllocation::make()->handle(Auth::id());

    return EmergencyFundCalculator::make()->handle(
        monthly_allocation: $monthly_allocation, user_id: Auth::id(), amount_saved_so_far: $user_current_savings, timeline: $timeline
    );

}

The above code validates the user input and calculates the emergency fund goal. The App\Actions\Calculators\EmergencyFundCalculator action is responsible for calculating the emergency fund goal.

{
    "emergency_fund": "270,000.00",
    "savings": "55,000.00",
    "target": 270000,
    "is_achieved": false,
    "timeline": 6,
    "timeline_text": "6 months",
    "amount": "0",
    "allocation": 148979,
    "allocation_percent": "270.871",
    "time_to_target": "2 months",
    "time_to_target_date": "May 2025"
}

The second step asks the user to confirm the amount they want to save monthly. Submitting this hits /api:IaYS-M9j/emergency_fund/update that runs the App\Http\Controllers\Goals\GoalsController@emergencyFundUpdate method.

public function emergencyFundUpdate(Request $request) {
    $validator = Validator::make($request->all(), [
        'savings_user' => ['required', 'numeric'],
        'saved_so_far' => ['sometimes', 'numeric'],
        'timeline' => ['sometimes', 'numeric']
    ]);

    if ($validator->fails()) {
        return RespondWithError::make()->handle(code: RespondWithError::$ERROR_CODE_BAD_REQUEST,
            message: $validator->errors()->first(),
            payload: $validator->errors()->all(), status: 422);
    }
    $saved_so_far = floatval($request->input('saved_so_far', 0));
    $user_monthly_allocation = floatval($request->input('savings_user'));
    $timeline = intval($request->input('timeline', 6));

    $response = EmergencyFundCalculator::make()->handle(
        monthly_allocation: $user_monthly_allocation,
        user_id: Auth::id(),
        amount_saved_so_far: $saved_so_far, timeline: $timeline
    );
    CreateEmergencyFundGoal::make()->handle(
        target: $response['target'],
        monthly_allocation: $user_monthly_allocation,
        user_id: Auth::id()
    );
    return RespondWithSuccess::make()->handle($response);
}

The above code validates the user input and creates the emergency fund goal. The App\Actions\Goals\CreateEmergencyFundGoal action is responsible for creating the emergency fund goal.

Custom Goals

Custom goals have two steps. The first asks the user to enter the title of the goal, the category, the goal target amount, how much they have saved so far, and when they want to achieve the goal. Submitting this hits /api:IaYS-M9j/get_suggested_monthly_savings that runs the App\Http\Controllers\Goals\GoalsController@getSuggestedMonthlySavings method.

public function getSuggestedMonthlySavings(Request $request)
{
    $validator = Validator::make($request->all(), [
        'date_achieve' => ['required', 'string', 'max:7', 'min:7'],
        'goal_cost' => ['required', 'numeric'],
        'goal_saved' => ['required', 'numeric'],
    ]);
    if ($validator->fails()) {
        return RespondWithError::make()->handle(
            message: $validator->errors()->first(),
            payload: $validator->errors()->all(), status: 422
        );
    }

    $date = Carbon::createFromFormat('m/Y', $request->input('date_achieve'));

    if ($date < now()) {

        return RespondWithError::make()->handle(
            message: "Please select a future date",
            status: 422
        );
    }
    $target = BigNumber::of($request->input('goal_cost'))
        ->minus($request->input('goal_saved', 0))->toFloat();

    $pmt = CalculatePMT::make()->handle(
        nper: abs($date->floatDiffInMonths()),
        interest_rate: InvestmentPool::$LONG_TERM_INTEREST_RATE,
        target: $target,
        current_value: 0,
        retry_with_lower_rate: true
    );

    return RespondWithSuccess::make()->handle([
        'amount' => BigNumber::of($pmt)->toScale(0, RoundingMode::UP),
        'target' => $target,
        'date' => [
            'short' => $date->format('F Y'),
            'full' => $date->format('d/m/Y')
        ],
        'text' => [
            'amount' => number_format($pmt)
        ]
    ]);

}

The above code validates the user input and calculates the suggested monthly savings. The App\Actions\Calculators\CalculatePMT action is responsible for calculating the suggested monthly savings.

{
    "amount": "1,000",
    "target": 100000,
    "date": {
        "short": "January 2025",
        "full": "01/01/2025"
    },
    "text": {
        "amount": "1,000"
    }
}

The second step asks the user to confirm the amount they want to save monthly. Submitting this hits /api:IaYS-M9j/goal_calculator that runs the App\Http\Controllers\Goals\GoalsController@goalCalculator method.

public function goalCalculator(Request $request)
{
    $validator = Validator::make($request->all(), [
        'target' => ['required', 'numeric', 'min:1000'],
        'monthly_savings' => ['required', 'numeric'],
        'name' => ['required', 'string'],
        'category_id' => ['required', 'int', 'exists:syst_goal_types,id'],
        'parent_goal_id' => ['sometimes', 'int'],
    ]);

    if ($validator->fails()) {
        return RespondWithError::make()->handle(
            message: $validator->errors()->first(), payload: $validator->errors()->all(), status: 422
        );
    }

    $financial_freedom = FinancialFreedom::query()->where('users_id', Auth::id())->first();
    $monthly_savings = BigDecimal::of($request->input('monthly_savings'));
    $allocation = $monthly_savings->dividedBy($financial_freedom->savings->savings, 3, RoundingMode::UP)
        ->toScale(0, RoundingMode::CEILING)->toFloat();

    $payload = [
        'users_id' => \Auth::id(),
        'sys_goal_type_id' => $request->input('category_id'),
        'title' => $request->input('name'),
        'target' => $request->input('target'),
        'status' => TransactionStatusEnum::PENDING->value,
        'allocation' => $allocation,
        'units' => 0,
        'principle' => 0,
        'value' => 0,
        'is_achieved' => false,
        'is_wishlist' => false,
        'parent_goal_id' => null,
        'savings' => [
            'available' => $monthly_savings->toScale(0, RoundingMode::UP), //Will be removed in the next phase
            'total' => $financial_freedom->savings->savings,
        ],
    ];

    try {
        $goal = Goal::query()->create($payload);
        GoalClassifierAction::run($goal->id);

        return [
            'goal' => $goal
        ];
    } catch (Exception $exception) {

        Log::channel('slack')->emergency("User cannot set a goal: {$exception->getMessage()}", $payload);

        return RespondWithError::make()->handle(
            message: "Unable to save a goal. We will get back to you when it has been resolved",
        );

    }
}

The above code validates the user input and creates the custom goal. The App\Actions\Goals\GoalClassifierAction action is responsible for classifying the custom goal.

{
    "goal": {
        "id": 1234,
        "users_id": 5305,
        "sys_goal_type_id": 1,
        "title": "My Custom Goal",
        "target": 100000,
        "status": "pending",
        "allocation": 2,
        "units": 0,
        "principle": 0,
        "value": 0,
        "is_achieved": false,
        "is_wishlist": false,
        "parent_goal_id": null,
        "savings": {
            "available": "1,000",
            "total": 55000
        },
        "created_at": "2025-01-24T03:24:04.000000Z",
        "updated_at": "2025-03-07T02:52:37.000000Z"
    }
}

All the above steps are part of the onboarding process that allows the user to create their first goals. The user proceeds to the onboarding/your-plan page after creating their goals. Which leads them to the next final step of the onboarding process, which is the onboarding/make-your-first-investment page.

Note that the functionality of onboarding/your-plan and onboarding/make-your-first-investment pages are not covered in this document. This is because they are similar to the functionality of the app/your-plan and app/make-your-first-investment pages respectively.