Skip to main content

Overview

The wallet system provides digital balance management for both passengers and drivers. Passengers use wallets to pay for trips instantly, while drivers accumulate earnings that can be withdrawn. The system supports multiple currencies and maintains detailed transaction histories.

Wallet Types

Passenger Wallet

Used for trip payments and top-ups. Supports instant booking confirmation.

Driver Wallet

Accumulates trip earnings minus platform commission. Supports withdrawals.

Passenger Wallet

Check Wallet Balance

GET /api/passengers/{passengerId}/wallet?currency=SAR
Implementation:
src/services/Trips/Trips.Api/Features/Passengers/GetWallet.cs
public sealed class GetPassengerWalletHandler(
    IPassengerWalletRepository passengerWalletRepository,
    ILogger<GetPassengerWalletHandler> logger)
    : IRequestHandler<GetPassengerWalletQuery, GetPassengerWalletResponse>
{
    public async Task<GetPassengerWalletResponse> Handle(
        GetPassengerWalletQuery request, 
        CancellationToken cancellationToken)
    {
        PassengerWallet? wallet = await passengerWalletRepository.GetPassengerWalletAsync(
            request.PassengerId,
            request.Currency,
            cancellationToken);

        if (wallet != null)
        {
            return new GetPassengerWalletResponse
            {
                Currency = wallet.Currency,
                WalletBalance = wallet.WalletBalance,
                HasWallet = true
            };
        }

        return new GetPassengerWalletResponse
        {
            Currency = request.Currency,
            WalletBalance = 0m,
            HasWallet = false
        };
    }
}
Response:
{
  "currency": "SAR",
  "walletBalance": 500.00,
  "hasWallet": true
}

Top Up Wallet via Bank Transfer

Passengers can add funds to their wallet by submitting a bank transfer receipt:
POST /api/wallet/top-up/bank-transfer
Request (multipart/form-data):
bankAccountId: ba_123456
amount: 500 SAR
note: Bank transfer from account ending in 1234
receiptImage: [image file]
1

Upload Receipt

Passenger uploads bank transfer receipt with transaction details:
src/services/Trips/Trips.Api/Features/WalletTopUp/WalletTopUpBankTransferEndpoint.cs
if (receiptImage is { Length: > 0 })
{
    string[] allowedExtensions = [".jpg", ".jpeg", ".png", ".gif", ".webp"];
    string extension = Path.GetExtension(receiptImage.FileName).ToLowerInvariant();
    
    if (!allowedExtensions.Contains(extension))
    {
        return Results.BadRequest(new WalletTopUpBankTransferResponse
        {
            Success = false,
            Message = "نوع الملف غير مدعوم. الأنواع المسموح بها: JPG, JPEG, PNG, GIF, WEBP"
        });
    }

    if (receiptImage.Length > 10 * 1024 * 1024)
    {
        return Results.BadRequest(new WalletTopUpBankTransferResponse
        {
            Success = false,
            Message = "حجم الملف كبير جداً. الحد الأقصى 10 ميجابايت"
        });
    }
}
2

Create Transfer Record

System creates a pending bank transfer record:
src/services/Trips/Trips.Api/Features/WalletTopUp/WalletTopUpBankTransferHandler.cs
var bankTransfer = new BankTransfer
{
    Id = transferId,
    BookingId = null,
    PassengerId = request.UserType == "Passenger" ? request.UserId : null,
    DriverId = request.UserType == "Driver" ? request.UserId : null,
    CompanyId = request.UserType == "Company" ? request.UserId : null,
    UserType = request.UserType,
    TransferType = BankTransferTypes.WalletTopUp,
    BankAccountId = request.BankAccountId,
    RequestedAmount = request.RequestedAmount,
    Amount = 0,
    Currency = currencyCode,
    Note = request.Note,
    ReceiptImageUrl = request.ReceiptImageUrl,
    Status = BankTransferStatuses.Pending,
    CreatedAt = DateTimeOffset.UtcNow
};

await bankTransferRepository.AddBankTransferAsync(bankTransfer, cancellationToken);
3

Admin Review

Admin reviews the transfer and either approves or rejects it:
POST /api/wallet/top-up/bank-transfer/{transferId}/approve
POST /api/wallet/top-up/bank-transfer/{transferId}/reject
4

Wallet Credit

