> ## Documentation Index
> Fetch the complete documentation index at: https://twigz.arqqin.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Store Operations

> Complete store lifecycle management from registration to approval

## Overview

The Store Operations API handles the complete store lifecycle including registration, updates, approval workflows, and wallet initialization. This system manages the onboarding process for new merchants and provides administrative tools for store management.

**Location:** `convex/stores/operations.ts`

## Store Lifecycle

<Steps>
  <Step title="Store Registration">
    Merchant registers their store with basic information and documents
  </Step>

  <Step title="Document Review">
    Admin reviews trade license and store information
  </Step>

  <Step title="Approval/Rejection">
    Admin approves or rejects the store application
  </Step>

  <Step title="Wallet Initialization">
    Approved stores get a wallet for payment processing
  </Step>

  <Step title="Store Goes Live">
    Store can start selling products to customers
  </Step>
</Steps>

## Register Store

Register a new store for the authenticated user.

<ParamField path="name" type="string" required>
  Store name for public display
</ParamField>

<ParamField path="primaryCategory" type="Id<'categories'>" required>
  Primary business category ID
</ParamField>

<ParamField path="logoId" type="Id<'_storage'>">
  Store logo image storage ID
</ParamField>

<ParamField path="coverImageId" type="Id<'_storage'>">
  Store cover/banner image storage ID
</ParamField>

<ParamField path="tradeLicenseId" type="Id<'_storage'>" required>
  Trade license document storage ID
</ParamField>

<CodeGroup>
  ```typescript TypeScript theme={null}
  const storeId = await convex.mutation(api.stores.operations.registerStore, {
    name: "Mario's Pizza",
    primaryCategory: "k123456789", // Food category ID
    logoId: "s123456789", // storage ID for logo
    coverImageId: "s987654321", // storage ID for cover image
    tradeLicenseId: "s456789123" // storage ID for trade license
  });
  ```

  ```javascript JavaScript theme={null}
  const storeId = await convex.mutation("stores.operations.registerStore", {
    name: "Mario's Pizza",
    primaryCategory: "k123456789",
    logoId: "s123456789",
    coverImageId: "s987654321",
    tradeLicenseId: "s456789123"
  });
  ```

  ```python Python theme={null}
  import requests

  response = requests.post(
      f"{CONVEX_URL}/api/mutation",
      json={
          "path": "stores.operations.registerStore",
          "args": {
              "name": "Mario's Pizza",
              "primaryCategory": "k123456789",
              "logoId": "s123456789",
              "coverImageId": "s987654321",
              "tradeLicenseId": "s456789123"
          }
      },
      headers={"Authorization": f"Bearer {token}"}
  )

  store_id = response.json()
  ```
</CodeGroup>

<ResponseExample>
  ```json Response theme={null}
  "j123456789"
  ```
</ResponseExample>

## Update Store

Update store information for the authenticated store owner.

<ParamField path="name" type="string">
  Updated store name
</ParamField>

<ParamField path="primaryCategory" type="Id<'categories'>">
  Updated primary category
</ParamField>

<ParamField path="logoId" type="Id<'_storage'>">
  Updated store logo
</ParamField>

<ParamField path="coverImageId" type="Id<'_storage'>">
  Updated cover image
</ParamField>

<ParamField path="tradeLicenseId" type="Id<'_storage'>">
  Updated trade license
</ParamField>

<ParamField path="status" type="'pending' | 'approved' | 'rejected'">
  Store status (admin only)
</ParamField>

<ParamField path="statusReason" type="string">
  Status reason or notes
</ParamField>

<ParamField path="settings" type="object">
  Store operational settings including delivery, pickup, and live status
</ParamField>

<ParamField path="bankAccount" type="object">
  Bank account details for payouts
</ParamField>

