@afidos/ussd-engine
v2.0.2
Published
Librairie ussd NestJS
Readme
@afidos/ussd-engine
A powerful Node.js framework for creating, managing, and deploying USSD (Unstructured Supplementary Service Data) applications simply and effectively. Ideal for developing mobile services accessible without internet in emerging markets.
🚀 Features
- 🔄 Standardized API: Abstraction of differences between telecom operators
- 📱 Advanced menu management: Intuitive API for creating interactive menus with static and dynamic options
- 💾 Robust session management: Persistent storage of user data between requests
- 🛠️ Flexible storage adapters: Support for memory, MongoDB, and PostgreSQL
- 🧩 Native NestJS integration: Ready-to-use module and interceptors
- 🌍 Multi-operator: Support for different operators (Moov, Celtis, etc.)
- 📝 Input fields: Easy creation of USSD forms with built-in validation
- 🔄 Contextual navigation: Navigation system with history and back functionality
📋 Table of Contents
- Installation
- Quick Start
- Architecture
- Usage Guide
- Storage Adapters
- Operator Formatters
- NestJS Integration
- Complete Examples
- API Reference
- Best Practices
- FAQ
- Contribution
- License
📦 Installation
npm install @afidos/ussd-engineOr with Yarn:
yarn add @afidos/ussd-engine🏁 Quick Start
Here's a minimal example to create a USSD application:
import { USSDMenuBuilder, USSDService, InMemoryAdapter } from '@afidos/ussd-engine';
// Create the menu builder
const menuBuilder = new USSDMenuBuilder();
// Define menus
menuBuilder
.createMenu('main', 'Main Menu')
.addOption('main', 'balance', 'Check balance', async (session) => {
// Simulate balance retrieval
session.setData('balance', 5000);
return USSDService.createNavigator().navigateTo('showBalance')(session);
})
.addOption('main', 'transfer', 'Transfer', async (session) => {
return USSDService.createNavigator().navigateTo('transferAmount')(session);
})
.createMenu('showBalance', (session) => `Your balance is ${session.getData('balance')} FCFA`)
.addOption('showBalance', 'back', 'Back', USSDService.createNavigator().goBack())
.addOption('showBalance', 'exit', 'Exit', USSDService.createNavigator().endSession('Thank you for using our service!'))
.createInputField('transferAmount', 'Enter the amount to transfer:',
USSDService.createNavigator().createInputField('amount', 'transferRecipient'))
.createInputField('transferRecipient', 'Enter the recipient number:',
USSDService.createNavigator().createInputField('recipient', 'confirmTransfer'))
.createMenu('confirmTransfer', (session) =>
`Transfer of ${session.getData('amount')} FCFA to ${session.getData('recipient')}. Confirm?`)
.addOption('confirmTransfer', 'confirm', 'Confirm', async (session) => {
// Transfer logic here
return USSDService.createNavigator().endSession('Transfer completed successfully!')(session);
})
.addOption('confirmTransfer', 'cancel', 'Cancel', USSDService.createNavigator().endSession('Transfer cancelled.'));
// Create the USSD service with an in-memory storage adapter
const storageAdapter = new InMemoryAdapter();
const ussdService = new USSDService(menuBuilder.build(), storageAdapter);
// Example of request processing
async function handleRequest(request) {
const response = await ussdService.processRequest({
sessionId: request.sessionId,
phoneNumber: request.phoneNumber,
text: request.text,
serviceCode: request.serviceCode
});
return response;
}🏛️ Architecture
USSD Engine's architecture is designed to be modular and extensible:
@afidos/ussd-engine
├── core - Core USSD service logic
├── menu - Menu management and navigation
├── session - User session management
├── storage - Adapters for different storage systems
├── transformer - Formatters for different telecom operators
├── types - Common interfaces and types
└── ussd-engine - NestJS module and integration📚 Usage Guide
Creating Menus
Creating USSD menus is done with the USSDMenuBuilder class:
const menuBuilder = new USSDMenuBuilder();
// Simple menu with static title
menuBuilder.createMenu('main', 'Welcome to our service');
// Menu with dynamic title based on session data
menuBuilder.createMenu('profile', (session) => `Hello ${session.getData('username')}`);
// Adding options to a menu
menuBuilder.addOption('main', 'profile', 'My profile', (session) => {
return USSDService.createNavigator().navigateTo('profile')(session);
});
// Option with custom action
menuBuilder.addOption('main', 'balance', 'Check balance', async (session) => {
const balance = await fetchBalanceFromAPI(session.phoneNumber);
session.setData('balance', balance);
return USSDService.createNavigator().navigateTo('showBalance')(session);
});Custom Option Indexes
You can define custom indexes for menu options instead of using automatic sequential numbering (1, 2, 3...):
const menuBuilder = new USSDMenuBuilder();
menuBuilder
.createMenu('main', 'What would you like to do?')
// Automatic index (will be 1)
.addOption('main', 'balance', 'Check balance', balanceAction)
// Automatic index (will be 2)
.addOption('main', 'transfer', 'Transfer money', transferAction)
// Custom index 9 for help
.addOption('main', 'help', {index: 9, text: 'Help'}, helpAction)
// Custom index 0 for back/exit
.addOption('main', 'exit', {index: 0, text: 'Exit'}, exitAction);Display result:
What would you like to do?
0. Exit
1. Check balance
2. Transfer money
9. HelpSupported text formats:
'Simple text'- Automatic sequential index{index: 5, text: 'Custom index 5'}- Custom index(session) => 'Dynamic text'- Dynamic function (automatic index)
Benefits:
- Consistent UX: Keep same indexes across different menus (0 = back, 9 = help)
- User-friendly: Users can memorize shortcuts
- Flexible: Mix automatic and custom indexes as needed
- Backward compatible: Existing code continues to work
Session Management
The USSDSession class allows managing user data between interactions:
// In an action handler
async function handleProfileUpdate(session) {
// Storing data
session.setData('username', 'John Doe');
session.setData('lastLogin', new Date().toISOString());
// Retrieving data
const username = session.getData('username');
// Retrieving all data
const allUserData = session.getAllData();
// Saving changes
await session.save();
// Ending the session
await session.end();
}Navigation Between Menus
Navigation is managed by the Navigator via the createNavigator() method:
// Get a navigator
const navigator = USSDService.createNavigator();
// Navigate to another menu
menuBuilder.addOption('main', 'settings', 'Settings', navigator.navigateTo('settingsMenu'));
// Return to previous menu
menuBuilder.addOption('settings', 'back', 'Back', navigator.goBack());
// End the session with a message
menuBuilder.addOption('main', 'exit', 'Exit', navigator.endSession('Thank you for using our service!'));Input Fields
Creating fields to collect information:
// Simple input field
menuBuilder.createInputField(
'enterName', // Field ID
'Enter your name:', // Prompt
(input, session) => { // Processing handler
session.setData('name', input);
return navigator.navigateTo('nextMenu')(session);
}
);
// Field with validation
menuBuilder.createInputField(
'enterPhone',
'Enter your phone number:',
navigator.createInputField('phone', 'confirmation', 'Invalid format',
(input) => /^\d{8}$/.test(input)) // Validation: 8 digits
);Dynamic Options
Generating options based on data with custom indexes:
menuBuilder
.createMenu('products', 'Our products')
.addDynamicOptions('products', async (session) => {
// Retrieve products from an API
const products = await fetchProductsFromAPI();
// Generate options with custom indexes
const options = products.map((product, index) => ({
id: `product_${product.id}`,
text: {index: index + 1, text: `${product.name} - ${product.price} FCFA`},
action: async (session) => {
session.setData('selectedProduct', product);
return navigator.navigateTo('productDetails')(session);
}
}));
// Add common navigation options
options.push(
{
id: 'back',
text: {index: 0, text: 'Back to main menu'},
action: navigator.goBack()
},
{
id: 'help',
text: {index: 9, text: 'Help'},
action: navigator.navigateTo('help')
}
);
return options;
});Validation and Error Handling
// Add a validator to a menu
menuBuilder.setValidator('transferAmount', (input, session) => {
const amount = parseInt(input);
return !isNaN(amount) && amount > 0 && amount <= 1000000;
});
// Add a custom error handler
menuBuilder.setErrorHandler('transferAmount', (error, session) => {
console.error('Transfer error:', error);
return {
text: 'An error occurred during the transfer. Please try again later.',
type: 'end'
};
});🗄️ Storage Adapters
In-Memory Storage
Ideal for testing and development:
import { InMemoryAdapter } from '@afidos/ussd-engine';
// Simple storage
const adapter = new InMemoryAdapter();
// With session expiration (TTL)
const adapter = new InMemoryAdapter({ ttlMs: 5 * 60 * 1000 }); // 5 minutesMongoDB Storage
For production environments with MongoDB:
import { MongoAdapter } from '@afidos/ussd-engine';
// Creating the adapter
const adapter = await MongoAdapter.create(
'mongodb://localhost:27017', // Connection URL
'ussd_database', // Database name
'ussd_sessions', // Collection name
{ ttlSeconds: 3600 } // Expiration after 1 hour
);PostgreSQL Storage
For production environments with PostgreSQL:
import { PgAdapter } from '@afidos/ussd-engine';
// Creating the adapter
const adapter = await PgAdapter.create(
'postgresql://user:password@localhost:5432/ussd_db',
'ussd_sessions' // Table name
);🔄 Operator Formatters
Moov Formatter
For Moov operator requests:
import { MoovFormatter, USSDInterceptor } from '@afidos/ussd-engine';
// Creating a Moov interceptor
const moovInterceptor = new USSDInterceptor({
formatter: new MoovFormatter()
});Celtis Formatter
For Celtis operator requests:
import { CeltisFormatter, USSDInterceptor } from '@afidos/ussd-engine';
// Creating a Celtis interceptor
const celtisInterceptor = new USSDInterceptor({
formatter: new CeltisFormatter()
});🦁 NestJS Integration
USSD Engine Module
import { Module } from '@nestjs/common';
import { UssdEngineModule, InMemoryAdapter, USSDMenuBuilder } from '@afidos/ussd-engine';
@Module({
imports: [
UssdEngineModule.register({
storageAdapter: new InMemoryAdapter(),
menuBuilderFactory: () => {
// Menu configuration
return new USSDMenuBuilder()
.createMenu('main', 'Main Menu')
// ...other menu configurations
.build();
}
})
]
})
export class AppModule {}Interceptors
import { Controller, Post, UseInterceptors } from '@nestjs/common';
import {
USSDInterceptor,
MoovFormatter,
CeltisFormatter,
UssdParam,
USSDRequest,
USSDResponse
} from '@afidos/ussd-engine';
@Controller('ussd')
export class UssdController {
constructor(private readonly ussdService: USSDService) {}
@Post('moov')
@UseInterceptors(new USSDInterceptor({ formatter: new MoovFormatter() }))
async handleMoovRequest(@UssdParam() request: USSDRequest): Promise<USSDResponse> {
return this.ussdService.processRequest(request);
}
@Post('celtis')
@UseInterceptors(new USSDInterceptor({ formatter: new CeltisFormatter() }))
async handleCeltisRequest(@UssdParam() request: USSDRequest): Promise<USSDResponse> {
return this.ussdService.processRequest(request);
}
}Decorators
The @UssdParam() decorator makes it easy to extract standardized USSD data:
// Extract the entire request
@UssdParam() request: USSDRequest
// Extract specific properties
@UssdParam('phoneNumber') phoneNumber: string
@UssdParam('text') userInput: string
@UssdParam('sessionId') sessionId: string📝 Complete Examples
USSD Banking Service
const menuBuilder = new USSDMenuBuilder();
menuBuilder
// Main menu
.createMenu('main', 'My Bank - Menu')
.addOption('main', 'account', 'My account', navigator.navigateTo('accountMenu'))
.addOption('main', 'transfer', 'Transfer', navigator.navigateTo('transferAmount'))
.addOption('main', 'payment', 'Bill payment', navigator.navigateTo('paymentMenu'))
.addOption('main', 'help', 'Help', navigator.navigateTo('helpMenu'))
// Account menu
.createMenu('accountMenu', 'Account Menu')
.addOption('accountMenu', 'balance', 'Balance', async (session) => {
// Get balance from API
const balance = await bankAPI.getBalance(session.phoneNumber);
session.setData('balance', balance);
return navigator.navigateTo('showBalance')(session);
})
.addOption('accountMenu', 'statement', 'Mini statement', navigator.navigateTo('statementMenu'))
.addOption('accountMenu', 'back', 'Back', navigator.goBack())
// Balance display
.createMenu('showBalance', (session) =>
`Your balance is: ${session.getData('balance')} FCFA`)
.addOption('showBalance', 'back', 'Back', navigator.goBack())
.addOption('showBalance', 'exit', 'Exit',
navigator.endSession('Thank you for using My Bank!'))
// Transfer: amount entry
.createInputField('transferAmount', 'Enter amount to transfer:',
navigator.createInputField('amount', 'transferRecipient', 'Invalid amount',
(input) => !isNaN(input) && parseInt(input) > 0))
// Transfer: recipient entry
.createInputField('transferRecipient', 'Enter recipient number:',
navigator.createInputField('recipient', 'confirmTransfer', 'Invalid number',
(input) => /^\d{8}$/.test(input)))
// Transfer confirmation
.createMenu('confirmTransfer', (session) =>
`Confirm transfer of ${session.getData('amount')} FCFA to ${session.getData('recipient')}?`)
.addOption('confirmTransfer', 'confirm', 'Confirm', async (session) => {
try {
await bankAPI.transfer(
session.phoneNumber,
session.getData('recipient'),
session.getData('amount')
);
return navigator.endSession('Transfer completed successfully!')(session);
} catch (error) {
return navigator.endSession('Transfer failed: ' + error.message)(session);
}
})
.addOption('confirmTransfer', 'cancel', 'Cancel',
navigator.endSession('Transfer cancelled.'));📘 API Reference
Main Classes
USSDMenuBuilder: USSD menu builderUSSDService: Main service for processing USSD requestsUSSDSession: User session managementUSSDInterceptor: NestJS interceptor for transforming requests/responses
Key Interfaces
USSDRequest: Standardized format for USSD requestsUSSDResponse: Standardized format for USSD responses with title and optionsUSSDMenu: USSD menu structureMenuOption: USSD menu option with flexible text formatMenuOptionConfig: Configuration object for custom option indexesStorageAdapter: Interface for storage adaptersUSSDFormatter: Interface for operator formatters
Updated Interface Definitions
export interface USSDResponse {
title: string;
options?: Record<number, string>;
type: 'continue' | 'end';
}
export interface MenuOptionConfig {
index: number;
text: string;
}
export interface MenuOption {
id: string;
text: string | MenuOptionConfig | ((session: USSDSessionInterface) => string | Promise<string>);
action: (session: USSDSessionInterface) => Promise<USSDResponse> | USSDResponse;
}Important Methods
USSDMenuBuilder:
createMenu(id, title): Creates a new menucreateInputField(id, title, processInput): Creates an input fieldaddOption(menuId, optionId, text, action): Adds an option to a menuaddDynamicOptions(menuId, generator): Adds dynamic optionssetValidator(menuId, validator): Sets a validator for a menusetErrorHandler(menuId, handler): Sets an error handlerbuild(): Builds and returns the menu map
USSDService:
processRequest(request): Processes a USSD requestcreateNavigator(): Creates a navigation object
USSDSession:
setData(key, value): Stores datagetData(key): Retrieves datagetAllData(): Retrieves all datasave(): Saves the sessionend(): Ends the session
💡 Best Practices
Menu Structure: Limit navigation depth to 3-4 levels maximum for a better user experience.
Short Texts: USSD screens have limitations (often 160-182 characters), keep your texts concise.
State Management: Use
session.setData()to store important data between requests.Validation: Always validate user inputs with appropriate validators.
Intuitive Navigation: Always provide back and exit options.
Performance: For menus with many options, use pagination or group them by categories.
Error Handling: Implement custom error handlers for each critical menu.
Testing: Test your USSD menus on different devices and with different operators.
❓ FAQ
Q: Can I use USSD Engine without NestJS?
A: Yes, USSD Engine works independently. NestJS integration is optional.
Q: How to handle expired sessions?
A: Use TTL options in storage adapters to automatically clean up inactive sessions.
Q: Does the application support Unicode/accented characters?
A: Yes, but be careful as some USSD terminals may have limited support.
Q: How to debug my USSD application?
A: Use detailed logs and the InMemoryAdapter during development to inspect session states.
Q: How to test my USSD service without real equipment?
A: You can simulate USSD requests with tools like Postman by following your telecom operator's format.
🤝 Contribution
Contributions are welcome! Feel free to submit pull requests.
- Fork the project
- Create your feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add some amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
📄 License
This project is licensed under the MIT License - see the LICENSE file for details.