Upon approval, the wallet is credited with the verified amount.
Response:
{
  "success": true,
  "message": "تم إرسال طلب شحن المحفظة للمراجعة. سيتم إشعارك عند الموافقة.",
  "transferId": "bt_abc123",
  "status": "Pending"
}

Approve Top-Up Request (Admin)

POST /api/wallet/top-up/bank-transfer/{transferId}/approve
{
  "verifiedAmount": 500.00
}

Reject Top-Up Request (Admin)

POST /api/wallet/top-up/bank-transfer/{transferId}/reject
{
  "reason": "Receipt image is not clear. Please resubmit."
}

Driver Wallet

Check Driver Wallet

GET /api/drivers/me/wallet
Response:
{
  "currency": "SAR",
  "walletBalance": 2500.00,
  "totalEarnings": 5000.00,
  "totalCommission": 500.00,
  "netProfit": 4500.00
}
Implementation:
src/services/Users/Users.Api/Features/Drivers/GetWallet.cs
public sealed class GetWalletHandler(
    AppDbContext context,
    ITripsUsageClient tripsUsageClient,
    ILogger<GetWalletHandler> logger)
{
    public async Task<Result<GetWalletResponse>> HandleAsync(
        ClaimsPrincipal user,
        CancellationToken cancellationToken = default)
    {
        string driverId = userIdResult.Value;

        Driver driver = await context.Drivers
            .FirstOrDefaultAsync(d => d.Id == driverId && !d.IsDeleted, cancellationToken)
            ?? throw new UserNotFoundException("لم يتم العثور على السائق");

        string? displayCurrencyId = !string.IsNullOrWhiteSpace(driver.DisplayCurrencyId)
            ? driver.DisplayCurrencyId
            : driver.DefaultCurrencyId;

        WalletDataResponse? walletData = await tripsUsageClient.GetDriverWalletAsync(
            driverId, 
            displayCurrency, 
            cancellationToken);

        return Result<GetWalletResponse>.Success(new GetWalletResponse
        {
            Currency = displayCurrency,
            WalletBalance = Math.Round(walletData.WalletBalance, 2),
            TotalEarnings = Math.Round(walletData.TotalEarnings, 2),
            TotalCommission = Math.Round(walletData.TotalCommission, 2),
            NetProfit = Math.Round(walletData.NetProfit, 2)
        });
    }
}

Wallet Breakdown

Sum of all trip fares received from passengers.
Platform commission deducted from earnings based on commission policy.
Total Earnings - Total Commission = Amount driver actually earned.
Available balance for withdrawal. May differ from Net Profit due to pending transactions.

Withdraw from Driver Wallet (Admin)

Drivers request withdrawals through customer support, and admins process them:
POST /api/admin/drivers/{driverId}/wallet/withdraw
{
  "amount": 1000.00,
  "currency": "SAR",
  "transferReference": "IBAN: SA1234567890",
  "notes": "Weekly withdrawal request"
}
Implementation:
src/services/Trips/Trips.Api/Features/Admin/DriverWallets/WithdrawFromDriverWallet.cs
public sealed class WithdrawFromDriverWalletHandler(
    IWalletOperationService walletOps,
    IUsersApiService usersApiService,
    ILogger<WithdrawFromDriverWalletHandler> logger,
    IMessageBus messageBus)
    : IRequestHandler<WithdrawFromDriverWalletCommand, WithdrawFromDriverWalletResponse>
{
    public async Task<WithdrawFromDriverWalletResponse> Handle(
        WithdrawFromDriverWalletCommand request, 
        CancellationToken cancellationToken)
    {
        string currencyCode = await CurrencyResolver.ResolveCodeAsync(
            request.Currency, usersApiService, cancellationToken);

        var result = await walletOps.AdminWithdrawFromDriverAsync(
            new AdminWithdrawFromDriverRequest(
                request.DriverId,
                currencyCode,
                request.Amount,
                request.AdminId,
                request.TransferReference,
                request.Notes),
            cancellationToken);

        if (result.Success)
        {
            await messageBus.PublishAsync(new WalletWithdrawnNotification(
                request.DriverId,
                request.Amount,
                currencyCode,
                result.BalanceAfter,
                request.TransferReference));
        }

        return new WithdrawFromDriverWalletResponse
        {
            Success = result.Success,
            Message = result.Success
                ? $"تم سحب {request.Amount} {currencyCode} بنجاح"
                : result.ErrorMessage ?? "فشل في سحب المبلغ",
            NewBalance = result.BalanceAfter
        };
    }
}
Response:
{
  "success": true,
  "message": "تم سحب 1000 SAR بنجاح",
  "newBalance": 1500.00
}