<ParamField path="address" type="object">
  Store physical address and location details

  <Expandable title="Address Fields">
    <ParamField path="fullAddress" type="string">
      Complete formatted address string
    </ParamField>

    <ParamField path="city" type="Id<'cities'>">
      City ID reference
    </ParamField>

    <ParamField path="area" type="Id<'areas'>">
      Area/district ID reference
    </ParamField>

    <ParamField path="flatVilaNumber" type="string">
      Flat or villa number
    </ParamField>

    <ParamField path="buildingNameNumber" type="string">
      Building name or number
    </ParamField>

    <ParamField path="landmark" type="string">
      Nearby landmark for easier location
    </ParamField>

    <ParamField path="latitude" type="string">
      GPS latitude coordinate
    </ParamField>

    <ParamField path="longitude" type="string">
      GPS longitude coordinate
    </ParamField>
  </Expandable>
</ParamField>

<CodeGroup>
  ```typescript TypeScript theme={null}
  const updatedStore = await convex.mutation(api.stores.operations.updateStore, {
    name: "Mario's Authentic Pizza",
    settings: {
      deliveryEnabled: true,
      pickupEnabled: true,
      storeLive: true
    },
    bankAccount: {
      IBAN: "AE123456789012345678901",
      bankName: "Emirates NBD",
      bankId: "b123456789",
      accountHolderName: "Mario Rossi"
    },
    address: {
      fullAddress: "123 Main Street, Business Bay, Dubai, UAE",
      city: "c123456789", // Dubai city ID
      area: "a987654321", // Business Bay area ID
      flatVilaNumber: "Villa 123",
      buildingNameNumber: "Marina Plaza",
      landmark: "Near Dubai Mall",
      latitude: "25.1972",
      longitude: "55.2744"
    }
  });
  ```

  ```javascript JavaScript theme={null}
  const updatedStore = await convex.mutation("stores.operations.updateStore", {
    name: "Mario's Authentic Pizza",
    settings: {
      deliveryEnabled: true,
      pickupEnabled: false,
      storeLive: true
    }
  });
  ```
</CodeGroup>

<ResponseExample>
  ```json Response theme={null}
  {
    "_id": "j123456789",
    "name": "Mario's Authentic Pizza",
    "primaryCategory": "k123456789",
    "status": "approved",
    "logoUrl": "https://storage.convex.dev/...",
    "coverImageUrl": "https://storage.convex.dev/...",
    "settings": {
      "deliveryEnabled": true,
      "pickupEnabled": true,
      "storeLive": true
    },
    "bankAccount": {
      "IBAN": "AE123456789012345678901",
      "bankName": "Emirates NBD",
      "accountHolderName": "Mario Rossi"
    },
    "address": {
      "fullAddress": "123 Main Street, Business Bay, Dubai, UAE",
      "city": {
        "_id": "c123456789",
        "name": "Dubai",
        "nameArabic": "دبي"
      },
      "area": {
        "_id": "a987654321",
        "name": "Business Bay",
        "city": "c123456789"
      },
      "flatVilaNumber": "Villa 123",
      "buildingNameNumber": "Marina Plaza",
      "landmark": "Near Dubai Mall",
      "latitude": "25.1972",
      "longitude": "55.2744"
    },
    "_creationTime": 1640995200000
  }
  ```
</ResponseExample>

## Update Store Address

Update only the address information for the authenticated store owner.

<ParamField path="fullAddress" type="string">
  Complete formatted address string
</ParamField>

<ParamField path="city" type="Id<'cities'>">
  City ID reference
</ParamField>

<ParamField path="area" type="Id<'areas'>">
  Area/district ID reference
</ParamField>

<ParamField path="flatVilaNumber" type="string">
  Flat or villa number
</ParamField>

<ParamField path="buildingNameNumber" type="string">
  Building name or number
</ParamField>

<ParamField path="landmark" type="string">
  Nearby landmark for easier location
</ParamField>

<ParamField path="latitude" type="string">
  GPS latitude coordinate
</ParamField>

<ParamField path="longitude" type="string">
  GPS longitude coordinate
</ParamField>

