Files
retail/docs/API_INTEGRATION_GUIDE.md
Phuoc Nguyen b94c158004 runable
2025-10-10 16:38:07 +07:00

700 lines
17 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# API Integration Guide - Retail POS App
## Overview
This guide provides comprehensive documentation for the API integration layer of the Retail POS application. The integration is built using **Dio** for HTTP requests with a robust error handling, retry logic, and offline-first architecture.
---
## Architecture
### Layered Architecture
```
Presentation Layer (UI)
Providers (Riverpod)
Use Cases
Repositories
Data Sources (Remote + Local)
Network Layer (Dio)
```
---
## Components
### 1. DioClient (`/lib/core/network/dio_client.dart`)
**Purpose**: Centralized HTTP client configuration with Dio.
**Features**:
- Base URL and timeout configuration
- Request/response interceptors
- Authentication header management
- Automatic retry logic
- Error handling and conversion to custom exceptions
**Usage Example**:
```dart
final dioClient = DioClient();
// GET request
final response = await dioClient.get('/products');
// POST request
final response = await dioClient.post(
'/products',
data: {'name': 'Product Name', 'price': 29.99},
);
// With query parameters
final response = await dioClient.get(
'/products/search',
queryParameters: {'q': 'laptop'},
);
```
**Methods**:
- `get()` - GET requests
- `post()` - POST requests
- `put()` - PUT requests
- `delete()` - DELETE requests
- `patch()` - PATCH requests
- `download()` - File downloads
- `updateAuthToken()` - Update authentication token
- `removeAuthToken()` - Remove authentication token
- `addHeader()` - Add custom header
- `removeHeader()` - Remove custom header
---
### 2. API Constants (`/lib/core/constants/api_constants.dart`)
**Purpose**: Centralized configuration for API endpoints, timeouts, and settings.
**Configuration**:
```dart
// Base URL
static const String baseUrl = 'https://api.retailpos.example.com';
static const String apiVersion = '/api/v1';
// Timeouts (in milliseconds)
static const int connectTimeout = 30000;
static const int receiveTimeout = 30000;
static const int sendTimeout = 30000;
// Retry configuration
static const int maxRetries = 3;
static const int retryDelay = 1000;
```
**Endpoints**:
```dart
// Products
ApiConstants.products // GET /products
ApiConstants.productById('123') // GET /products/123
ApiConstants.productsByCategory('cat1') // GET /products/category/cat1
ApiConstants.searchProducts // GET /products/search?q=query
ApiConstants.syncProducts // POST /products/sync
// Categories
ApiConstants.categories // GET /categories
ApiConstants.categoryById('123') // GET /categories/123
ApiConstants.syncCategories // POST /categories/sync
```
---
### 3. Network Info (`/lib/core/network/network_info.dart`)
**Purpose**: Check network connectivity status using `connectivity_plus`.
**Features**:
- Check current connectivity status
- Stream of connectivity changes
- Check connection type (WiFi, Mobile, etc.)
**Usage Example**:
```dart
final networkInfo = NetworkInfoImpl(Connectivity());
// Check if connected
final isConnected = await networkInfo.isConnected;
// Listen to connectivity changes
networkInfo.onConnectivityChanged.listen((isConnected) {
if (isConnected) {
print('Device is online');
} else {
print('Device is offline');
}
});
// Check connection type
final isWiFi = await networkInfo.isConnectedViaWiFi;
final isMobile = await networkInfo.isConnectedViaMobile;
```
---
### 4. API Interceptors (`/lib/core/network/api_interceptor.dart`)
#### Logging Interceptor
Logs all requests and responses for debugging.
```dart
REQUEST[GET] => PATH: /products
Headers: {Content-Type: application/json}
RESPONSE[200] => PATH: /products
Data: {...}
```
#### Authentication Interceptor
Automatically adds authentication headers to requests.
```dart
// Set auth token
authInterceptor.setAuthToken('your-jwt-token');
// All requests now include:
// Authorization: Bearer your-jwt-token
// Clear token
authInterceptor.clearAuthToken();
```
#### Error Interceptor
Converts HTTP status codes to custom exceptions.
**Status Code Mapping**:
- `400` `BadRequestException`
- `401` `UnauthorizedException`
- `403` `ForbiddenException`
- `404` `NotFoundException`
- `422` `ValidationException`
- `429` `RateLimitException`
- `500+` `ServerException`
- `503` `ServiceUnavailableException`
#### Retry Interceptor
Automatically retries failed requests.
**Retry Conditions**:
- Connection timeout
- Send/receive timeout
- Connection errors
- HTTP 408, 429, 502, 503, 504
**Retry Strategy**:
- Max retries: 3 (configurable)
- Exponential backoff: delay * (retry_count + 1)
- Default delay: 1000ms
---
### 5. Custom Exceptions (`/lib/core/errors/exceptions.dart`)
**Network Exceptions**:
- `NoInternetException` - No internet connection
- `TimeoutException` - Request timeout
- `ConnectionException` - Connection failed
- `NetworkException` - General network error
**Server Exceptions**:
- `ServerException` - Server error (500+)
- `ServiceUnavailableException` - Service unavailable (503)
**Client Exceptions**:
- `BadRequestException` - Invalid request (400)
- `UnauthorizedException` - Authentication required (401)
- `ForbiddenException` - Access forbidden (403)
- `NotFoundException` - Resource not found (404)
- `ValidationException` - Validation failed (422)
- `RateLimitException` - Rate limit exceeded (429)
**Cache Exceptions**:
- `CacheException` - Cache operation failed
- `CacheNotFoundException` - Data not found in cache
**Data Exceptions**:
- `DataParsingException` - Failed to parse data
- `InvalidDataFormatException` - Invalid data format
**Business Exceptions**:
- `OutOfStockException` - Product out of stock
- `InsufficientStockException` - Insufficient stock
- `TransactionException` - Transaction failed
- `PaymentException` - Payment processing failed
---
### 6. Failure Classes (`/lib/core/errors/failures.dart`)
Failures are used in the domain/presentation layer to represent errors without throwing exceptions.
**Network Failures**:
- `NoInternetFailure`
- `ConnectionFailure`
- `TimeoutFailure`
- `NetworkFailure`
**Server Failures**:
- `ServerFailure`
- `ServiceUnavailableFailure`
**Client Failures**:
- `BadRequestFailure`
- `UnauthorizedFailure`
- `ForbiddenFailure`
- `NotFoundFailure`
- `ValidationFailure`
- `RateLimitFailure`
**Usage Pattern**:
```dart
// In repository
try {
final products = await remoteDataSource.fetchProducts();
return Right(products);
} on NoInternetException {
return Left(NoInternetFailure());
} on ServerException catch (e) {
return Left(ServerFailure(e.message, e.statusCode));
} catch (e) {
return Left(NetworkFailure('Unexpected error: ${e.toString()}'));
}
```
---
### 7. Remote Data Sources
#### Product Remote Data Source (`/lib/features/products/data/datasources/product_remote_datasource.dart`)
**Methods**:
```dart
// Fetch all products
Future<List<ProductModel>> fetchProducts();
// Fetch single product
Future<ProductModel> fetchProductById(String id);
// Fetch products by category
Future<List<ProductModel>> fetchProductsByCategory(String categoryId);
// Search products
Future<List<ProductModel>> searchProducts(String query);
// Sync products
Future<void> syncProducts(List<ProductModel> products);
```
**Usage Example**:
```dart
final dataSource = ProductRemoteDataSourceImpl(dioClient);
// Fetch all products
final products = await dataSource.fetchProducts();
// Search products
final results = await dataSource.searchProducts('laptop');
// Fetch by category
final categoryProducts = await dataSource.fetchProductsByCategory('electronics');
```
#### Category Remote Data Source (`/lib/features/categories/data/datasources/category_remote_datasource.dart`)
**Methods**:
```dart
// Fetch all categories
Future<List<CategoryModel>> fetchCategories();
// Fetch single category
Future<CategoryModel> fetchCategoryById(String id);
// Sync categories
Future<void> syncCategories(List<CategoryModel> categories);
```
---
## Setup & Installation
### 1. Dependencies
Already added to `pubspec.yaml`:
```yaml
dependencies:
dio: ^5.7.0
connectivity_plus: ^6.1.1
equatable: ^2.0.7
get_it: ^8.0.4
```
### 2. Initialize Dependencies
In `main.dart`:
```dart
import 'package:flutter/material.dart';
import 'core/di/injection_container.dart' as di;
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// Initialize dependencies
await di.initDependencies();
runApp(const MyApp());
}
```
### 3. Configure API Base URL
Update in `/lib/core/constants/api_constants.dart`:
```dart
static const String baseUrl = 'https://your-api-url.com';
```
### 4. Using Mock Data (Development)
Enable mock data in `/lib/core/constants/api_constants.dart`:
```dart
static const bool useMockData = true;
```
---
## Usage Examples
### Example 1: Fetch Products in a Repository
```dart
import 'package:dartz/dartz.dart';
import '../../../core/errors/exceptions.dart';
import '../../../core/errors/failures.dart';
import '../../domain/repositories/product_repository.dart';
import '../datasources/product_remote_datasource.dart';
import '../models/product_model.dart';
class ProductRepositoryImpl implements ProductRepository {
final ProductRemoteDataSource remoteDataSource;
final NetworkInfo networkInfo;
ProductRepositoryImpl({
required this.remoteDataSource,
required this.networkInfo,
});
@override
Future<Either<Failure, List<ProductModel>>> getProducts() async {
// Check network connectivity
if (!await networkInfo.isConnected) {
return Left(NoInternetFailure());
}
try {
final products = await remoteDataSource.fetchProducts();
return Right(products);
} on ServerException catch (e) {
return Left(ServerFailure(e.message, e.statusCode));
} on TimeoutException {
return Left(TimeoutFailure());
} on NetworkException catch (e) {
return Left(NetworkFailure(e.message, e.statusCode));
} catch (e) {
return Left(NetworkFailure('Unexpected error: ${e.toString()}'));
}
}
}
```
### Example 2: Using in a Riverpod Provider
```dart
import 'package:riverpod_annotation/riverpod_annotation.dart';
import '../../../core/di/injection_container.dart';
import '../../data/datasources/product_remote_datasource.dart';
import '../../data/models/product_model.dart';
part 'products_provider.g.dart';
@riverpod
class Products extends _$Products {
@override
Future<List<ProductModel>> build() async {
final dataSource = sl<ProductRemoteDataSource>();
return await dataSource.fetchProducts();
}
Future<void> refresh() async {
state = const AsyncValue.loading();
state = await AsyncValue.guard(() async {
final dataSource = sl<ProductRemoteDataSource>();
return await dataSource.fetchProducts();
});
}
}
```
### Example 3: Handling Errors in UI
```dart
// In your widget
Consumer(
builder: (context, ref, child) {
final productsAsync = ref.watch(productsProvider);
return productsAsync.when(
data: (products) => ProductGrid(products: products),
loading: () => const CircularProgressIndicator(),
error: (error, stack) {
// Handle different error types
if (error is NoInternetFailure) {
return ErrorWidget(
message: 'No internet connection',
onRetry: () => ref.refresh(productsProvider),
);
} else if (error is ServerFailure) {
return ErrorWidget(
message: 'Server error. Please try again later',
onRetry: () => ref.refresh(productsProvider),
);
}
return ErrorWidget(
message: 'An error occurred',
onRetry: () => ref.refresh(productsProvider),
);
},
);
},
);
```
---
## Testing
### Testing Network Connectivity
```dart
// Use mock implementation
final mockNetworkInfo = NetworkInfoMock(isConnected: false);
// Test offline scenario
final repository = ProductRepositoryImpl(
remoteDataSource: mockRemoteDataSource,
networkInfo: mockNetworkInfo,
);
final result = await repository.getProducts();
expect(result.isLeft(), true);
expect(result.fold((l) => l, (r) => null), isA<NoInternetFailure>());
```
### Testing API Calls
```dart
// Use mock data source
final mockDataSource = ProductRemoteDataSourceMock();
final products = await mockDataSource.fetchProducts();
expect(products, isNotEmpty);
expect(products.first.name, 'Sample Product 1');
```
---
## API Response Format
### Expected JSON Response Formats
#### Products List
```json
{
"products": [
{
"id": "1",
"name": "Product Name",
"description": "Product description",
"price": 29.99,
"imageUrl": "https://example.com/image.jpg",
"categoryId": "cat1",
"stockQuantity": 100,
"isAvailable": true,
"createdAt": "2024-01-01T00:00:00Z",
"updatedAt": "2024-01-01T00:00:00Z"
}
]
}
```
Or direct array:
```json
[
{
"id": "1",
"name": "Product Name",
...
}
]
```
#### Single Product
```json
{
"product": {
"id": "1",
"name": "Product Name",
...
}
}
```
#### Categories List
```json
{
"categories": [
{
"id": "1",
"name": "Electronics",
"description": "Electronic devices",
"iconPath": "assets/icons/electronics.png",
"color": "#2196F3",
"productCount": 25,
"createdAt": "2024-01-01T00:00:00Z"
}
]
}
```
#### Error Response
```json
{
"message": "Error message",
"error": "Detailed error",
"errors": {
"field1": ["Validation error 1"],
"field2": ["Validation error 2"]
}
}
```
---
## Troubleshooting
### Issue: Connection Timeout
**Solution**:
- Increase timeout in `api_constants.dart`
- Check network connectivity
- Verify API endpoint is accessible
### Issue: SSL Certificate Error
**Solution**:
In development, you can allow self-signed certificates:
```dart
(_dio.httpClientAdapter as IOHttpClientAdapter).createHttpClient = () {
final client = HttpClient();
client.badCertificateCallback = (cert, host, port) => true;
return client;
};
```
**  Warning**: Never use this in production!
### Issue: 401 Unauthorized
**Solution**:
- Check authentication token is set correctly
- Verify token hasn't expired
- Refresh token if necessary
### Issue: Data Parsing Error
**Solution**:
- Verify API response format matches expected format
- Check JSON field names match model properties
- Add debug logging to see actual response
---
## Best Practices
1. **Always check network connectivity** before making API calls
2. **Use offline-first approach** - read from cache first, sync in background
3. **Handle all error scenarios** gracefully with user-friendly messages
4. **Implement proper retry logic** for transient errors
5. **Log requests/responses** in development, disable in production
6. **Use mock data sources** for development and testing
7. **Implement request cancellation** for expensive operations
8. **Cache API responses** to improve performance
9. **Use proper timeout values** based on expected response time
10. **Implement proper authentication** token management
---
## Next Steps
1. **Implement Repositories**: Create repository implementations
2. **Add Use Cases**: Define business logic in use cases
3. **Create Riverpod Providers**: Wire up data layer with UI
4. **Implement Offline Sync**: Add background sync logic
5. **Add Authentication**: Implement OAuth/JWT authentication
6. **Implement Caching**: Add response caching with Hive
7. **Add Pagination**: Implement paginated API requests
8. **Error Tracking**: Integrate error tracking (Sentry, Firebase Crashlytics)
---
## Support & Resources
- **Dio Documentation**: https://pub.dev/packages/dio
- **Connectivity Plus**: https://pub.dev/packages/connectivity_plus
- **GetIt Documentation**: https://pub.dev/packages/get_it
- **Riverpod Documentation**: https://riverpod.dev
---
## File Structure
```
lib/
 core/
  constants/
   api_constants.dart  API configuration
  di/
   injection_container.dart  Dependency injection
  errors/
   exceptions.dart  Custom exceptions
   failures.dart  Failure classes
  network/
  dio_client.dart  Dio HTTP client
  api_interceptor.dart  Interceptors
  network_info.dart  Network connectivity
 features/
  products/
   data/
   datasources/
    product_remote_datasource.dart 
   models/
   product_model.dart 
  categories/
  data/
  datasources/
   category_remote_datasource.dart 
  models/
  category_model.dart 
```
---
**All API integration components are now complete and ready to use!** <