Wallet Transactions

Every wallet operation creates a transaction record:

Transaction Types

  • Credit: Funds added to wallet
    • Top-up approved
    • Trip earnings credited
    • Refund processed
  • Debit: Funds removed from wallet
    • Trip payment
    • Withdrawal
    • Commission deduction

View Wallet Transactions

GET /api/passengers/{passengerId}/wallet/transactions?currency=SAR&page=1&pageSize=20
GET /api/drivers/{driverId}/wallet/transactions?currency=SAR&page=1&pageSize=20
Response:
{
  "transactions": [
    {
      "id": "wt_abc123",
      "type": "Debit",
      "amount": 250.00,
      "currency": "SAR",
      "description": "Payment for trip t_xyz789",
      "balanceBefore": 750.00,
      "balanceAfter": 500.00,
      "createdAt": "2026-03-10T14:30:00Z"
    },
    {
      "id": "wt_def456",
      "type": "Credit",
      "amount": 500.00,
      "currency": "SAR",
      "description": "Wallet top-up via bank transfer",
      "balanceBefore": 250.00,
      "balanceAfter": 750.00,
      "createdAt": "2026-03-09T10:15:00Z"
    }
  ],
  "totalCount": 45,
  "page": 1,
  "pageSize": 20
}

Wallet Updates on Booking

When a driver accepts a booking with wallet payment:
1

Passenger Wallet Deduction

// Passenger wallet is debited
var passengerWalletResult = await walletOps.DeductFromPassengerWalletAsync(
    booking.PassengerId,
    booking.Currency,
    booking.TotalPrice,
    booking.Id);
2

Driver Wallet Credit

// Driver wallet is credited (minus commission)
var driverWalletResult = await walletOps.CreditDriverWalletAsync(
    trip.DriverId,
    booking.Currency,
    booking.TotalPrice,
    booking.Id);
3

Send Notifications

var walletNotification = new WalletDeductedNotification(
    booking.Id, trip.Id, booking.PassengerId,
    booking.TotalPrice, tripCurrency, 
    passengerWalletResult.BalanceAfter,
    trip.From, trip.To);
await messageBus.PublishAsync(walletNotification);

var driverWalletNotification = new WalletUpdatedNotification(
    trip.Id, trip.DriverId,
    driverWalletResult.BalanceAfter,
    driverWalletResult.TotalEarnings,
    driverWalletResult.TotalCommission,
    driverWalletResult.NetProfit,
    tripCurrency);
await messageBus.PublishAsync(driverWalletNotification);

Multi-Currency Support

Wallets are currency-specific. Each user can have multiple wallets in different currencies:
PassengerWallet? wallet = await passengerWalletRepository.GetPassengerWalletAsync(
    passengerId,
    currency,  // e.g., "SAR", "USD", "AED"
    cancellationToken);
Payment currency must match the trip currency. Cross-currency payments require currency conversion.

Admin Wallet Management

Admins have full visibility and control over all wallets:

View All Passenger Wallets

GET /api/admin/wallets/passengers

View All Driver Wallets

GET /api/admin/wallets/drivers

Manual Wallet Adjustment

Admins can manually credit or debit wallets for corrections:
POST /api/admin/drivers/{driverId}/wallet/withdraw
POST /api/admin/passengers/{passengerId}/wallet/credit

Error Handling

{
  "success": false,
  "message": "رصيد المحفظة غير كافٍ. الرصيد الحالي: 100 SAR، المطلوب: 250 SAR"
}
{
  "success": false,
  "message": "العملة المحددة غير مدعومة أو غير نشطة"
}
{
  "success": false,
  "message": "الحساب البنكي غير موجود"
}
{
  "success": false,
  "message": "فشل رفع صورة الإيصال"
}

Best Practices

Maintain Balance

Keep sufficient wallet balance to enable instant trip confirmations.

Track Transactions

Regularly review wallet transactions to monitor spending and earnings.

Clear Receipts

Upload high-quality, legible receipt images for faster top-up approvals.

Regular Withdrawals

Drivers should withdraw earnings regularly to manage cash flow.

Build docs developers (and LLMs) love