<CodeGroup>
  ```typescript TypeScript theme={null}
  const addressResult = await convex.mutation(api.stores.operations.updateStoreAddress, {
    fullAddress: "456 New Address Street, Marina, Dubai, UAE",
    city: "c123456789", // Dubai city ID
    area: "a555666777", // Marina area ID
    flatVilaNumber: "Apartment 204",
    buildingNameNumber: "Ocean View Tower",
    landmark: "Next to Marina Mall",
    latitude: "25.0657",
    longitude: "55.1713"
  });
  ```

  ```javascript JavaScript theme={null}
  const addressResult = await convex.mutation("stores.operations.updateStoreAddress", {
    fullAddress: "456 New Address Street, Marina, Dubai, UAE",
    city: "c123456789",
    area: "a555666777",
    flatVilaNumber: "Apartment 204",
    buildingNameNumber: "Ocean View Tower",
    landmark: "Next to Marina Mall",
    latitude: "25.0657",
    longitude: "55.1713"
  });
  ```

  ```python Python theme={null}
  import requests

  response = requests.post(
      f"{CONVEX_URL}/api/mutation",
      json={
          "path": "stores.operations.updateStoreAddress",
          "args": {
              "fullAddress": "456 New Address Street, Marina, Dubai, UAE",
              "city": "c123456789",
              "area": "a555666777",
              "flatVilaNumber": "Apartment 204",
              "buildingNameNumber": "Ocean View Tower",
              "landmark": "Next to Marina Mall",
              "latitude": "25.0657",
              "longitude": "55.1713"
          }
      },
      headers={"Authorization": f"Bearer {token}"}
  )

  address_result = response.json()
  ```
</CodeGroup>

<ResponseExample>
  ```json Response theme={null}
  {
    "_id": "j123456789",
    "address": {
      "fullAddress": "456 New Address Street, Marina, Dubai, UAE",
      "city": {
        "_id": "c123456789",
        "name": "Dubai",
        "nameArabic": "دبي"
      },
      "area": {
        "_id": "a555666777",
        "name": "Marina",
        "city": "c123456789"
      },
      "flatVilaNumber": "Apartment 204",
      "buildingNameNumber": "Ocean View Tower",
      "landmark": "Next to Marina Mall",
      "latitude": "25.0657",
      "longitude": "55.1713"
    }
  }
  ```
</ResponseExample>

## Approve Store (Admin)

Admin function to approve a store application and initialize its wallet.

<ParamField path="storeId" type="Id<'stores'>" required>
  Store ID to approve
</ParamField>

<ParamField path="statusReason" type="string">
  Reason for approval (optional)
</ParamField>

<CodeGroup>
  ```typescript TypeScript theme={null}
  const approvalResult = await convex.mutation(api.stores.operations.approveStore, {
    storeId: "j123456789",
    statusReason: "All documents verified and approved"
  });
  ```

  ```javascript JavaScript theme={null}
  const approvalResult = await convex.mutation("stores.operations.approveStore", {
    storeId: "j123456789",
    statusReason: "Trade license verified, ready to go live"
  });
  ```
</CodeGroup>

<ResponseExample>
  ```json Response theme={null}
  {
    "storeId": "j123456789",
    "walletId": "w123456789",
    "message": "Store approved successfully and wallet initialized",
    "notificationSent": true
  }
  ```
</ResponseExample>

## Reject Store (Admin)

Admin function to reject a store application.

<ParamField path="storeId" type="Id<'stores'>" required>
  Store ID to reject
</ParamField>

<ParamField path="rejectionReason" type="string" required>
  Detailed reason for rejection
</ParamField>

<CodeGroup>
  ```typescript TypeScript theme={null}
  await convex.mutation(api.stores.operations.rejectStore, {
    storeId: "j123456789",
    rejectionReason: "Trade license document is expired. Please upload a valid license."
  });
  ```

  ```javascript JavaScript theme={null}
  await convex.mutation("stores.operations.rejectStore", {
    storeId: "j123456789",
    rejectionReason: "Missing required business registration documents"
  });
  ```
</CodeGroup>

<ResponseExample>
  ```json Response theme={null}
  {
    "storeId": "j123456789",
    "message": "Store rejected successfully",
    "notificationSent": true
  }
  ```
