Skip to main content

Overview

The Store Wallet API manages financial operations for store owners including balance tracking, payout requests, transaction history, and earnings management. This system automatically processes order earnings and handles marketplace commission deductions. Location: convex/stores/wallet.ts

Wallet Structure

Available Balance

Funds available for immediate payout requests

Pending Balance

Earnings from recent orders (held for security period)

Total Earnings

Lifetime earnings from all completed orders

Payout History

Complete record of all payout transactions

Get Store Wallet Balance

Retrieve comprehensive wallet information and statistics for a store.
storeId
Id<'stores'>
required
Store ID to get wallet information for
const wallet = await convex.query(api.stores.wallet.getStoreWalletBalance, { 
  storeId: "j123456789" 
});
{
  "_id": "w123456789",
  "storeId": "j123456789",
  "availableBalance": 1250.75,
  "pendingBalance": 150.25,
  "totalEarnings": 5000.00,
  "totalOrders": 42,
  "totalPayouts": 3500.00,
  "totalCommissionPaid": 245.50,
  "minPayoutAmount": 50.00,
  "maxPayoutAmount": 2000.00,
  "lastOrderDate": 1640995800000,
  "lastPayoutDate": 1640990000000,
  "_creationTime": 1640980000000
}

Request Payout

Request a payout from the store wallet to the registered bank account.
storeId
Id<'stores'>
required
Store ID requesting the payout
amount
number
required
Payout amount (must be within available balance and limits)
const payoutId = await convex.mutation(api.stores.wallet.requestPayout, {
  storeId: "j123456789",
  amount: 500.00
});
"p123456789"

Get Payout History

Retrieve payout-related transactions by filtering wallet transactions.
storeId
Id<'stores'>
required
Store ID
limit
number
Maximum number of payout records to return
const payoutHistory = await convex.query(api.stores.wallet.getWalletTransactionHistory, {
  storeId: "j123456789",
  type: "payout",
  limit: 20
});
[
  {
    "_id": "wt987654321",
    "storeWalletId": "w123456789",
    "type": "payout",
    "amount": -500.00,
    "description": "Payout to bank account",
    "payoutId": "p123456789",
    "balanceAfter": 750.75,
    "_creationTime": 1640990000000
  }
]

Get Wallet Transaction History

Retrieve detailed transaction history for a store wallet.
storeId
Id<'stores'>
required
Store ID
limit
number
Maximum number of transaction records
type
string
Filter by transaction type: order_funding, commission_deduction, payout, payout_fee, refund, adjustment, penalty, bonus
// Get all transactions
const allTransactions = await convex.query(api.stores.wallet.getWalletTransactionHistory, {
  storeId: "j123456789",
  limit: 50
});

// Get only order funding transactions
const orderTransactions = await convex.query(api.stores.wallet.getWalletTransactionHistory, {
  storeId: "j123456789",
  type: "order_funding",
  limit: 30
});
[
  {
    "_id": "wt123456789",
    "storeWalletId": "w123456789",
    "type": "order_funding",
    "amount": 45.50,
    "description": "Order #o123456789 earnings",
    "orderId": "o123456789",
    "balanceAfter": 1250.75,
    "metadata": {
      "orderTotal": 50.97,
      "platformCommission": 5.47,
      "storeEarnings": 45.50
    },
    "_creationTime": 1640995800000
  },
  {
    "_id": "wt987654321",
    "storeWalletId": "w123456789",
    "type": "commission_deduction",
    "amount": -5.47,
    "description": "Platform commission for order #o123456789",
    "orderId": "o123456789",
    "balanceAfter": 1205.25,
    "_creationTime": 1640995800000
  },
  {
    "_id": "wt555666777",
    "storeWalletId": "w123456789",
    "type": "payout",
    "amount": -500.00,
    "description": "Payout to bank account",
    "payoutId": "p123456789",
    "balanceAfter": 750.75,
    "_creationTime": 1640990000000
  }
]

Wallet Dashboard Component

