Skip to content

Tutorial ‐ Part 2

Isaac Sai edited this page Dec 28, 2023 · 4 revisions

Let us create a model to save customer details.

php artisan make:model Customer -mf

Let us create a model to save account details.

php artisan make:model Account -mf

Edit the generated migration file database/migrations/****_create_cutomers_table to look like this:

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    /**
     * Run the migrations.
     */
    public function up(): void
    {
        Schema::create('customers', function (Blueprint $table) {
            $table->id();
            $table->string('full_name');
            $table->string('phone_number')->unique();
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     */
    public function down(): void
    {
        Schema::dropIfExists('customers');
    }
};

Edit app/Models/Customer.php to look like this:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class Customer extends Model
{
    use HasFactory;

    protected $fillable = ['full_name', 'phone_number'];

    public function accounts()
    {
        return $this->hasMany(Account::class);
    }
}

Edit the generated migration file database/migrations/****_create_accounts_table to look like this:

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    /**
     * Run the migrations.
     */
    public function up(): void
    {
        Schema::create('accounts', function (Blueprint $table) {
            $table->id();
            $table->string('type')->default('savings');
            $table->decimal('balance')->default(0);
            $table->foreignId('customer_id')->constrained();
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     */
    public function down(): void
    {
        Schema::dropIfExists('accounts');
    }
};

Edit app/Models/Account.php to look like this:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class Account extends Model
{
    use HasFactory;

    protected $fillable = ['type', 'phone_number'];

    public function customer()
    {
        return $this->belongsTo(Customer::class);
    }
}

Now let create a new state to get the customer name, and account type and show registration completion.

php artisan ussd:state RegistrationCustomerNameState
php artisan ussd:state RegistrationAccountTypeState
php artisan ussd:state RegistrationSucceededState

Edit the generated file app/Ussd/States/RegistrationCustomerNameState.php to look like

<?php

namespace App\Ussd\States;

use Sparors\Ussd\Attributes\Transition;
use Sparors\Ussd\Context;
use Sparors\Ussd\Contracts\State;
use Sparors\Ussd\Decisions\Regex;
use Sparors\Ussd\Menu;
use Sparors\Ussd\Record;

#[Transition(RegistrationAccountTypeState::class, new Regex('/^[a-zA-Z\s]+$/'), [self::class, 'callback'])]
class RegistrationCustomerNameState implements State
{
    public function render(): Menu
    {
        return Menu::build()->text('What is your full name?');
    }

    public function callback(Context $context, Record $record)
    {
        $record->set('full_name', $context->input());
    }
}

Edit the generated file app/Ussd/States/RegistrationAccountTypeState.php to look like

<?php

namespace App\Ussd\States;

use App\Models\Customer;
use Illuminate\Support\Facades\DB;
use Sparors\Ussd\Attributes\Transition;
use Sparors\Ussd\Context;
use Sparors\Ussd\Contracts\State;
use Sparors\Ussd\Decisions\Between;
use Sparors\Ussd\Menu;
use Sparors\Ussd\Record;

#[Transition(RegistrationSucceededState::class, new Between(1, 2), [self::class, 'callback'])]
class RegistrationAccountTypeState implements State
{
    public function render(): Menu
    {
        return Menu::build()
            ->line('Account Type')
            ->listing([
                'Savings',
                'Current',
            ]);
    }

    public function callback(Context $context, Record $record)
    {
        $accountType = '1' === $context->input() ? 'savings' : 'current';
        $fullName = $record->get('full_name');
        $phoneNumber = $context->get('phone_number');

        DB::transaction(function () use ($accountType, $fullName, $phoneNumber) {
           $customer = Customer::query()->create(['full_name' => $fullName, 'phone_number' => $phoneNumber]);
           $customer->accounts()->create(['type' => $accountType]);
        }, 5);
    }
}

Edit the generated file app/Ussd/States/RegistrationSucceededState.php to look like

<?php

namespace App\Ussd\States;

use Sparors\Ussd\Attributes\Terminate;
use Sparors\Ussd\Contracts\State;
use Sparors\Ussd\Menu;

#[Terminate]
class RegistrationSucceededState implements State
{
    public function render(): Menu
    {
        return Menu::build()
            ->line('Banc')
            ->lineBreak()
            ->text('Thank you for registering with us.');
    }
}

Finally edit app/Ussd/States/GuestMenuState.php to look like this:

<?php

namespace App\Ussd\States;

use Sparors\Ussd\Attributes\Transition;
use Sparors\Ussd\Contracts\InitialState;
use Sparors\Ussd\Decisions\Equal;
use Sparors\Ussd\Menu;

#[Transition(RegistrationCustomerNameState::class, new Equal(1))]
#[Transition(HelplineState::class, new Equal(2))]
class GuestMenuState implements InitialState
{
    public function render(): Menu
    {
        return Menu::build()
            ->line('Banc')
            ->listing([
                'Register',
                'Helpline',
            ])
            ->text('Powered by Sparors');
    }
}

Although we have already registered, when we start the process, it going to ask us to register. Let fix that.

Let create a menu for registered customers.

php artisan ussd:state CustomerMenuState

Edit the generated file app/Ussd/States/CustomerMenuState.php to look like

<?php

namespace App\Ussd\States;

use Sparors\Ussd\Contracts\State;
use Sparors\Ussd\Menu;

class CustomerMenuState implements State
{
    public function render(): Menu
    {
        return Menu::build()
            ->line('Banc')
            ->listing([
                'Transfer',
                'Deposit',
                'Withdraw',
                'New Account',
                'Helpline',
            ])
            ->text('Powered by Sparors');
    }
}

Let us make an action to decide which menu should be shown to the user

php artisan make:action MenuAction --init

Edit the generated file app/Ussd/Actions/MenuAction.php to look like this

<?php

namespace App\Ussd\Actions;

use App\Models\Customer;
use App\Ussd\States\CustomerMenuState;
use App\Ussd\States\GuestMenuState;
use Sparors\Ussd\Context;
use Sparors\Ussd\Contracts\InitialAction;

class MenuAction implements InitialAction
{
    public function execute(Context $context): string
    {
        $isRegistered = Customer::query()
            ->where('phone_number', $context->get('phone_number'))
            ->exists();

        return $isRegistered ? CustomerMenuState::class : GuestMenuState::class;
    }
}

Now edit app/Ussd/States/GuestMenuState.php to

<?php

namespace App\Ussd\States;

use Sparors\Ussd\Attributes\Transition;
use Sparors\Ussd\Contracts\State;
use Sparors\Ussd\Decisions\Equal;
use Sparors\Ussd\Menu;

#[Transition(RegistrationCustomerNameState::class, new Equal(1))]
#[Transition(HelplineState::class, new Equal(2))]
class GuestMenuState implements State
{
    public function render(): Menu
    {
        return Menu::build()
            ->line('Banc')
            ->listing([
                'Register',
                'Helpline',
            ])
            ->text('Powered by Sparors');
    }
}

Now edit app/Http/Controllers/UssdController.php to

<?php

namespace App\Http\Controllers;

use App\Ussd\Actions\MenuAction;
use App\Ussd\Responses\AfricasTalkingResponse;
use Illuminate\Http\Request;
use Sparors\Ussd\Context;
use Sparors\Ussd\Ussd;

class UssdController extends Controller
{
    /**
     * Handle the incoming request.
     */
    public function __invoke(Request $request)
    {
        $lastText = $request->input('text') ?? '';

        if (strlen($lastText) > 0) {
            $lastText = explode('*', $lastText);
            $lastText = end($lastText);
        }

        return Ussd::build(
            Context::create(
                $request->input('sessionId'),
                $request->input('phoneNumber'),
                $lastText
            )
            ->with(['phone_number' => $request->input('phoneNumber')])
        )
        ->useInitialState(MenuAction::class)
        ->useResponse(AfricasTalkingResponse::class)
        ->run();
    }
}

Now you should see a different menu when you dial with either a registered or non-registered number.

We will leave the implementation for the registered customer for you to play around with.

Clone this wiki locally