</ResponseExample>

## Initialize Store Wallet

Initialize wallet for an existing approved store.

<ParamField path="storeId" type="Id<'stores'>" required>
  Store ID to initialize wallet for
</ParamField>

<CodeGroup>
  ```typescript TypeScript theme={null}
  const walletResult = await convex.mutation(api.stores.operations.initializeExistingStoreWallet, {
    storeId: "j123456789"
  });
  ```

  ```javascript JavaScript theme={null}
  const walletResult = await convex.mutation("stores.operations.initializeExistingStoreWallet", {
    storeId: "j123456789"
  });
  ```
</CodeGroup>

<ResponseExample>
  ```json Response theme={null}
  {
    "walletId": "w123456789",
    "message": "Wallet initialized successfully for store"
  }
  ```
</ResponseExample>

## Bulk Initialize Wallets (Admin)

Admin function to bulk initialize wallets for all approved stores without wallets.

<CodeGroup>
  ```typescript TypeScript theme={null}
  const bulkResult = await convex.mutation(api.stores.operations.bulkInitializeWallets);
  ```

  ```javascript JavaScript theme={null}
  const bulkResult = await convex.mutation("stores.operations.bulkInitializeWallets");
  ```
</CodeGroup>

<ResponseExample>
  ```json Response theme={null}
  {
    "processedStores": 25,
    "walletsCreated": 23,
    "errors": [
      {
        "storeId": "j987654321",
        "storeName": "Test Store",
        "error": "Store not approved"
      },
      {
        "storeId": "j555666777",
        "storeName": "Another Store",
        "error": "Wallet already exists"
      }
    ]
  }
  ```
</ResponseExample>

## Store Registration Form

Complete store registration component:

<CodeGroup>
  ```typescript React Component theme={null}
  import { useState } from 'react';
  import { useMutation, useQuery } from 'convex/react';
  import { api } from './convex/_generated/api';

  interface StoreRegistrationProps {
    onSuccess?: (storeId: string) => void;
  }

  function StoreRegistrationForm({ onSuccess }: StoreRegistrationProps) {
    const [formData, setFormData] = useState({
      name: '',
      primaryCategory: '',
      logoFile: null as File | null,
      coverImageFile: null as File | null,
      tradeLicenseFile: null as File | null
    });
    const [uploading, setUploading] = useState(false);
    const [errors, setErrors] = useState<Record<string, string>>({});

    const categories = useQuery(api.shared.categories.getParents);
    const generateUploadUrl = useMutation(api.shared.utils.generateUploadUrl);
    const registerStore = useMutation(api.stores.operations.registerStore);

    const uploadFile = async (file: File): Promise<string> => {
      const uploadUrl = await generateUploadUrl();
      const response = await fetch(uploadUrl, {
        method: 'POST',
        body: file
      });
      const { storageId } = await response.json();
      return storageId;
    };

    const handleSubmit = async (e: React.FormEvent) => {
      e.preventDefault();
      setUploading(true);
      setErrors({});

      try {
        // Validation
        const newErrors: Record<string, string> = {};
        if (!formData.name.trim()) newErrors.name = 'Store name is required';
        if (!formData.primaryCategory) newErrors.primaryCategory = 'Category is required';
        if (!formData.tradeLicenseFile) newErrors.tradeLicense = 'Trade license is required';

        if (Object.keys(newErrors).length > 0) {
          setErrors(newErrors);
          return;
        }

        // Upload files
        const tradeLicenseId = await uploadFile(formData.tradeLicenseFile!);
        const logoId = formData.logoFile ? await uploadFile(formData.logoFile) : undefined;
        const coverImageId = formData.coverImageFile ? await uploadFile(formData.coverImageFile) : undefined;

        // Register store
        const storeId = await registerStore({
          name: formData.name.trim(),
          primaryCategory: formData.primaryCategory,
          logoId,
          coverImageId,
          tradeLicenseId
        });

        onSuccess?.(storeId);

      } catch (error) {
        console.error('Store registration failed:', error);
        setErrors({ submit: 'Registration failed. Please try again.' });
      } finally {
        setUploading(false);
      }
    };

    return (
      <form onSubmit={handleSubmit} className="max-w-2xl mx-auto space-y-6">
        <div className="bg-blue-50 p-4 rounded-lg">
          <h2 className="text-xl font-bold text-blue-900 mb-2">Register Your Store</h2>
          <p className="text-blue-700 text-sm">
            Complete this form to register your store. All information will be reviewed by our team.
          </p>
        </div>

        {/* Store Name */}
        <div>
          <label className="block text-sm font-medium text-gray-700 mb-2">
            Store Name <span className="text-red-500">*</span>
          </label>
          <input
            type="text"
            value={formData.name}
            onChange={(e) => {
              setFormData(prev => ({ ...prev, name: e.target.value }));
              setErrors(prev => ({ ...prev, name: '' }));
            }}
            placeholder="Enter your store name"
            className="w-full p-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
            disabled={uploading}
          />
          {errors.name && <p className="text-red-500 text-sm mt-1">{errors.name}</p>}
        </div>

        {/* Primary Category */}
        <div>
          <label className="block text-sm font-medium text-gray-700 mb-2">
            Primary Category <span className="text-red-500">*</span>
          </label>
          <select
            value={formData.primaryCategory}
            onChange={(e) => {
              setFormData(prev => ({ ...prev, primaryCategory: e.target.value }));
              setErrors(prev => ({ ...prev, primaryCategory: '' }));
            }}
            className="w-full p-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
            disabled={uploading}
          >
            <option value="">Select a category</option>
            {categories?.map((category) => (
              <option key={category._id} value={category._id}>
                {category.name}
              </option>
            ))}
          </select>
          {errors.primaryCategory && <p className="text-red-500 text-sm mt-1">{errors.primaryCategory}</p>}
        </div>

        {/* Logo Upload */}
        <div>
          <label className="block text-sm font-medium text-gray-700 mb-2">
            Store Logo (Optional)
          </label>
          <input
            type="file"
            accept="image/*"
            onChange={(e) => setFormData(prev => ({ ...prev, logoFile: e.target.files?.[0] || null }))}
            className="w-full p-3 border border-gray-300 rounded-lg"
            disabled={uploading}
          />
          <p className="text-xs text-gray-500 mt-1">
            Recommended: Square image, minimum 200x200px
          </p>
        </div>

        {/* Cover Image Upload */}
        <div>
          <label className="block text-sm font-medium text-gray-700 mb-2">
            Cover Image (Optional)
          </label>
          <input
            type="file"
            accept="image/*"
            onChange={(e) => setFormData(prev => ({ ...prev, coverImageFile: e.target.files?.[0] || null }))}
            className="w-full p-3 border border-gray-300 rounded-lg"
            disabled={uploading}
          />
          <p className="text-xs text-gray-500 mt-1">
            Recommended: 1200x400px banner image
          </p>
        </div>

        {/* Trade License Upload */}
        <div>
          <label className="block text-sm font-medium text-gray-700 mb-2">
            Trade License <span className="text-red-500">*</span>
          </label>
          <input
            type="file"
            accept=".pdf,.jpg,.jpeg,.png"
            onChange={(e) => {
              setFormData(prev => ({ ...prev, tradeLicenseFile: e.target.files?.[0] || null }));
              setErrors(prev => ({ ...prev, tradeLicense: '' }));
            }}
            className="w-full p-3 border border-gray-300 rounded-lg"
            disabled={uploading}
          />
          <p className="text-xs text-gray-500 mt-1">
            Upload a clear copy of your trade license (PDF, JPG, PNG)
          </p>
          {errors.tradeLicense && <p className="text-red-500 text-sm mt-1">{errors.tradeLicense}</p>}
        </div>

        {/* Submit Button */}
        <div>
          <button
            type="submit"
            disabled={uploading}
            className="w-full bg-blue-600 text-white py-3 px-4 rounded-lg hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center"
          >
            {uploading ? (
              <>
                <svg className="animate-spin -ml-1 mr-3 h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
                  <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
                  <path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
                </svg>
                Registering Store...
              </>
            ) : (
              'Register Store'
            )}
          </button>
        </div>

        {errors.submit && (
          <div className="bg-red-50 p-4 rounded-lg">
            <p className="text-red-700 text-sm">{errors.submit}</p>
          </div>
        )}

        <div className="bg-gray-50 p-4 rounded-lg">
          <h3 className="font-medium text-gray-900 mb-2">What happens next?</h3>
          <ol className="text-sm text-gray-600 space-y-1 list-decimal list-inside">
            <li>Your application will be reviewed by our team</li>
            <li>We'll verify your trade license and business details</li>
            <li>You'll receive approval/rejection notification within 24-48 hours</li>
            <li>Once approved, you can start adding products and go live</li>
          </ol>
        </div>
      </form>
    );
  }

  export default StoreRegistrationForm;
  ```