Complete wallet management interface:
import { useState } from 'react';
import { useQuery, useMutation } from 'convex/react';
import { api } from './convex/_generated/api';
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts';

interface WalletDashboardProps {
  storeId: string;
}

function WalletDashboard({ storeId }: WalletDashboardProps) {
  const [payoutAmount, setPayoutAmount] = useState<string>('');
  const [showPayoutForm, setShowPayoutForm] = useState(false);

  const wallet = useQuery(api.stores.wallet.getStoreWalletBalance, { storeId });
  const payoutHistory = useQuery(api.stores.wallet.getWalletTransactionHistory, { 
    storeId, 
    type: 'payout',
    limit: 10 
  });
  const transactions = useQuery(api.stores.wallet.getWalletTransactionHistory, { 
    storeId, 
    limit: 20 
  });

  const requestPayout = useMutation(api.stores.wallet.requestPayout);

  const handlePayoutRequest = async () => {
    if (!wallet || !payoutAmount) return;

    const amount = parseFloat(payoutAmount);
    
    if (amount < wallet.minPayoutAmount) {
      alert(`Minimum payout amount is $${wallet.minPayoutAmount}`);
      return;
    }
    
    if (amount > wallet.availableBalance) {
      alert('Insufficient available balance');
      return;
    }

    try {
      const payoutId = await requestPayout({ storeId, amount });
      console.log('Payout requested:', payoutId);
      setPayoutAmount('');
      setShowPayoutForm(false);
    } catch (error) {
      console.error('Payout request failed:', error);
      alert('Payout request failed. Please try again.');
    }
  };

  if (!wallet) {
    return <div>Loading wallet...</div>;
  }

  // Prepare chart data
  const chartData = transactions?.filter(t => t.type === 'order_funding')
    .slice(0, 10)
    .reverse()
    .map(t => ({
      date: new Date(t._creationTime).toLocaleDateString(),
      earnings: t.amount,
      balance: t.balanceAfter
    })) || [];

  return (
    <div className="space-y-6">
      {/* Wallet Overview */}
      <div className="grid grid-cols-1 md:grid-cols-4 gap-4">
        <div className="bg-white p-6 rounded-lg shadow">
          <div className="flex items-center">
            <div className="p-2 bg-green-100 rounded-lg">
              <svg className="w-6 h-6 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1" />
              </svg>
            </div>
            <div className="ml-4">
              <p className="text-sm font-medium text-gray-600">Available Balance</p>
              <p className="text-2xl font-bold text-green-600">${wallet.availableBalance.toFixed(2)}</p>
            </div>
          </div>
        </div>

        <div className="bg-white p-6 rounded-lg shadow">
          <div className="flex items-center">
            <div className="p-2 bg-yellow-100 rounded-lg">
              <svg className="w-6 h-6 text-yellow-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
              </svg>
            </div>
            <div className="ml-4">
              <p className="text-sm font-medium text-gray-600">Pending Balance</p>
              <p className="text-2xl font-bold text-yellow-600">${wallet.pendingBalance.toFixed(2)}</p>
            </div>
          </div>
        </div>

        <div className="bg-white p-6 rounded-lg shadow">
          <div className="flex items-center">
            <div className="p-2 bg-blue-100 rounded-lg">
              <svg className="w-6 h-6 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
              </svg>
            </div>
            <div className="ml-4">
              <p className="text-sm font-medium text-gray-600">Total Earnings</p>
              <p className="text-2xl font-bold text-blue-600">${wallet.totalEarnings.toFixed(2)}</p>
            </div>
          </div>
        </div>

        <div className="bg-white p-6 rounded-lg shadow">
          <div className="flex items-center">
            <div className="p-2 bg-purple-100 rounded-lg">
              <svg className="w-6 h-6 text-purple-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17 9V7a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2m2 4h10a2 2 0 002-2v-6a2 2 0 00-2-2H9a2 2 0 00-2 2v6a2 2 0 002 2zm7-5a2 2 0 11-4 0 2 2 0 014 0z" />
              </svg>
            </div>
            <div className="ml-4">
              <p className="text-sm font-medium text-gray-600">Total Payouts</p>
              <p className="text-2xl font-bold text-purple-600">${wallet.totalPayouts.toFixed(2)}</p>
            </div>
          </div>
        </div>
      </div>

      {/* Payout Request Section */}
      <div className="bg-white rounded-lg shadow p-6">
        <div className="flex justify-between items-center mb-4">
          <h2 className="text-lg font-semibold text-gray-900">Request Payout</h2>
          {!showPayoutForm && (
            <button
              onClick={() => setShowPayoutForm(true)}
              disabled={wallet.availableBalance < wallet.minPayoutAmount}
              className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed"
            >
              Request Payout
            </button>
          )}
        </div>

        {wallet.availableBalance < wallet.minPayoutAmount && (
          <div className="bg-yellow-50 p-4 rounded-lg mb-4">
            <p className="text-yellow-800 text-sm">
              ⚠️ Minimum payout amount is ${wallet.minPayoutAmount.toFixed(2)}. 
              You need ${(wallet.minPayoutAmount - wallet.availableBalance).toFixed(2)} more to request a payout.
            </p>
          </div>
        )}

        {showPayoutForm && (
          <div className="space-y-4">
            <div>
              <label className="block text-sm font-medium text-gray-700 mb-2">
                Payout Amount ($)
              </label>
              <input
                type="number"
                step="0.01"
                min={wallet.minPayoutAmount}
                max={wallet.availableBalance}
                value={payoutAmount}
                onChange={(e) => setPayoutAmount(e.target.value)}
                placeholder={`Min: $${wallet.minPayoutAmount} • Max: $${wallet.availableBalance.toFixed(2)}`}
                className="w-full p-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
              />
              <p className="text-xs text-gray-500 mt-1">
                Available balance: ${wallet.availableBalance.toFixed(2)}
              </p>
            </div>

            <div className="bg-gray-50 p-4 rounded">
              <h3 className="font-medium text-gray-900 mb-2">Payout Details</h3>
              <div className="text-sm space-y-1">
                <div className="flex justify-between">
                  <span>Requested Amount:</span>
                  <span>${parseFloat(payoutAmount || '0').toFixed(2)}</span>
                </div>
                <div className="flex justify-between">
                  <span>Processing Fee:</span>
                  <span>$5.00</span>
                </div>
                <div className="flex justify-between font-medium">
                  <span>Net Amount:</span>
                  <span>${Math.max(0, parseFloat(payoutAmount || '0') - 5).toFixed(2)}</span>
                </div>
              </div>
            </div>

            <div className="flex space-x-3">
              <button
                onClick={handlePayoutRequest}
                disabled={!payoutAmount || parseFloat(payoutAmount) < wallet.minPayoutAmount}
                className="flex-1 bg-green-600 text-white py-2 px-4 rounded hover:bg-green-700 disabled:opacity-50"
              >
                Request Payout
              </button>
              <button
                onClick={() => {
                  setShowPayoutForm(false);
                  setPayoutAmount('');
                }}
                className="px-4 py-2 text-gray-600 hover:text-gray-800"
              >
                Cancel
              </button>
            </div>
          </div>
        )}
      </div>

      {/* Earnings Chart */}
      {chartData.length > 0 && (
        <div className="bg-white rounded-lg shadow p-6">
          <h2 className="text-lg font-semibold text-gray-900 mb-4">Earnings Trend</h2>
          <ResponsiveContainer width="100%" height={300}>
            <LineChart data={chartData}>
              <CartesianGrid strokeDasharray="3 3" />
              <XAxis dataKey="date" />
              <YAxis />
              <Tooltip />
              <Line 
                type="monotone" 
                dataKey="balance" 
                stroke="#3B82F6" 
                strokeWidth={2}
                dot={{ fill: '#3B82F6' }}
              />
            </LineChart>
          </ResponsiveContainer>
        </div>
      )}

      {/* Recent Payouts */}
      {payoutHistory && payoutHistory.length > 0 && (
        <div className="bg-white rounded-lg shadow p-6">
          <h2 className="text-lg font-semibold text-gray-900 mb-4">Recent Payouts</h2>
          <div className="space-y-3">
            {payoutHistory.map((payout) => (
              <div key={payout._id} className="flex items-center justify-between p-4 bg-gray-50 rounded">
                <div>
                  <p className="font-medium text-gray-900">
                    ${payout.amount.toFixed(2)}
                  </p>
                  <p className="text-sm text-gray-600">
                    {new Date(payout.requestedAt).toLocaleDateString()}
                    {payout.transactionReference && ` • ${payout.transactionReference}`}
                  </p>
                </div>
                <span className={`px-3 py-1 rounded-full text-sm font-medium ${
                  payout.status === 'completed' ? 'bg-green-100 text-green-800' :
                  payout.status === 'processing' ? 'bg-blue-100 text-blue-800' :
                  payout.status === 'pending' ? 'bg-yellow-100 text-yellow-800' :
                  'bg-red-100 text-red-800'
                }`}>
                  {payout.status.charAt(0).toUpperCase() + payout.status.slice(1)}
                </span>
              </div>
            ))}
          </div>
        </div>
      )}

      {/* Transaction History */}
      {transactions && transactions.length > 0 && (
        <div className="bg-white rounded-lg shadow p-6">
          <h2 className="text-lg font-semibold text-gray-900 mb-4">Transaction History</h2>
          <div className="space-y-2">
            {transactions.map((transaction) => (
              <div key={transaction._id} className="flex items-center justify-between p-3 border-b last:border-b-0">
                <div>
                  <p className="font-medium text-gray-900">{transaction.description}</p>
                  <p className="text-sm text-gray-600">
                    {new Date(transaction._creationTime).toLocaleString()}
                  </p>
                </div>
                <div className="text-right">
                  <p className={`font-medium ${
                    transaction.amount > 0 ? 'text-green-600' : 'text-red-600'
                  }`}>
                    {transaction.amount > 0 ? '+' : ''}${transaction.amount.toFixed(2)}
                  </p>
                  <p className="text-sm text-gray-500">
                    Balance: ${transaction.balanceAfter.toFixed(2)}
                  </p>
                </div>
              </div>
            ))}
          </div>
        </div>
      )}
    </div>
  );
}