</CodeGroup>

## Store Status Management

Store applications go through several status states:

<CardGroup cols={2}>
  <Card title="Pending" icon="clock">
    Initial status when store is registered. Awaiting admin review.
  </Card>

  <Card title="Under Review" icon="magnifying-glass">
    Admin is actively reviewing documents and store information.
  </Card>

  <Card title="Approved" icon="check-circle">
    Store is approved and can start selling. Wallet is automatically initialized.
  </Card>

  <Card title="Rejected" icon="x-circle">
    Store application rejected. Reason provided for resubmission.
  </Card>
</CardGroup>

## Admin Review Process

For administrators reviewing store applications:

```typescript theme={null}
// Review and approve a store
const handleApproval = async (storeId: string) => {
  try {
    const result = await convex.mutation(api.stores.operations.approveStore, {
      storeId,
      statusReason: "All documents verified. Welcome to the marketplace!"
    });
    
    console.log(`Store approved: ${result.storeId}`);
    console.log(`Wallet created: ${result.walletId}`);
    
    // Send notification to store owner
    await convex.action(api.shared.notifications.sendNotificationToUser, {
      userId: storeOwnerId,
      title: "🎉 Store Approved!",
      body: "Your store has been approved and is now live",
      data: {
        type: "store_status",
        storeId: result.storeId,
        status: "approved"
      }
    });
    
  } catch (error) {
    console.error('Approval failed:', error);
  }
};

// Review and reject a store
const handleRejection = async (storeId: string, reason: string) => {
  try {
    await convex.mutation(api.stores.operations.rejectStore, {
      storeId,
      rejectionReason: reason
    });
    
    // Send notification to store owner
    await convex.action(api.shared.notifications.sendNotificationToUser, {
      userId: storeOwnerId,
      title: "Store Application Update",
      body: "Your store application needs attention",
      data: {
        type: "store_status",
        storeId,
        status: "rejected"
      }
    });
    
  } catch (error) {
    console.error('Rejection failed:', error);
  }
};
```

## Error Handling

<AccordionGroup>
  <Accordion title="Store already exists">
    **Status Code**: `400`

    ```json theme={null}
    {
      "error": "User already has a registered store",
      "existingStoreId": "j123456789"
    }
    ```
  </Accordion>

  <Accordion title="Invalid category">
    **Status Code**: `404`

    ```json theme={null}
    {
      "error": "Primary category not found",
      "categoryId": "k123456789"
    }
    ```
  </Accordion>

  <Accordion title="Missing trade license">
    **Status Code**: `400`

    ```json theme={null}
    {
      "error": "Trade license document is required for store registration"
    }
    ```
  </Accordion>

  <Accordion title="Store not found">
    **Status Code**: `404`

    ```json theme={null}
    {
      "error": "Store not found or access denied",
      "storeId": "j123456789"
    }
    ```
  </Accordion>
</AccordionGroup>

<Note>
  Store registration requires a valid trade license document. The approval process typically takes 24-48 hours for document verification.
</Note>

<Tip>
  Upload high-quality images for better store presentation. Square logos (200x200px minimum) and banner cover images (1200x400px) work best.
</Tip>