export default WalletDashboard;

Payout Processing Timeline

1

Request Submitted

Store owner submits payout request through dashboard
2

Admin Review

Platform admin reviews request and bank account details
3

Processing

Payment is initiated to store’s registered bank account
4

Completed

Funds transferred successfully, confirmation sent to store

Transaction Types

Order Funding

Type: order_funding
Description: Earnings from completed orders
Effect: Increases wallet balance

Commission Deduction

Type: commission_deduction
Description: Platform commission on orders
Effect: Decreases wallet balance

Payout

Type: payout
Description: Funds transferred to bank account
Effect: Decreases wallet balance

Payout Fee

Type: payout_fee
Description: Processing fee for payouts
Effect: Decreases wallet balance

Refund

Type: refund
Description: Order refunds processed
Effect: Decreases wallet balance

Adjustment

Type: adjustment
Description: Manual balance adjustments
Effect: Can increase or decrease balance

Error Handling

Status Code: 400
{
  "error": "Insufficient available balance",
  "requested": 1000.00,
  "available": 750.50
}
Status Code: 400
{
  "error": "Amount below minimum payout threshold",
  "requested": 25.00,
  "minimum": 50.00
}
Status Code: 400
{
  "error": "Bank account details required for payout",
  "message": "Please add your bank account information in store settings"
}
Status Code: 404
{
  "error": "Store wallet not found",
  "storeId": "j123456789"
}
Wallet balances are updated in real-time as orders are processed. Pending balances are held for a security period before becoming available for payout.
Set up bank account details before requesting payouts. Processing typically takes 1-3 business days depending on the bank and amount.