# Products Management Service

# Product Management

**Author**: Josh S. Sakweli, Backend Lead Team  
**Last Updated**: 2026-05-19  
**Version**: v2.1

**Short Description**: The Product Management API provides comprehensive functionality for managing products within shops on the NextGate platform. It supports group buying, installment payment plans, color variations, specifications, digital product file delivery, product preview media (video, PDF, 3D, image), and comprehensive search/filter capabilities with role-based access control.

**Hints**:
- All shop-related endpoints use the prefix `api/v1/e-commerce/shops/{shopId}/products`
- Shop owners and system admins have full product management access
- Public users can only view active products from approved, active shops
- Products have a `productType`: `PHYSICAL` (shipping + delivery confirmation) or `DIGITAL` (instant download after payment)
- Action parameter (`SAVE_DRAFT`/`SAVE_PUBLISH`) determines product status on create/update
- Search supports multi-word queries with enhanced pattern matching across multiple fields
- Pagination is 1-indexed (page parameter starts from 1)
- Products use soft-delete (marked as deleted rather than permanently removed, except drafts)
- Product slugs are unique within each shop scope
- SKUs are auto-generated using shop, category, brand, and sequence information
- Group buying requires a maximum participant count, a group price, and a time limit
- Installment plans are managed separately via the Installment Plan Config endpoints
- Color variations can have individual price adjustments and separate image sets
- Digital products require uploading files via the Digital File Management endpoints before buyers can download
- Products can have a single preview (`previewType` + `previewUrl`) uploaded via the Product Preview endpoints — visible publicly before purchase, stored in a public bucket separate from private digital content
- Preview supports: `VIDEO`, `PDF`, `THREE_D`, `IMAGE`. Null means no preview. Managed via presign → upload → confirm flow

---

## Endpoints

## 1. Create Product
**Purpose**: Creates a new product in a shop, supporting group buying, color variations, specifications, and digital product rules.

**Endpoint**: <span style="background-color: #28a745; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">POST</span> `api/v1/e-commerce/shops/{shopId}/products`

**Access Level**: 🔒 Protected (Requires shop owner or system admin role)

**Authentication**: Bearer Token

**Request Headers**:
| Header | Type | Required | Description |
|--------|------|----------|-------------|
| Authorization | string | Yes | Bearer token |
| Content-Type | string | Yes | application/json |

**Path Parameters**:
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| shopId | UUID | Yes | ID of the shop |

**Query Parameters**:
| Parameter | Type | Required | Default | Description |
|-----------|------|----------|---------|-------------|
| action | ReqAction | Yes | — | `SAVE_DRAFT` or `SAVE_PUBLISH` |

**Request Body Parameters**:
| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| productType | ProductType | Yes | `PHYSICAL` or `DIGITAL` | Required |
| productName | string | Yes | Unique name within shop | Min: 2, Max: 100 chars |
| productDescription | string | Yes | Detailed description | Min: 10, Max: 1000 chars |
| price | decimal | Yes | Selling price | Min: 0.01, max 8 digits + 2 decimal places |
| stockQuantity | integer | Yes | Available stock | Min: 0 |
| categoryId | UUID | Yes | Product category | Must exist and be active |
| comparePrice | decimal | No | Original price for discount display | Must be > price if provided |
| lowStockThreshold | integer | No | Low stock alert threshold | Min: 1, Max: 1000, Default: 5 |
| condition | ProductCondition | No | Product condition | `NEW`, `USED_LIKE_NEW`, `USED_GOOD`, `USED_FAIR`, `REFURBISHED`, `FOR_PARTS` |
| status | ProductStatus | No | Initial status (overridden by `action`) | Default: `ACTIVE` |
| productImages | array | Yes | Product image URLs | Valid URLs, at least 1 required |
| specifications | object | No | Key-value specs | Key max 100 chars, Value max 500 chars |
| colors | array | No | Color variations | See Color object below |
| minOrderQuantity | integer | No | Minimum order qty | Min: 1, Default: 1 |
| maxOrderQuantity | integer | No | Maximum order qty per order | Min: 1, must be ≥ minOrderQuantity |
| groupBuyingEnabled | boolean | No | Enable group buying | Default: false |
| groupMaxSize | integer | No | Maximum group participants | Min: 2, required if groupBuyingEnabled |
| groupPrice | decimal | No | Discounted group price | Must be < price |
| groupTimeLimitHours | integer | No | Group formation time limit | Min: 1, Max: 8760 |
| downloadExpiryDays | integer | No | Download link expiry (DIGITAL only) | Min: 1, Default: 7 |
| maxDownloadsPerBuyer | integer | No | Download attempts per buyer (DIGITAL only) | Min: 1 |
| maxQuantityForDigital | integer | No | Purchase cap per buyer (DIGITAL only) | Min: 1 |

**Color Object**:
| Field | Type | Required | Validation |
|-------|------|----------|------------|
| name | string | Yes | Max: 50 chars |
| hex | string | Yes | Valid `#RRGGBB` format |
| images | array | No | Valid URLs |
| priceAdjustment | decimal | No | Min: 0.0, Default: 0 |

**Request JSON Sample (PHYSICAL)**:
```json
{
  "productType": "PHYSICAL",
  "productName": "iPhone 15 Pro Max 256GB",
  "productDescription": "The most advanced iPhone featuring the A17 Pro chip and titanium design.",
  "price": 1199.00,
  "comparePrice": 1299.00,
  "stockQuantity": 25,
  "lowStockThreshold": 5,
  "categoryId": "123e4567-e89b-12d3-a456-426614174000",
  "condition": "NEW",
  "productImages": [
    "https://example.com/images/iphone15-main.jpg"
  ],
  "specifications": {
    "Display": "6.7-inch Super Retina XDR OLED",
    "Chip": "A17 Pro"
  },
  "colors": [
    {
      "name": "Natural Titanium",
      "hex": "#F5F5DC",
      "images": ["https://example.com/colors/natural-titanium.jpg"],
      "priceAdjustment": 0.00
    }
  ],
  "minOrderQuantity": 1,
  "maxOrderQuantity": 3,
  "groupBuyingEnabled": true,
  "groupMaxSize": 50,
  "groupPrice": 1099.00,
  "groupTimeLimitHours": 72
}
```

**Request JSON Sample (DIGITAL)**:
```json
{
  "productType": "DIGITAL",
  "productName": "UI Design Kit Pro",
  "productDescription": "A comprehensive Figma component library with 500+ components.",
  "price": 49.00,
  "stockQuantity": 1000,
  "categoryId": "123e4567-e89b-12d3-a456-426614174000",
  "productImages": ["https://example.com/images/design-kit-preview.jpg"],
  "downloadExpiryDays": 30,
  "maxDownloadsPerBuyer": 5,
  "maxQuantityForDigital": 1
}
```

**Response JSON Sample**:
```json
{
  "success": true,
  "message": "Product created successfully",
  "data": null
}
```

**Business Rules**:
- Product name must be unique within the shop
- `comparePrice` must be greater than `price`
- `groupPrice` must be less than `price`
- All group buying settings (`groupMaxSize`, `groupPrice`, `groupTimeLimitHours`) required when `groupBuyingEnabled=true`
- `maxOrderQuantity` must be ≥ `minOrderQuantity`
- Digital download fields (`downloadExpiryDays`, `maxDownloadsPerBuyer`, `maxQuantityForDigital`) only apply to `DIGITAL` products
- After creation, add installment plans via [Installment Plan Config](#15-installment-plan-config) and digital files via [Digital File Management](#16-digital-file-management)

**Error Responses**:
- `400`: Validation errors or business rule violations
- `401`: Authentication required
- `403`: Insufficient permissions
- `404`: Shop or category not found
- `409`: Product with same name already exists in shop
- `422`: Field-level validation errors

---

## 2. Update Product
**Purpose**: Updates an existing product. Only provided fields are updated.

**Endpoint**: <span style="background-color: #ffc107; color: black; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">PUT</span> `api/v1/e-commerce/shops/{shopId}/products/{productId}`

**Access Level**: 🔒 Protected (Requires shop owner or system admin role)

**Authentication**: Bearer Token

**Path Parameters**:
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| shopId | UUID | Yes | ID of the shop |
| productId | UUID | Yes | ID of the product |

**Query Parameters**:
| Parameter | Type | Required | Default | Description |
|-----------|------|----------|---------|-------------|
| action | ReqAction | Yes | — | `SAVE_DRAFT` or `SAVE_PUBLISH` |

**Request Body Parameters** (all optional):
| Parameter | Type | Description | Validation |
|-----------|------|-------------|------------|
| productName | string | Updated name | Min: 2, Max: 100 chars |
| productDescription | string | Updated description | Min: 10, Max: 1000 chars |
| price | decimal | Updated selling price | Min: 0.01 |
| comparePrice | decimal | Updated compare price | Must be > price |
| stockQuantity | integer | Updated stock | Min: 0 |
| lowStockThreshold | integer | Updated low stock threshold | Min: 1, Max: 1000 |
| condition | ProductCondition | Updated condition | See enum values |
| status | ProductStatus | Updated status | Overridden by `action` |
| urgencyTag | UrgencyTag | Urgency badge on product | `NONE`, `NEW_ARRIVAL`, `LIMITED_EDITION`, `LIMITED_OFFER`, `FEW_REMAINS` |
| categoryId | UUID | Updated category | Must exist and be active |
| productImages | array | Updated image URLs | Valid URLs, replaces existing |
| specifications | object | Updated specifications | Completely replaces existing |
| colors | array | Updated color variations | Completely replaces existing |
| minOrderQuantity | integer | Updated min order qty | Min: 1 |
| maxOrderQuantity | integer | Updated max order qty | Min: 1, must be ≥ min |
| groupBuyingEnabled | boolean | Enable/disable group buying | — |
| groupMaxSize | integer | Updated group max | Min: 2 |
| groupPrice | decimal | Updated group price | Must be < price |
| groupTimeLimitHours | integer | Updated group time limit | Min: 1, Max: 8760 |
| installmentEnabled | boolean | Enable/disable installment feature toggle | — |
| maxQuantityForInstallment | integer | Max qty a buyer can purchase on installment | Min: 1 |
| showStockAvailableToPublic | boolean | Show available stock count publicly | — |
| showSoldCountToPublic | boolean | Show sold count publicly | — |
| clearPreview | boolean | Set `true` to remove the product's preview file and type | — |
| previewDownloadable | boolean | Allow/disallow viewers from downloading the preview file | — |

**Response JSON Sample**:
```json
{
  "success": true,
  "message": "Product updated successfully and published",
  "data": {
    "productId": "456e7890-e89b-12d3-a456-426614174001",
    "productName": "iPhone 15 Pro Max 512GB",
    "productSlug": "iphone-15-pro-max-512gb",
    "price": 1399.00,
    "status": "ACTIVE",
    "updatedAt": "2026-05-19T14:30:00Z"
  }
}
```

**Error Responses**:
- `400`: Invalid update data or validation errors
- `401`: Authentication required
- `403`: Insufficient permissions
- `404`: Shop or product not found
- `409`: Updated product name already exists

---

## 3. Publish Product
**Purpose**: Publishes a draft product making it active and publicly available.

**Endpoint**: <span style="background-color: #6f42c1; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">PATCH</span> `api/v1/e-commerce/shops/{shopId}/products/{productId}/publish`

**Access Level**: 🔒 Protected (Requires shop owner or system admin role)

**Authentication**: Bearer Token

**Path Parameters**:
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| shopId | UUID | Yes | ID of the shop |
| productId | UUID | Yes | ID of the draft product |

**Response JSON Sample**:
```json
{
  "success": true,
  "message": "Product 'iPhone 15 Pro Max' published successfully",
  "data": {
    "productId": "456e7890-e89b-12d3-a456-426614174001",
    "productName": "iPhone 15 Pro Max",
    "status": "ACTIVE",
    "publishedAt": "2026-05-19T14:45:00Z"
  }
}
```

**Publishing Requirements**:
- Product name, description, price, stock quantity, category, and at least one image must be present
- Group buying settings complete if enabled
- Installment plans present if installment is enabled

**Error Responses**:
- `400`: Product already published or missing required publish fields
- `401`: Authentication required
- `403`: Insufficient permissions
- `404`: Shop or product not found

---

## 4. Delete Product
**Purpose**: Deletes a product. Draft products are hard-deleted; published products are soft-deleted.

**Endpoint**: <span style="background-color: #dc3545; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">DELETE</span> `api/v1/e-commerce/shops/{shopId}/products/{productId}`

**Access Level**: 🔒 Protected (Requires shop owner or system admin role)

**Authentication**: Bearer Token

**Path Parameters**:
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| shopId | UUID | Yes | ID of the shop |
| productId | UUID | Yes | ID of the product |

**Response JSON Sample (Soft Delete)**:
```json
{
  "success": true,
  "message": "Product 'iPhone 15 Pro Max' has been deleted and will be permanently removed after 30 days",
  "data": {
    "productName": "iPhone 15 Pro Max",
    "productId": "456e7890-e89b-12d3-a456-426614174001",
    "previousStatus": "ACTIVE",
    "deletedAt": "2026-05-19T15:00:00Z",
    "deletionType": "SOFT_DELETE"
  }
}
```

**Response JSON Sample (Hard Delete)**:
```json
{
  "success": true,
  "message": "Draft product 'iPhone 15 Pro Max' has been permanently deleted",
  "data": null
}
```

**Deletion Logic**:
- **Draft products**: Hard delete (permanently removed)
- **Published products**: Soft delete (status → ARCHIVED, 30-day recovery window)

**Error Responses**:
- `401`: Authentication required
- `403`: Insufficient permissions
- `404`: Shop or product not found

---

## 5. Restore Product
**Purpose**: Restores a soft-deleted product back to draft status.

**Endpoint**: <span style="background-color: #6f42c1; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">PATCH</span> `api/v1/e-commerce/shops/{shopId}/products/{productId}/restore`

**Access Level**: 🔒 Protected (Requires shop owner or system admin role)

**Authentication**: Bearer Token

**Path Parameters**:
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| shopId | UUID | Yes | ID of the shop |
| productId | UUID | Yes | ID of the soft-deleted product |

**Response JSON Sample**:
```json
{
  "success": true,
  "message": "Product 'iPhone 15 Pro Max' has been restored successfully",
  "data": {
    "productId": "456e7890-e89b-12d3-a456-426614174001",
    "productName": "iPhone 15 Pro Max",
    "status": "DRAFT",
    "restoredAt": "2026-05-19T15:30:00Z",
    "note": "Product restored as draft. Publish to make it active again."
  }
}
```

**Error Responses**:
- `400`: Product is not deleted
- `401`: Authentication required
- `403`: Insufficient permissions
- `404`: Shop or product not found

---

## 6. Get Product Detailed (Owner/Admin View)
**Purpose**: Retrieves comprehensive product details including all management information.

**Endpoint**: <span style="background-color: #007bff; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">GET</span> `api/v1/e-commerce/shops/{shopId}/products/{productId}/detailed`

**Access Level**: 🔒 Protected (Requires shop owner or system admin role)

**Authentication**: Bearer Token

**Path Parameters**:
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| shopId | UUID | Yes | ID of the shop |
| productId | UUID | Yes | ID of the product |

**Response JSON Sample**:
```json
{
  "success": true,
  "message": "Product details retrieved successfully",
  "data": {
    "productId": "456e7890-e89b-12d3-a456-426614174001",
    "productName": "iPhone 15 Pro Max 512GB",
    "productSlug": "iphone-15-pro-max-512gb",
    "productType": "PHYSICAL",
    "productDescription": "The most advanced iPhone ever...",
    "productImages": ["https://example.com/images/iphone15-main.jpg"],
    "price": 1199.00,
    "comparePrice": 1299.00,
    "discountAmount": 100.00,
    "discountPercentage": 7.69,
    "isOnSale": true,
    "stockQuantity": 25,
    "isInStock": true,
    "isLowStock": false,
    "sku": "SHP12345678-ELE-APP-512-0001",
    "condition": "NEW",
    "status": "ACTIVE",
    "urgencyTag": "NONE",
    "shopId": "123e4567-e89b-12d3-a456-426614174000",
    "shopName": "TechStore Pro",
    "categoryId": "789e0123-e89b-12d3-a456-426614174002",
    "categoryName": "Smartphones",
    "specifications": {
      "Display": "6.7-inch Super Retina XDR OLED",
      "Chip": "A17 Pro"
    },
    "colors": [
      {
        "name": "Natural Titanium",
        "hex": "#F5F5DC",
        "images": ["https://example.com/colors/natural-titanium.jpg"],
        "priceAdjustment": 0.00,
        "finalPrice": 1199.00
      }
    ],
    "groupBuying": {
      "isEnabled": true,
      "groupMaxSize": 50,
      "groupPrice": 1099.00,
      "timeLimitHours": 72
    },
    "installmentOptions": {
      "isEnabled": true,
      "plans": []
    },
    "previewType": "VIDEO",
    "previewUrl": "https://minio.example.com/nextgate-preview-content/preview/shop-id/product-id/uuid_trailer.mp4",
    "previewDownloadable": false,
    "createdAt": "2026-05-19T10:30:00Z",
    "updatedAt": "2026-05-19T14:30:00Z"
  }
}
```

**Error Responses**:
- `401`: Authentication required
- `403`: Insufficient permissions
- `404`: Shop or product not found

---

## 7. Get Shop Products (Management View)
**Purpose**: Retrieves all products for a shop with summary statistics.

**Endpoint**: <span style="background-color: #007bff; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">GET</span> `api/v1/e-commerce/shops/{shopId}/products/all`

**Access Level**: 🔒 Protected (Requires shop owner or system admin role)

**Authentication**: Bearer Token

**Path Parameters**:
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| shopId | UUID | Yes | ID of the shop |

**Response JSON Sample**:
```json
{
  "success": true,
  "message": "Retrieved 47 products from shop: TechStore Pro",
  "data": {
    "shop": {
      "shopId": "123e4567-e89b-12d3-a456-426614174000",
      "shopName": "TechStore Pro",
      "isVerified": true,
      "isMyShop": true
    },
    "summary": {
      "totalProducts": 47,
      "activeProducts": 35,
      "draftProducts": 8,
      "outOfStockProducts": 4,
      "lowStockProducts": 6,
      "productsWithGroupBuying": 18,
      "productsWithInstallments": 25
    },
    "products": [
      {
        "productId": "456e7890-e89b-12d3-a456-426614174001",
        "productName": "iPhone 15 Pro Max 512GB",
        "price": 1199.00,
        "stockQuantity": 25,
        "status": "ACTIVE",
        "isInStock": true,
        "hasGroupBuying": true,
        "hasInstallments": true,
        "createdAt": "2026-05-19T10:30:00Z"
      }
    ],
    "totalProducts": 47
  }
}
```

**Error Responses**:
- `401`: Authentication required
- `403`: Insufficient permissions
- `404`: Shop not found

---

## 8. Get Shop Products Paginated (Management View)
**Purpose**: Retrieves shop products with pagination for management dashboard.

**Endpoint**: <span style="background-color: #007bff; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">GET</span> `api/v1/e-commerce/shops/{shopId}/products/all-paged`

**Access Level**: 🔒 Protected (Requires shop owner or system admin role)

**Authentication**: Bearer Token

**Path Parameters**:
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| shopId | UUID | Yes | ID of the shop |

**Query Parameters**:
| Parameter | Type | Required | Default | Description |
|-----------|------|----------|---------|-------------|
| page | integer | No | 1 | Page number (1-indexed) |
| size | integer | No | 10 | Items per page (max: 100) |

**Response JSON Sample**:
```json
{
  "success": true,
  "message": "Retrieved 10 products from shop: TechStore Pro (Page 1 of 5)",
  "data": {
    "contents": { "...same structure as /all..." },
    "currentPage": 1,
    "pageSize": 10,
    "totalElements": 47,
    "totalPages": 5,
    "hasNext": true,
    "hasPrevious": false
  }
}
```

**Error Responses**:
- `401`: Authentication required
- `403`: Insufficient permissions
- `404`: Shop not found

---

## 9. Get Public Product by ID
**Purpose**: Retrieves a single active product for public viewing.

**Endpoint**: <span style="background-color: #007bff; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">GET</span> `api/v1/e-commerce/shops/{shopId}/products/{productId}`

**Access Level**: 🌐 Public

**Path Parameters**:
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| shopId | UUID | Yes | Shop must be active and approved |
| productId | UUID | Yes | Product must be active |

**Response JSON Sample**:
```json
{
  "success": true,
  "message": "Product retrieved successfully",
  "data": {
    "productId": "456e7890-e89b-12d3-a456-426614174001",
    "productName": "iPhone 15 Pro Max 512GB",
    "productSlug": "iphone-15-pro-max-512gb",
    "productType": "PHYSICAL",
    "productDescription": "The most advanced iPhone ever...",
    "price": 1199.00,
    "comparePrice": 1299.00,
    "discountAmount": 100.00,
    "discountPercentage": 7.69,
    "isOnSale": true,
    "isInStock": true,
    "stockQuantity": 25,
    "condition": "NEW",
    "shopName": "TechStore Pro",
    "categoryName": "Smartphones",
    "specifications": { "Display": "6.7-inch OLED" },
    "colors": [
      {
        "name": "Natural Titanium",
        "hex": "#F5F5DC",
        "priceAdjustment": 0.00,
        "finalPrice": 1199.00
      }
    ],
    "groupBuying": {
      "isAvailable": true,
      "groupMaxSize": 50,
      "groupPrice": 1099.00,
      "timeLimitHours": 72
    },
    "installmentOptions": {
      "isAvailable": true,
      "plans": [
        {
          "planId": "...",
          "planName": "6-Month Interest-Free",
          "paymentFrequency": "MONTHLY",
          "numberOfPayments": 6,
          "apr": 0.00,
          "minDownPaymentPercent": 20
        }
      ]
    },
    "previewType": "PDF",
    "previewUrl": "https://minio.example.com/nextgate-preview-content/preview/shop-id/product-id/uuid_sample.pdf",
    "previewDownloadable": true,
    "createdAt": "2026-05-19T10:30:00Z"
  }
}
```

**Notes**:
- `previewType` and `previewUrl` are `null` when no preview has been uploaded
- Preview files are publicly accessible without authentication — the URL is permanent and direct

**Error Responses**:
- `404`: Shop not found/not approved, or product not found/not active

---

## 10. Get Public Shop Products
**Purpose**: Retrieves all active products from a shop for public browsing.

**Endpoint**: <span style="background-color: #007bff; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">GET</span> `api/v1/e-commerce/shops/{shopId}/products/public-view/all`

**Access Level**: 🌐 Public

**Path Parameters**:
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| shopId | UUID | Yes | Shop must be active and approved |

**Response JSON Sample**:
```json
{
  "success": true,
  "message": "Retrieved 23 products from TechStore Pro",
  "data": {
    "shop": {
      "shopId": "123e4567-e89b-12d3-a456-426614174000",
      "shopName": "TechStore Pro",
      "isVerified": true
    },
    "products": [
      {
        "productId": "456e7890-e89b-12d3-a456-426614174001",
        "productName": "iPhone 15 Pro Max",
        "price": 1199.00,
        "isOnSale": true,
        "isInStock": true,
        "hasGroupBuying": true,
        "hasInstallments": true
      }
    ],
    "totalProducts": 23
  }
}
```

**Error Responses**:
- `404`: Shop not found, not approved, or not active

---

## 11. Get Public Shop Products Paginated
**Purpose**: Retrieves active products from a shop with pagination for public browsing.

**Endpoint**: <span style="background-color: #007bff; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">GET</span> `api/v1/e-commerce/shops/{shopId}/products/public-view/all-paged`

**Access Level**: 🌐 Public

**Path Parameters**:
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| shopId | UUID | Yes | Shop must be active and approved |

**Query Parameters**:
| Parameter | Type | Required | Default | Description |
|-----------|------|----------|---------|-------------|
| page | integer | No | 1 | Page number (1-indexed) |
| size | integer | No | 10 | Items per page (max: 50) |

**Error Responses**:
- `404`: Shop not found, not approved, or not active

---

## 12. Search Products
**Purpose**: Searches products within a shop using multi-word query matching across multiple fields.

**Endpoint**: <span style="background-color: #007bff; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">GET</span> `api/v1/e-commerce/shops/{shopId}/products/search`

**Access Level**: 🌐 Public (Enhanced features for authenticated users)

**Authentication**: Optional Bearer Token

**Path Parameters**:
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| shopId | UUID | Yes | ID of the shop |

**Query Parameters**:
| Parameter | Type | Required | Default | Description |
|-----------|------|----------|---------|-------------|
| q | string | Yes | — | Search query (min: 2, max: 100 chars) |
| status | ProductStatus[] | No | ACTIVE | Statuses to search (owners/admins only for non-ACTIVE) |
| page | integer | No | 1 | Page number |
| size | integer | No | 10 | Items per page (max: 50) |
| sortBy | string | No | relevance | `relevance`, `createdAt`, `updatedAt`, `productName`, `price`, `stockQuantity`, `brand` |
| sortDir | string | No | desc | `asc` or `desc` |

**Search Behavior**:
| Feature | Description |
|---------|-------------|
| Multi-word | Searches for products containing ALL words |
| Partial match | `"iph"` matches `"iPhone"` |
| Cross-field | Matches against name, description, brand, tags, specifications |
| Case-insensitive | `"APPLE"` matches `"apple"` |

**User Access**:
| User Type | Searchable Statuses |
|-----------|-------------------|
| Public / Authenticated | ACTIVE only |
| Shop Owner / Admin | All statuses |

**Response JSON Sample**:
```json
{
  "success": true,
  "message": "Found 12 products matching 'iphone'",
  "data": {
    "contents": {
      "shop": { "shopId": "...", "shopName": "TechStore Pro" },
      "products": [ { "...product summary fields..." } ],
      "totalProducts": 12,
      "searchMetadata": {
        "searchQuery": "iphone",
        "searchedStatuses": ["ACTIVE"],
        "userType": "PUBLIC"
      }
    },
    "currentPage": 1,
    "pageSize": 10,
    "totalElements": 12,
    "totalPages": 2,
    "hasNext": true,
    "hasPrevious": false
  }
}
```

**Error Responses**:
- `400`: Query too short or too long
- `404`: Shop not found or not accessible

---

## 13. Advanced Product Filter
**Purpose**: Filters products using multiple criteria with combined AND/OR logic.

**Endpoint**: <span style="background-color: #007bff; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">GET</span> `api/v1/e-commerce/shops/{shopId}/products/advanced-filter`

**Access Level**: 🌐 Public (Enhanced features for authenticated users)

**Authentication**: Optional Bearer Token

**Path Parameters**:
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| shopId | UUID | Yes | ID of the shop |

**Query Parameters**:
| Parameter | Type | Required | Default | Description |
|-----------|------|----------|---------|-------------|
| minPrice | decimal | No | — | Minimum price |
| maxPrice | decimal | No | — | Maximum price (must be ≥ minPrice) |
| condition | ProductCondition | No | — | Condition filter |
| categoryId | UUID | No | — | Category filter |
| inStock | boolean | No | — | Filter by availability |
| onSale | boolean | No | — | Filter by sale status |
| hasGroupBuying | boolean | No | — | Filter by group buying |
| hasInstallments | boolean | No | — | Filter by installments |
| hasMultipleColors | boolean | No | — | Filter by color variations |
| status | ProductStatus[] | No | ACTIVE | Status filter (owners/admins for non-ACTIVE) |
| page | integer | No | 1 | Page number |
| size | integer | No | 10 | Items per page (max: 50) |
| sortBy | string | No | createdAt | `createdAt`, `updatedAt`, `productName`, `price`, `stockQuantity` |
| sortDir | string | No | desc | `asc` or `desc` |

**Filter Logic**:
| Filter Type | Logic |
|-------------|-------|
| Price range | AND (minPrice AND maxPrice) |
| Feature flags | AND (all must match) |
| Multiple statuses | OR |

**Error Responses**:
- `400`: Invalid filter values or price range error
- `404`: Shop or category not found

---

## 14. Get Public Product by Slug
**Purpose**: Retrieves a single active product by its slug (same response shape as [Get Public Product by ID](#9-get-public-product-by-id)).

**Endpoint**: <span style="background-color: #007bff; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">GET</span> `api/v1/e-commerce/shops/{shopId}/products/find-by-slug/{slug}`

**Access Level**: 🌐 Public

**Path Parameters**:
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| shopId | UUID | Yes | Shop must be active and approved |
| slug | string | Yes | Product slug |

**Error Responses**:
- `404`: Shop not found/not approved, or product not found/not active

---

## 15. Installment Plan Config
**Purpose**: CRUD for installment plans attached to a product. Plans are created separately after the product, and linked to it by `productId`.

**Base URL**: `api/v1/e-commerce/products/{shopId}/{productId}/installment-plans`

**Access Level**: 🔒 Protected (Requires shop owner or system admin role)

**Authentication**: Bearer Token

**Path Parameters (shared)**:
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| shopId | UUID | Yes | ID of the shop |
| productId | UUID | Yes | ID of the product |

---

### 15a. Create Installment Plan
**Endpoint**: <span style="background-color: #28a745; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">POST</span> `api/v1/e-commerce/products/{shopId}/{productId}/installment-plans`

**Request Body**:
| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| planName | string | Yes | Display name for the plan | Min: 3, Max: 100 chars |
| paymentFrequency | PaymentFrequency | Yes | Payment interval | `DAILY`, `WEEKLY`, `BI_WEEKLY`, `SEMI_MONTHLY`, `MONTHLY`, `QUARTERLY`, `CUSTOM_DAYS` |
| customFrequencyDays | integer | Conditional | Days between payments | Required when `paymentFrequency=CUSTOM_DAYS`, min: 1 |
| numberOfPayments | integer | Yes | Total number of payments | Min: 2, Max: 120 |
| apr | decimal | Yes | Annual percentage rate | Min: 0.0, Max: 36.0, 2 decimal places |
| minDownPaymentPercent | integer | Yes | Minimum down payment % | Min: 10, Max: 50 |
| fulfillmentTiming | FulfillmentTiming | Yes | When to ship | `IMMEDIATE` (ship after down payment), `AFTER_PAYMENT` (layaway — ship after final payment) |
| displayOrder | integer | No | Sort order in UI | Min: 0, Default: 0 |
| isFeatured | boolean | No | Highlight as recommended plan | Default: false |
| isActive | boolean | No | Plan is available to buyers | Default: true |

**Request JSON Sample**:
```json
{
  "planName": "6-Month Interest-Free",
  "paymentFrequency": "MONTHLY",
  "numberOfPayments": 6,
  "apr": 0.00,
  "minDownPaymentPercent": 20,
  "fulfillmentTiming": "IMMEDIATE",
  "displayOrder": 1,
  "isFeatured": true,
  "isActive": true
}
```

**Error Responses**:
- `400`: Validation errors
- `404`: Shop or product not found

---

### 15b. Get All Installment Plans
**Endpoint**: <span style="background-color: #007bff; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">GET</span> `api/v1/e-commerce/products/{shopId}/{productId}/installment-plans`

Returns a list of all installment plans for the product.

**Error Responses**:
- `404`: Shop or product not found

---

### 15c. Get Installment Plan by ID
**Endpoint**: <span style="background-color: #007bff; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">GET</span> `api/v1/e-commerce/products/{shopId}/{productId}/installment-plans/{planId}`

**Additional Path Parameter**:
| Parameter | Type | Description |
|-----------|------|-------------|
| planId | UUID | ID of the installment plan |

---

### 15d. Update Installment Plan
**Endpoint**: <span style="background-color: #ffc107; color: black; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">PUT</span> `api/v1/e-commerce/products/{shopId}/{productId}/installment-plans/{planId}`

All fields are optional — only provided fields are updated. Same field structure as Create.

---

### 15e. Delete Installment Plan
**Endpoint**: <span style="background-color: #dc3545; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">DELETE</span> `api/v1/e-commerce/products/{shopId}/{productId}/installment-plans/{planId}`

---

### 15f. Activate / Deactivate Plan
**Activate**: <span style="background-color: #6f42c1; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">PATCH</span> `api/v1/e-commerce/products/{shopId}/{productId}/installment-plans/{planId}/activate`

**Deactivate**: <span style="background-color: #6f42c1; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">PATCH</span> `api/v1/e-commerce/products/{shopId}/{productId}/installment-plans/{planId}/deactivate`

Toggles `isActive` on the plan without changing any other fields.

---

### 15g. Set Featured Plan
**Endpoint**: <span style="background-color: #6f42c1; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">PATCH</span> `api/v1/e-commerce/products/{shopId}/{productId}/installment-plans/{planId}/set-featured`

Marks the specified plan as the featured (recommended) plan for this product.

---

## 16. Digital File Management
**Purpose**: Manages downloadable files for `DIGITAL` products. Uses a presign → upload → confirm flow to upload files directly to object storage.

**Base URL**: `api/v1/e-commerce/shops/{shopId}/products/{productId}/digital-files`

**Access Level**: 🔒 Protected (Requires shop owner or system admin role)

**Authentication**: Bearer Token

**Path Parameters (shared)**:
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| shopId | UUID | Yes | ID of the shop |
| productId | UUID | Yes | ID of the digital product |

---

### 16a. Presign Upload
**Purpose**: Generates a presigned PUT URL. The client uploads the file directly to this URL, then calls `/confirm`.

**Endpoint**: <span style="background-color: #28a745; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">POST</span> `api/v1/e-commerce/shops/{shopId}/products/{productId}/digital-files/presign-upload`

**Request Body**:
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| fileName | string | Yes | Original file name |
| contentType | string | Yes | MIME type (e.g. `application/pdf`) |
| fileSize | long | Yes | File size in bytes (must be positive) |
| displayOrder | integer | No | Sort order for multiple files |

**Request JSON Sample**:
```json
{
  "fileName": "design-kit-v2.fig",
  "contentType": "application/octet-stream",
  "fileSize": 52428800,
  "displayOrder": 1
}
```

**Response JSON Sample**:
```json
{
  "success": true,
  "message": "Upload URL generated — upload directly to this URL then call /confirm",
  "data": {
    "uploadUrl": "https://s3.example.com/bucket/key?X-Amz-Signature=...",
    "objectKey": "digital-files/product-456/design-kit-v2.fig",
    "expiresAt": "2026-05-19T11:00:00"
  }
}
```

**Upload Flow**:
1. Call `POST /presign-upload` → receive `uploadUrl` and `objectKey`
2. `PUT {uploadUrl}` with binary file body (do not call the API for this step)
3. Call `POST /confirm` with `objectKey` to register the file

---

### 16b. Confirm Upload
**Purpose**: Registers a file after direct upload to object storage. Must be called after the actual file upload succeeds.

**Endpoint**: <span style="background-color: #28a745; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">POST</span> `api/v1/e-commerce/shops/{shopId}/products/{productId}/digital-files/confirm`

**Request Body**:
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| objectKey | string | Yes | Returned by presign-upload |
| fileName | string | Yes | Original file name |
| contentType | string | Yes | MIME type |
| fileSize | long | Yes | File size in bytes |
| displayOrder | integer | No | Sort order |

**Response JSON Sample**:
```json
{
  "success": true,
  "message": "File confirmed and linked to product",
  "data": {
    "fileId": "aaa1bbbb-e89b-12d3-a456-426614174001",
    "productId": "456e7890-e89b-12d3-a456-426614174001",
    "fileName": "design-kit-v2.fig",
    "contentType": "application/octet-stream",
    "fileSize": 52428800,
    "fileVersion": 1,
    "displayOrder": 1,
    "isActive": true,
    "uploadedAt": "2026-05-19T10:45:00"
  }
}
```

---

### 16c. Get Product Files
**Endpoint**: <span style="background-color: #007bff; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">GET</span> `api/v1/e-commerce/shops/{shopId}/products/{productId}/digital-files`

Returns a list of `DigitalFileResponse` objects for all files linked to the product.

---

### 16d. Delete File
**Endpoint**: <span style="background-color: #dc3545; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">DELETE</span> `api/v1/e-commerce/shops/{shopId}/products/{productId}/digital-files/{fileId}`

**Additional Path Parameter**:
| Parameter | Type | Description |
|-----------|------|-------------|
| fileId | UUID | ID of the file to delete |

---

### 16e. Toggle File Active Status
**Endpoint**: <span style="background-color: #6f42c1; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">PATCH</span> `api/v1/e-commerce/shops/{shopId}/products/{productId}/digital-files/{fileId}/toggle`

**Query Parameters**:
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| isActive | boolean | Yes | `true` to activate, `false` to deactivate |

Deactivated files are hidden from buyers but not deleted.

---

## 17. Product Preview Management
**Purpose**: Manages a single preview file per product — a publicly accessible teaser shown to buyers before purchase. Distinct from private digital content files. Supports VIDEO, PDF, 3D models, and IMAGE previews.

**Base URL**: `api/v1/e-commerce/shops/{shopId}/products/{productId}/preview`

**Access Level**: 🔒 Protected (Requires shop owner or system admin role)

**Authentication**: Bearer Token

**Path Parameters (shared)**:
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| shopId | UUID | Yes | ID of the shop |
| productId | UUID | Yes | ID of the product (any type — PHYSICAL or DIGITAL) |

**Storage**: Uploaded to `nextgate-preview-content` bucket (public read). The confirmed URL is permanent and requires no authentication to access.

---

### 17a. Presign Preview Upload
**Purpose**: Generates a presigned PUT URL. The client uploads the preview file directly to this URL, then calls `/confirm`.

**Endpoint**: <span style="background-color: #28a745; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">POST</span> `api/v1/e-commerce/shops/{shopId}/products/{productId}/preview/presign-upload`

**Request Body**:
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| fileName | string | Yes | Original file name (used to build the storage key) |
| contentType | string | Yes | MIME type (e.g. `video/mp4`, `application/pdf`, `image/jpeg`, `model/gltf-binary`) |
| fileSize | long | Yes | File size in bytes (must be positive) |

**Request JSON Sample**:
```json
{
  "fileName": "product-trailer.mp4",
  "contentType": "video/mp4",
  "fileSize": 52428800
}
```

**Response JSON Sample**:
```json
{
  "success": true,
  "message": "Preview upload URL generated — upload directly then call /confirm",
  "data": {
    "uploadUrl": "https://minio.example.com/nextgate-preview-content/preview/shop-id/product-id/uuid_product-trailer.mp4?X-Amz-Signature=...",
    "objectKey": "preview/shop-id/product-id/uuid_product-trailer.mp4",
    "expiresAt": "2026-05-19T11:30:00"
  }
}
```

**Upload Flow**:
1. Call `POST /presign-upload` → receive `uploadUrl` and `objectKey`
2. `PUT {uploadUrl}` with binary file body (client-to-MinIO directly, not through the API)
3. Call `POST /confirm` with `objectKey` and `previewType` to link the file to the product

**Error Responses**:
- `401`: Authentication required
- `403`: Insufficient permissions
- `404`: Shop or product not found

---

### 17b. Confirm Preview Upload
**Purpose**: Links the uploaded file to the product. Sets `previewType` and stores the permanent public URL as `previewUrl`. If the product already has a preview, the old file is deleted from storage.

**Endpoint**: <span style="background-color: #28a745; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">POST</span> `api/v1/e-commerce/shops/{shopId}/products/{productId}/preview/confirm`

**Request Body**:
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| objectKey | string | Yes | Returned by `presign-upload` |
| previewType | PreviewType | Yes | `VIDEO`, `PDF`, `THREE_D`, or `IMAGE` |
| previewDownloadable | boolean | No | Whether viewers can download the file. Default: `false` (view/stream only) |

**Request JSON Sample**:
```json
{
  "objectKey": "preview/shop-id/product-id/uuid_product-trailer.mp4",
  "previewType": "VIDEO",
  "previewDownloadable": false
}
```

**Response JSON Sample**:
```json
{
  "success": true,
  "message": "Preview confirmed and linked to product",
  "data": null
}
```

**After confirm**, the product's public response will include:
```json
{
  "previewType": "VIDEO",
  "previewUrl": "https://minio.example.com/nextgate-preview-content/preview/shop-id/product-id/uuid_product-trailer.mp4",
  "previewDownloadable": false
}
```

**Error Responses**:
- `400`: Missing objectKey or previewType
- `401`: Authentication required
- `403`: Insufficient permissions
- `404`: Shop or product not found

---

### 17c. Remove Preview
**Purpose**: Deletes the product's preview file from storage and clears `previewType` and `previewUrl` on the product.

**Endpoint**: <span style="background-color: #dc3545; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">DELETE</span> `api/v1/e-commerce/shops/{shopId}/products/{productId}/preview`

**Response JSON Sample**:
```json
{
  "success": true,
  "message": "Preview removed from product",
  "data": null
}
```

**Error Responses**:
- `401`: Authentication required
- `403`: Insufficient permissions
- `404`: Shop or product not found

---

## Quick Reference

### Common HTTP Status Codes
| Code | Meaning |
|------|---------|
| `200 OK` | Successful GET/PUT/PATCH |
| `201 Created` | Successful POST (resource created) |
| `400 Bad Request` | Invalid data, validation errors, business rule violations |
| `401 Unauthorized` | Authentication required or invalid token |
| `403 Forbidden` | Insufficient permissions |
| `404 Not Found` | Resource not found or not accessible |
| `409 Conflict` | Duplicate product name or business constraint violation |
| `422 Unprocessable Entity` | Field-level validation errors |
| `500 Internal Server Error` | Server error |

### User Access Levels
| User Type | Product Management | Status Access |
|-----------|-------------------|---------------|
| **Public** | View active products only | ACTIVE only |
| **Authenticated** | View active products only | ACTIVE only |
| **Shop Owner** | Full CRUD on own shop | All statuses |
| **System Admin** | Full CRUD on all shops | All statuses |

### Product Type Fulfillment Flows
| Type | Flow |
|------|------|
| `PHYSICAL` | Payment → `PENDING_SHIPMENT` → Seller ships → Buyer confirms with 6-digit code → Escrow releases → `COMPLETED` |
| `DIGITAL` | Payment → `COMPLETED` immediately → Escrow released → `DigitalDownloadAccess` records created → Buyer downloads |

### Product Status Lifecycle
```
DRAFT → ACTIVE → INACTIVE → ARCHIVED
  ↑                            ↓
  └───────── RESTORE ──────────┘

OUT_OF_STOCK ←→ ACTIVE (automatic based on inventory)
```

| Status | Public Visibility | Available Actions |
|--------|-------------------|-------------------|
| `DRAFT` | Hidden | Edit, Publish, Hard Delete |
| `ACTIVE` | Visible | Edit, Deactivate, Soft Delete |
| `INACTIVE` | Hidden | Edit, Activate, Soft Delete |
| `OUT_OF_STOCK` | Visible (out of stock badge) | Restock (auto-activates) |
| `ARCHIVED` | Hidden | Restore |

### Enums Reference

**ProductType**: `PHYSICAL`, `DIGITAL`

**PreviewType**: `VIDEO`, `PDF`, `THREE_D`, `IMAGE` — `null` means no preview

**ProductCondition**: `NEW`, `USED_LIKE_NEW`, `USED_GOOD`, `USED_FAIR`, `REFURBISHED`, `FOR_PARTS`

**ReqAction**: `SAVE_DRAFT` (→ DRAFT status), `SAVE_PUBLISH` (→ ACTIVE status)

**UrgencyTag**: `NONE`, `NEW_ARRIVAL`, `LIMITED_EDITION`, `LIMITED_OFFER`, `FEW_REMAINS`

**PaymentFrequency**: `DAILY`, `WEEKLY`, `BI_WEEKLY`, `SEMI_MONTHLY`, `MONTHLY`, `QUARTERLY`, `CUSTOM_DAYS`

**FulfillmentTiming**: `IMMEDIATE` (ship after down payment), `AFTER_PAYMENT` (layaway — ship after final payment)

### SKU Format
`SHP[8-CHAR-UUID]-[CATEGORY-3]-[BRAND-3]-[ATTRIBUTE-3]-[SEQUENCE-4]`

Example: `SHP12345678-ELE-APP-512-0001`

### Data Format Standards
- **Dates**: ISO 8601 (`2026-05-19T14:30:00Z`)
- **Prices**: Decimal with 2 decimal places, stored as BigDecimal
- **UUIDs**: Standard UUID format
- **Pagination**: 1-indexed page parameter
- **Colors**: Hex format `#RRGGBB`
- **Percentages**: Decimal format (`20.00` = 20%)

### Error Response Format
```json
{
  "success": false,
  "message": "Human-readable error message",
  "error": {
    "code": "ERROR_CODE",
    "details": "Detailed information",
    "field": "fieldName (if field-specific)",
    "timestamp": "2026-05-19T14:30:00Z"
  }
}
```

### Product Creation Flow
```
1. POST /shops/{shopId}/products?action=SAVE_DRAFT
   — create the product shell

2. POST /products/{shopId}/{productId}/installment-plans
   — add installment plans (if installmentEnabled)

3. POST /shops/{shopId}/products/{productId}/digital-files/presign-upload
   PUT {uploadUrl} (direct to storage)
   POST /shops/{shopId}/products/{productId}/digital-files/confirm
   — upload private digital files (DIGITAL products only)

4. POST /shops/{shopId}/products/{productId}/preview/presign-upload
   PUT {uploadUrl} (direct to storage)
   POST /shops/{shopId}/products/{productId}/preview/confirm
   — upload preview teaser (any product type, optional)
   — preview is stored in public bucket; URL is immediately accessible

5. PATCH /shops/{shopId}/products/{productId}/publish
   — publish when ready
```

### Preview vs Digital Files
| | Preview | Digital Files |
|--|---------|---------------|
| **Who sees it** | Everyone (before purchase) | Buyers only (after payment) |
| **Bucket** | `nextgate-preview-content` (public) | `nextgate-digital-content` (private) |
| **Access** | Permanent public URL | Presigned URL, expires per `downloadExpiryDays` |
| **Count** | One per product | Multiple files per product |
| **Products** | PHYSICAL or DIGITAL | DIGITAL only |
| **Purpose** | Teaser/sample before buying | Actual purchased content |

# Wishlist Management

**Author**: Josh S. Sakweli, Backend Lead Team  
**Last Updated**: 2026-06-04  
**Version**: v1.0

**Base URL**: `{base_url}/api/v1/e-commerce/wishlist`

**Short Description**: The Wishlist API lets authenticated users save products for later, organize them into named groups, transfer items between groups, and move items directly to the cart. Every wishlist operation is strictly private — users can only read and modify their own wishlist.

**Hints**:
- All endpoints require a valid JWT Bearer token — there are no public wishlist endpoints
- When adding a product, pass either `groupId` (existing group) or `groupName` (creates a new group) — passing both returns `400`
- Group names are unique per user — attempting to create a duplicate name returns `400`
- Deleting a group with `?deleteProducts=false` (default) moves all items in that group to Ungrouped; `?deleteProducts=true` permanently removes those items from the wishlist
- `PATCH /{itemId}/group` with `groupId: null` moves an item to Ungrouped without deleting it
- `isInWishlist`, `wishlistItemId`, `wishlistGroupId`, and `wishlistGroupName` are populated on the single product detail response (`GET /api/v1/e-commerce/products/{slug}`) for authenticated users

---

## Standard Response Format

### Success Response Structure
```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Operation completed successfully",
  "action_time": "2025-09-23T10:30:45",
  "data": {}
}
```

### Error Response Structure
```json
{
  "success": false,
  "httpStatus": "BAD_REQUEST",
  "message": "Error description",
  "action_time": "2025-09-23T10:30:45",
  "data": "Error description"
}
```

### Standard Response Fields
| Field | Type | Description |
|-------|------|-------------|
| `success` | boolean | `true` for successful operations, `false` for errors |
| `httpStatus` | string | HTTP status name (OK, BAD_REQUEST, NOT_FOUND, etc.) |
| `message` | string | Human-readable message describing the result |
| `action_time` | string | ISO 8601 timestamp of when the response was generated |
| `data` | object/string | Response payload on success, error details on failure |

### Standard Error Types
- `400 BAD_REQUEST`: Business rule violation (duplicate product, duplicate group name, ambiguous group fields)
- `401 UNAUTHORIZED`: Missing, expired, or invalid JWT token
- `404 NOT_FOUND`: Product, wishlist item, or group not found
- `422 UNPROCESSABLE_ENTITY`: Validation errors with field-level detail
- `500 INTERNAL_SERVER_ERROR`: Unexpected server error

---

## Endpoints

## 1. Add Product to Wishlist

**Purpose**: Adds a product to the authenticated user's wishlist. Optionally places it in an existing group (via `groupId`) or creates a new group on the fly (via `groupName`). If neither is provided the item lands in Ungrouped.

**Endpoint**: <span style="background-color: #007bff; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">POST</span> `{base_url}/api/v1/e-commerce/wishlist/add`

**Access Level**: 🔒 Protected (Requires valid JWT)

**Authentication**: Bearer Token

**Request Headers**:
| Header | Type | Required | Description |
|--------|------|----------|-------------|
| `Authorization` | string | Yes | `Bearer <token>` |

**Request JSON Sample — add to Ungrouped (no group)**:
```json
{
  "productId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
}
```

**Request JSON Sample — add to an existing group**:
```json
{
  "productId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "groupId": "f9e8d7c6-b5a4-3210-fedc-ba9876543210"
}
```

**Request JSON Sample — add and create a new group**:
```json
{
  "productId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "groupName": "Birthday Gifts"
}
```

**Request Body Parameters**:
| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| `productId` | UUID | Yes | ID of the product to add | Must be a valid, non-deleted product |
| `groupId` | UUID | No | Add to an existing group | Must belong to the authenticated user. Cannot be combined with `groupName` |
| `groupName` | string | No | Create a new group with this name and add the product to it | Must not already exist for this user. Cannot be combined with `groupId` |

**Success Response JSON Sample — added to Ungrouped**:
```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Product added to wishlist successfully",
  "action_time": "2026-06-04T14:22:10",
  "data": null
}
```

**Success Response JSON Sample — added to existing group**:
```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Product added to wishlist in group 'Electronics'",
  "action_time": "2026-06-04T14:22:10",
  "data": null
}
```

**Success Response JSON Sample — new group created and product added**:
```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Product added to wishlist in group 'Birthday Gifts'",
  "action_time": "2026-06-04T14:22:10",
  "data": null
}
```

**Success Response Fields**:
| Field | Description |
|-------|-------------|
| `message` | Confirms the product was added; includes group name when a group is involved |

**Error Response JSON Sample**:
```json
{
  "success": false,
  "httpStatus": "BAD_REQUEST",
  "message": "'Sony WH-1000XM5' is already in your wishlist",
  "action_time": "2026-06-04T14:22:10",
  "data": "'Sony WH-1000XM5' is already in your wishlist"
}
```

---

## 2. Get Wishlist (Flat)

**Purpose**: Returns the authenticated user's full wishlist as a flat list. Each item includes its group ID and group name (or `null` if Ungrouped). Useful for list views where grouping is handled client-side.

**Endpoint**: <span style="background-color: #28a745; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">GET</span> `{base_url}/api/v1/e-commerce/wishlist`

**Access Level**: 🔒 Protected (Requires valid JWT)

**Authentication**: Bearer Token

**Request Headers**:
| Header | Type | Required | Description |
|--------|------|----------|-------------|
| `Authorization` | string | Yes | `Bearer <token>` |

**Success Response JSON Sample**:
```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Wishlist retrieved successfully",
  "action_time": "2026-06-04T14:22:10",
  "data": {
    "user": {
      "userId": "uuid",
      "userName": "josh_dev",
      "name": "Josh Sakweli"
    },
    "wishlistSummary": {
      "totalItems": 3,
      "totalValue": 749.97,
      "inStockItems": 2,
      "outOfStockItems": 1
    },
    "wishlistItems": [
      {
        "wishlistId": "uuid",
        "productId": "uuid",
        "productName": "Sony WH-1000XM5",
        "productSlug": "sony-wh-1000xm5",
        "productImage": "https://cdn.example.com/img.jpg",
        "unitPrice": 349.99,
        "isOnSale": false,
        "shop": {
          "shopId": "uuid",
          "shopName": "Tech Haven",
          "shopSlug": "tech-haven",
          "logoUrl": "https://cdn.example.com/logo.jpg"
        },
        "availability": {
          "inStock": true,
          "stockQuantity": 12
        },
        "groupId": "uuid",
        "groupName": "Birthday Gifts",
        "addedAt": "2026-06-04T10:00:00"
      }
    ],
    "updatedAt": "2026-06-04T10:00:00"
  }
}
```

**Success Response Fields**:
| Field | Description |
|-------|-------------|
| `user.userId` | UUID of the authenticated user |
| `user.userName` | System username |
| `user.name` | Full name |
| `wishlistSummary.totalItems` | Total number of items in the wishlist |
| `wishlistSummary.totalValue` | Sum of unit prices of all items |
| `wishlistSummary.inStockItems` | Count of items currently in stock |
| `wishlistSummary.outOfStockItems` | Count of items out of stock |
| `wishlistItems[].wishlistId` | UUID of the wishlist entry (used for remove/transfer) |
| `wishlistItems[].productId` | UUID of the product |
| `wishlistItems[].productName` | Product display name |
| `wishlistItems[].productSlug` | URL-friendly product identifier |
| `wishlistItems[].productImage` | URL of the primary product image |
| `wishlistItems[].unitPrice` | Current price of the product |
| `wishlistItems[].isOnSale` | Whether the product is currently on sale |
| `wishlistItems[].shop` | Shop that sells the product (id, name, slug, logo) |
| `wishlistItems[].availability.inStock` | Whether the product is in stock |
| `wishlistItems[].availability.stockQuantity` | Current stock count |
| `wishlistItems[].groupId` | UUID of the group this item belongs to (`null` if Ungrouped) |
| `wishlistItems[].groupName` | Name of the group (`null` if Ungrouped) |
| `wishlistItems[].addedAt` | Timestamp when the product was added |
| `updatedAt` | Timestamp of the most recently added item |

---

## 3. Get Wishlist (Grouped)

**Purpose**: Returns the wishlist organized into sections — one section per named group plus a separate Ungrouped section. Useful for grouped display views.

**Endpoint**: <span style="background-color: #28a745; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">GET</span> `{base_url}/api/v1/e-commerce/wishlist/grouped`

**Access Level**: 🔒 Protected (Requires valid JWT)

**Authentication**: Bearer Token

**Request Headers**:
| Header | Type | Required | Description |
|--------|------|----------|-------------|
| `Authorization` | string | Yes | `Bearer <token>` |

**Success Response JSON Sample**:
```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Grouped wishlist retrieved successfully",
  "action_time": "2026-06-04T14:22:10",
  "data": {
    "user": {
      "userId": "uuid",
      "userName": "josh_dev",
      "name": "Josh Sakweli"
    },
    "wishlistSummary": {
      "totalItems": 3,
      "totalValue": 749.97,
      "inStockItems": 2,
      "outOfStockItems": 1
    },
    "groups": [
      {
        "groupId": "uuid",
        "groupName": "Birthday Gifts",
        "itemCount": 2,
        "items": [ ]
      }
    ],
    "ungrouped": {
      "groupId": null,
      "groupName": "Ungrouped",
      "itemCount": 1,
      "items": [ ]
    },
    "updatedAt": "2026-06-04T10:00:00"
  }
}
```

**Success Response Fields**:
| Field | Description |
|-------|-------------|
| `user` | Same user summary as flat response |
| `wishlistSummary` | Same summary totals across all items |
| `groups` | Array of named group sections, ordered by creation date (oldest first) |
| `groups[].groupId` | UUID of the group |
| `groups[].groupName` | Name of the group |
| `groups[].itemCount` | Number of items in this group |
| `groups[].items` | Array of wishlist item responses (same shape as flat list items) |
| `ungrouped` | Section for items with no group assigned |
| `ungrouped.groupId` | Always `null` |
| `ungrouped.groupName` | Always `"Ungrouped"` |
| `ungrouped.itemCount` | Count of items with no group |
| `ungrouped.items` | Array of ungrouped wishlist items |
| `updatedAt` | Timestamp of the most recently added item |

---

## 4. Remove Item from Wishlist

**Purpose**: Permanently removes a specific item from the authenticated user's wishlist.

**Endpoint**: <span style="background-color: #dc3545; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">DELETE</span> `{base_url}/api/v1/e-commerce/wishlist/{itemId}`

**Access Level**: 🔒 Protected (Requires valid JWT — owns the item)

**Authentication**: Bearer Token

**Request Headers**:
| Header | Type | Required | Description |
|--------|------|----------|-------------|
| `Authorization` | string | Yes | `Bearer <token>` |

**Path Parameters**:
| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| `itemId` | UUID | Yes | The `wishlistId` of the item to remove | Must belong to the authenticated user |

**Success Response JSON Sample**:
```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Product removed from wishlist successfully",
  "action_time": "2026-06-04T14:22:10",
  "data": null
}
```

---

## 5. Clear Wishlist

**Purpose**: Permanently removes all items from the authenticated user's wishlist. Groups are not deleted — only the items inside them.

**Endpoint**: <span style="background-color: #dc3545; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">DELETE</span> `{base_url}/api/v1/e-commerce/wishlist/clear`

**Access Level**: 🔒 Protected (Requires valid JWT)

**Authentication**: Bearer Token

**Request Headers**:
| Header | Type | Required | Description |
|--------|------|----------|-------------|
| `Authorization` | string | Yes | `Bearer <token>` |

**Success Response JSON Sample**:
```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Wishlist cleared successfully",
  "action_time": "2026-06-04T14:22:10",
  "data": null
}
```

---

## 6. Move Item to Cart

**Purpose**: Adds a wishlist item to the user's cart at the specified quantity. The item remains in the wishlist — it is not automatically removed.

**Endpoint**: <span style="background-color: #007bff; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">POST</span> `{base_url}/api/v1/e-commerce/wishlist/move-to-cart/{itemId}`

**Access Level**: 🔒 Protected (Requires valid JWT)

**Authentication**: Bearer Token

**Request Headers**:
| Header | Type | Required | Description |
|--------|------|----------|-------------|
| `Authorization` | string | Yes | `Bearer <token>` |

**Path Parameters**:
| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| `itemId` | UUID | Yes | The `wishlistId` of the item to move | Must belong to the authenticated user |

**Query Parameters**:
| Parameter | Type | Required | Description | Validation | Default |
|-----------|------|----------|-------------|------------|---------|
| `quantity` | integer | No | Quantity to add to cart | Min: 1 | `1` |

**Success Response JSON Sample**:
```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Product moved to cart successfully",
  "action_time": "2026-06-04T14:22:10",
  "data": null
}
```

---

## 7. Transfer Item to Group

**Purpose**: Moves a wishlist item to a different group, or removes it from its current group by setting `groupId` to `null` (moves to Ungrouped). The item stays in the wishlist — only its group assignment changes.

**Endpoint**: <span style="background-color: #fd7e14; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">PATCH</span> `{base_url}/api/v1/e-commerce/wishlist/{itemId}/group`

**Access Level**: 🔒 Protected (Requires valid JWT — owns the item)

**Authentication**: Bearer Token

**Request Headers**:
| Header | Type | Required | Description |
|--------|------|----------|-------------|
| `Authorization` | string | Yes | `Bearer <token>` |

**Path Parameters**:
| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| `itemId` | UUID | Yes | The `wishlistId` of the item to transfer | Must belong to the authenticated user |

**Request JSON Sample — move to a group**:
```json
{
  "groupId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
}
```

**Request JSON Sample — remove from group (move to Ungrouped)**:
```json
{
  "groupId": null
}
```

**Request Body Parameters**:
| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| `groupId` | UUID or null | Yes | Target group UUID, or `null` to move to Ungrouped | When a UUID, must be a group that belongs to the authenticated user |

**Success Response JSON Sample**:
```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Item moved to group 'Electronics'",
  "action_time": "2026-06-04T14:22:10",
  "data": null
}
```

---

## 8. Create Group

**Purpose**: Creates a new named wishlist group for the authenticated user. Group names must be unique per user.

**Endpoint**: <span style="background-color: #007bff; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">POST</span> `{base_url}/api/v1/e-commerce/wishlist/groups`

**Access Level**: 🔒 Protected (Requires valid JWT)

**Authentication**: Bearer Token

**Request Headers**:
| Header | Type | Required | Description |
|--------|------|----------|-------------|
| `Authorization` | string | Yes | `Bearer <token>` |

**Request JSON Sample**:
```json
{
  "name": "Electronics"
}
```

**Request Body Parameters**:
| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| `name` | string | Yes | Display name for the group | Must not be blank. Must be unique for the authenticated user |

**Success Response JSON Sample**:
```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Wishlist group created successfully",
  "action_time": "2026-06-04T14:22:10",
  "data": {
    "groupId": "uuid",
    "name": "Electronics",
    "itemCount": 0,
    "createdAt": "2026-06-04T14:22:10"
  }
}
```

**Success Response Fields**:
| Field | Description |
|-------|-------------|
| `groupId` | UUID of the newly created group |
| `name` | Name of the group as saved |
| `itemCount` | Always `0` on creation |
| `createdAt` | Timestamp of group creation |

**Error Response JSON Sample**:
```json
{
  "success": false,
  "httpStatus": "BAD_REQUEST",
  "message": "A group named 'Electronics' already exists",
  "action_time": "2026-06-04T14:22:10",
  "data": "A group named 'Electronics' already exists"
}
```

---

## 9. Get Groups

**Purpose**: Returns all wishlist groups created by the authenticated user, ordered by creation date (oldest first). Each group includes a live item count.

**Endpoint**: <span style="background-color: #28a745; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">GET</span> `{base_url}/api/v1/e-commerce/wishlist/groups`

**Access Level**: 🔒 Protected (Requires valid JWT)

**Authentication**: Bearer Token

**Request Headers**:
| Header | Type | Required | Description |
|--------|------|----------|-------------|
| `Authorization` | string | Yes | `Bearer <token>` |

**Success Response JSON Sample**:
```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Wishlist groups retrieved successfully",
  "action_time": "2026-06-04T14:22:10",
  "data": [
    {
      "groupId": "uuid",
      "name": "Birthday Gifts",
      "itemCount": 3,
      "createdAt": "2026-06-01T09:00:00"
    },
    {
      "groupId": "uuid",
      "name": "Electronics",
      "itemCount": 1,
      "createdAt": "2026-06-03T11:30:00"
    }
  ]
}
```

**Success Response Fields**:
| Field | Description |
|-------|-------------|
| `[].groupId` | UUID of the group |
| `[].name` | Display name of the group |
| `[].itemCount` | Current number of wishlist items in this group |
| `[].createdAt` | Timestamp of group creation |

---

## 10. Delete Group

**Purpose**: Deletes a wishlist group. Controls what happens to the items inside via the `deleteProducts` query parameter.

**Endpoint**: <span style="background-color: #dc3545; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">DELETE</span> `{base_url}/api/v1/e-commerce/wishlist/groups/{groupId}`

**Access Level**: 🔒 Protected (Requires valid JWT — owns the group)

**Authentication**: Bearer Token

**Request Headers**:
| Header | Type | Required | Description |
|--------|------|----------|-------------|
| `Authorization` | string | Yes | `Bearer <token>` |

**Path Parameters**:
| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| `groupId` | UUID | Yes | UUID of the group to delete | Must belong to the authenticated user |

**Query Parameters**:
| Parameter | Type | Required | Description | Validation | Default |
|-----------|------|----------|-------------|------------|---------|
| `deleteProducts` | boolean | No | `true` → permanently delete all items in the group from wishlist. `false` → move items to Ungrouped, then delete group | `true` or `false` | `false` |

**Success Response JSON Sample — items moved to Ungrouped**:
```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Group deleted, products moved to Ungrouped",
  "action_time": "2026-06-04T14:22:10",
  "data": null
}
```

**Success Response JSON Sample — items permanently deleted**:
```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Group and all its products deleted from wishlist",
  "action_time": "2026-06-04T14:22:10",
  "data": null
}
```

---

## Quick Reference

### All Endpoints Summary

| # | Method | Path | Description |
|---|--------|------|-------------|
| 1 | POST | `/wishlist/add` | Add product to wishlist |
| 2 | GET | `/wishlist` | Get flat wishlist |
| 3 | GET | `/wishlist/grouped` | Get grouped wishlist |
| 4 | DELETE | `/wishlist/{itemId}` | Remove item from wishlist |
| 5 | DELETE | `/wishlist/clear` | Clear entire wishlist |
| 6 | POST | `/wishlist/move-to-cart/{itemId}` | Move item to cart |
| 7 | PATCH | `/wishlist/{itemId}/group` | Transfer item to group |
| 8 | POST | `/wishlist/groups` | Create group |
| 9 | GET | `/wishlist/groups` | Get all groups |
| 10 | DELETE | `/wishlist/groups/{groupId}` | Delete group |

### Group Behavior Rules

| Scenario | Behavior |
|----------|----------|
| Add with no group | Item lands in Ungrouped |
| Add with `groupId` | Item added to that existing group |
| Add with `groupName` (new) | New group created, item added to it |
| Add with `groupName` (exists) | `400` — duplicate group name |
| Add with both `groupId` and `groupName` | `400` — ambiguous request |
| Delete group `?deleteProducts=false` | Items move to Ungrouped, group deleted |
| Delete group `?deleteProducts=true` | Items permanently removed, group deleted |
| Transfer item with `groupId: null` | Item moved to Ungrouped |

# NextGate Product Ecosystem — Architecture & Purchase Flows

## Overview

NextGate's e-commerce layer handles two fundamentally different kinds of products: **physical products** (tangible goods that require shipping and delivery confirmation) and **digital products** (files that a buyer downloads). Both types share the same payment infrastructure, checkout session system, financial rails, and **inventory system** — but diverge completely at the fulfillment stage.

---

## 1. Product Types

### Physical Products

A physical product has real-world stock. The platform tracks inventory, holds it during checkout, ships it through a seller, and releases escrow only after the buyer confirms physical receipt. The entire lifecycle can take days or weeks.

### Digital Products

A digital product is a file (or collection of files) that the seller uploads to a private, access-controlled storage bucket. There is no shipping. There is no confirmation code. The moment payment clears, the buyer can download. Escrow releases immediately. The lifecycle is measured in seconds.

A single product can be either physical or digital — never both. The `productType` field on the product record is the authoritative signal that drives every downstream decision.

---

## 2. Inventory — Shared by Both Product Types

> **Key rule:** Every product has a `stockQuantity`. Digital and physical products are treated identically by the inventory system. The only difference is what happens after payment.

Every product — physical or digital — requires a `stockQuantity` set by the seller at creation time. The seller decides what that number is:

```
10        →  limited edition digital art, exclusive release
500       →  cohort-based course intake
1,000,000 →  effectively open — seller still sets a real number
```

There is no "unlimited" mode and no `trackInventory` toggle. All inventory mechanics apply to both product types without exception:

```
- stockQuantity check on add-to-cart
- Inventory hold at checkout session creation
- Hold released on session expiry or cancellation
- stockQuantity decremented on successful payment
- Low stock threshold warnings apply to both
- isInStock() and canOrderQuantity() run identically
```

This means zero special-casing in the codebase. Every product behaves the same through cart, checkout, and payment. The divergence only begins at fulfillment.

### Why sellers set quantity for digital products

The business reasons vary:

```
Exclusivity / scarcity    →  "Only 500 copies ever sold" — creates urgency and perceived value
License seat control      →  Software licensed for exactly N seats
Cohort control            →  Course intake capped at N students for direct purchase
Business guardrail        →  Seller wants a hard ceiling as a safety net
```

Even if a seller sets 1,000,000, the system enforces it as a real number. The seller always sees and manages a stock figure.

---

## 3. The Only Real Difference Between Physical and Digital

```
                           Physical        Digital
─────────────────────────────────────────────────────────────
stockQuantity              ✓               ✓
Inventory hold at checkout ✓               ✓
Stock decrements on buy    ✓               ✓
Low stock warnings         ✓               ✓
Requires shipping          ✓               ✗
Delivery confirmation code ✓               ✗
Escrow held until delivery ✓               ✗
Order → PENDING_SHIPMENT   ✓               ✗
Escrow released immediately✗               ✓
Order → COMPLETED          ✗               ✓
DigitalDownloadAccess      ✗               ✓
```

Everything above the dividing line is shared. Everything below is where the paths split.

---

## 4. How Sellers Prepare Digital Products

Before a digital product can be purchased, the seller must upload its files through a two-step process designed to handle large files without routing them through the API server.

```
Seller → Request presigned upload URL
       → Platform generates time-limited URL (MinIO private bucket)
       → Seller uploads file DIRECTLY to MinIO (API server bypassed)
       → Seller confirms upload to API
       → Platform records file metadata and links to product
```

A product can have multiple files. Each gets its own record. Buyers get access to all files on purchase.

Sellers configure per product:

```
stockQuantity      →  required · how many units can ever be sold
lowStockThreshold  →  when to trigger low stock warning
maxQuantityForDigital → max a single buyer can purchase in one order
                        (1 for personal-use content, higher for licenses/gifts)

Download rules:
  expiryDays       →  days download links stay active after purchase (default: 7)
  downloadCap      →  max downloads per buyer (default: unlimited)
  clockStart       →  expiry from purchase time OR from first download
```

---

## 5. The Cart

The cart is a persistent bag of products. It does not distinguish between physical and digital items — both coexist freely. Stock availability is checked on add-to-cart for both types.

> The physical/digital split only becomes relevant at fulfillment — not at cart, not at payment.

---

## 6. Checkout Session Types

Every purchase flows through a checkout session — a short-lived record holding purchase intent before payment. All four session types work for both physical and digital products.

| Session Type | Description |
|---|---|
| `REGULAR_DIRECTLY` | Single-item purchase from the product page |
| `REGULAR_CART` | Multi-item purchase from a cart |
| `GROUP_PURCHASE` | Coordinated group buy at a discounted group price |
| `INSTALLMENT` | Down payment now, remainder paid over time |

Sessions expire (typically 15–30 minutes). During this window, inventory is held for both physical and digital products — preventing overselling while the buyer completes payment.

---

## 7. Shipping Logic

```
All items digital?  →  Skip shipping entirely. Cost = 0.
                        Inventory hold still applies.
Any item physical?  →  Shipping address + method required. Inventory held.

Mixed cart (physical + digital, same shop)?
    →  Engine splits into TWO sessions automatically:
           Session 1: physical items  →  shipping lifecycle
           Session 2: digital items   →  download lifecycle
```

---

## 8. Payment Infrastructure (Shared by All Types)

Payment works identically regardless of product type:

```
Buyer wallet  →  debited
Escrow        →  credited (money held, not yet with seller)
Ledger entry  →  recorded (double-entry, full audit trail)
Tx history    →  updated for buyer
```

For installment purchases, only the down payment moves at session time. Each subsequent payment creates its own ledger entry.

> **Escrow is the control point.**
> Physical → held until buyer confirms delivery.
> Digital  → released immediately at order creation.

---

## 9. Post-Payment Fulfillment — The Core Split

### Physical path

```
Payment completes
  → stockQuantity decremented
  → Inventory hold released
  → Order created  [PENDING_SHIPMENT]
  → Seller notified
  → Seller ships → marks order SHIPPED
  → 6-digit confirmation code generated → sent to buyer
  → Buyer enters code in app
  → Escrow releases to seller
  → Order  [COMPLETED]
```

> Escrow is held the entire shipping period. The code is the handshake — seller cannot claim money without buyer confirming receipt.

---

### Digital path

```
Payment completes
  → stockQuantity decremented
  → Inventory hold released
  → Order created  [COMPLETED immediately]
  → Escrow released to seller immediately
  → DigitalDownloadAccess records created (one per file)
  → Buyer notified with download link
  → Buyer downloads within expiry window
```

> No shipping. No confirmation code. Delivery = access records created.

---

## 10. Group Purchase — Physical vs Digital

Both product types support group purchase. The financial hold (escrow per participant) and inventory hold are identical in both cases during the waiting period. What differs is post-completion fulfillment.

### Physical group purchase

```
Each participant pays → Escrow held + Inventory held per participant
Group fills → COMPLETED
  → Physical orders created for all participants  [PENDING_SHIPMENT]
  → stockQuantity decremented for all participants
  → Each participant goes through shipping lifecycle individually
  → Each escrow releases on individual delivery confirmation

Group expires without filling:
  → All escrows refunded
  → All inventory holds released
```

### Digital group purchase

```
Each participant pays → Escrow held + Inventory held per participant
Group fills → COMPLETED
  → Orders created for all participants  [COMPLETED immediately]
  → stockQuantity decremented for all participants
  → All escrows released immediately
  → DigitalDownloadAccess records created for every participant
  → All buyers can download immediately

Group expires without filling:
  → All escrows refunded
  → All inventory holds released  (identical to physical)
```

> The seller's participant cap on the group instance is separate from `stockQuantity`. Both are enforced — a buyer cannot join if either the group is full or the product stock is exhausted.

---

## 11. Installment Purchase — Physical vs Digital

Both product types support installment. The payment schedule, ledger entries, agreement lifecycle, early payoff, and flexible payment features are identical. What differs is fulfillment timing.

### Fulfillment timing options

**IMMEDIATE** — order and access created after the down payment. Buyer gets the product now, pays over time.
- Physical: seller ships after down payment, takes risk on future payments.
- Digital: buyer downloads immediately. If buyer defaults, downloaded files cannot be revoked. Seller has no recourse.

**AFTER_PAYMENT** — order and access created only after the final payment clears.
- Physical: layaway — seller holds stock, ships at the end.
- Digital: safest model — access never opens until fully paid. On default, access records simply never created.

> **Platform recommendation:** AFTER_PAYMENT is the default for digital installment products. Sellers must explicitly opt into IMMEDIATE with acknowledgment that pre-delivery means no recourse on default.

### On default — digital IMMEDIATE

```
Buyer stops paying → Agreement DEFAULTED
  → No new access records created for future files
  → Already-downloaded files cannot be revoked  ← known limitation, seller accepts this
```

### On default — digital AFTER_PAYMENT

```
Buyer stops paying → Agreement DEFAULTED
  → Access records never created
  → No content ever delivered  ← clean outcome
```

---

## 12. The Download System

On every successful digital purchase (any session type, any fulfillment trigger):

```
Platform creates DigitalDownloadAccess record per file:
  - buyer ID + order ID + file ID
  - downloadCount (starts at 0)
  - maxDownloads (null = unlimited, or seller-set cap)
  - accessExpiresAt (now + expiry window)
  - firstDownloadAt (set on first use)
```

On every download request:

```
Buyer hits authenticated endpoint
  → Check 1: Does buyer own an active access record for this file?
  → Check 2: Has expiry window passed?
  → Check 3: Has download cap been reached?

All pass → Platform generates presigned GET URL (5-min TTL)
         → Buyer browser downloads DIRECTLY from MinIO private bucket
         → Access record downloadCount incremented
         → API server is NOT in the file transfer path
```

> The 5-minute TTL means a leaked URL is useless within minutes. The raw MinIO object key is never exposed to the buyer.

---

## 13. Scenario Walkthrough — All Combinations

### Scenario A — Direct purchase, physical product

> Buyer finds a T-shirt. Clicks "Buy Now". Quantity: 1.

```
REGULAR_DIRECTLY session created
  → Stock check passes · Inventory held
  → Shipping address + method required
  → Buyer pays → Escrow funded
  → stockQuantity decremented · hold released
  → Order created  [PENDING_SHIPMENT]
  → Seller ships → marks SHIPPED
  → 6-digit code sent to buyer
  → Buyer confirms → Escrow released
  → Order  [COMPLETED]
```

---

### Scenario B — Direct purchase, digital product

> Buyer finds a PDF course. Clicks "Buy Now". Quantity: 1.

```
REGULAR_DIRECTLY session created
  → Stock check passes · Inventory held
  → No shipping fields collected
  → Buyer pays → Escrow funded and immediately released
  → stockQuantity decremented · hold released
  → Order created  [COMPLETED]
  → 3 DigitalDownloadAccess records created (one per chapter PDF)
  → Buyer receives download link · downloads within 7 days
```

---

### Scenario C — Direct purchase, digital product, quantity 3

> Buyer wants 3 software licenses to distribute to colleagues.

```
REGULAR_DIRECTLY session created  (quantity: 3)
  → Stock check: stockQuantity >= 3? passes · 3 units held
  → No shipping fields collected
  → Buyer pays (unitPrice × 3) → Escrow funded and immediately released
  → stockQuantity decremented by 3 · hold released
  → Order created  [COMPLETED]
  → 6 DigitalDownloadAccess records created (3 sets × 2 files per product)
  → Each set is independent — buyer can share with colleagues
```

---

### Scenario D — Cart, physical only, multiple shops

> Phone case from Shop A + charger from Shop B.

```
REGULAR_CART session created
  → Inventory held for both items
  → Buyer pays once
  → Order engine groups by shop:
       Order 1: Shop A  [PENDING_SHIPMENT]
       Order 2: Shop B  [PENDING_SHIPMENT]
  → Each seller ships independently
  → Each escrow releases on individual buyer confirmation
```

---

### Scenario E — Cart, digital only

> Video course + design template pack.

```
REGULAR_CART session created
  → Stock check + inventory hold for both digital products
  → No shipping. 
  → Buyer pays once
  → Order 1: video course     [COMPLETED] → 4 access records
  → Order 2: template pack    [COMPLETED] → 2 access records
  → Buyer downloads all 6 files within 7 days
```

---

### Scenario F — Cart, mixed physical + digital, same shop

> Printed book (physical) + PDF supplement (digital), same shop.

```
Engine detects mixed cart → splits automatically:
  Session 1: printed book  →  shipping lifecycle · inventory held
  Session 2: PDF           →  download lifecycle · inventory held

Buyer sees one checkout flow, gets two orders in history:
  Order 1 (book):  [PENDING_SHIPMENT] → shipping → confirmation → escrow releases
  Order 2 (PDF):   [COMPLETED]        → download access immediate
```

---

### Scenario G — Group purchase, physical product

> 5 buyers collectively buy a speaker.

```
Buyer 1 initiates → GROUP_PURCHASE session → pays → group instance created (timer starts)
Buyers 2–5 join   → each pays → inventory held + escrow held per participant
Group fills → COMPLETED
  → Physical orders created for all 5  [PENDING_SHIPMENT]
  → stockQuantity decremented for all 5
  → Each participant ships independently
  → Each escrow releases on individual delivery confirmation

Timer expires before filling:
  → All 5 escrows refunded · all inventory holds released
```

---

### Scenario H — Group purchase, digital product

> 30 buyers join a cohort-based online course. Seller stock: 30.

```
Buyers 1–30 join → each pays → inventory held + escrow held per participant
Group fills (30th seat) → COMPLETED
  → Orders created for all 30  [COMPLETED immediately]
  → stockQuantity decremented by 30 (now 0 — sold out)
  → All 30 escrows released immediately
  → DigitalDownloadAccess records created for every participant
  → All 30 buyers can download immediately

Timer expires before filling:
  → All escrows refunded · all inventory holds released
```

---

### Scenario I — Installment, physical, IMMEDIATE fulfillment

> Laptop, 6-month plan, 20% down, seller ships immediately.

```
INSTALLMENT session created
  → Stock check · inventory held
  → Down payment charged (20%)
  → Agreement created (6 scheduled payments)
  → Order created  [PENDING_SHIPMENT]  ← immediately after down payment
  → stockQuantity decremented · hold released
  → Seller ships
  → Monthly payments auto-process via scheduled jobs
```

---

### Scenario J — Installment, physical, AFTER_PAYMENT fulfillment

> Same laptop, layaway model.

```
INSTALLMENT session created
  → Stock check · inventory held
  → Down payment charged
  → Agreement created
  → NO order created yet · inventory held until final payment
  → Monthly payments auto-process
  → Final payment clears → Agreement COMPLETED
  → Order created  [PENDING_SHIPMENT]  ← only now
  → stockQuantity decremented · hold released
  → Shipping lifecycle begins
```

---

### Scenario K — Installment, digital, AFTER_PAYMENT (recommended)

> Video course library, 500,000 TZS, 3-month plan.

```
INSTALLMENT session created
  → Stock check · inventory held
  → Down payment charged (30%)
  → Agreement created (3 scheduled payments)
  → NO order, NO download access yet · inventory held
  → Monthly payments auto-process
  → Final payment clears → Agreement COMPLETED
  → stockQuantity decremented · hold released
  → Order created  [COMPLETED]
  → DigitalDownloadAccess records created  ← only now
  → Buyer downloads

If buyer defaults:
  → Agreement DEFAULTED
  → Inventory hold released · stockQuantity restored
  → Access records never created · no content delivered
```

---

### Scenario L — Installment, digital, IMMEDIATE fulfillment

> Same course, seller opts into IMMEDIATE.

```
INSTALLMENT session created
  → Stock check · inventory held
  → Down payment charged (30%)
  → Agreement created
  → stockQuantity decremented · hold released
  → Order created  [COMPLETED]           ← immediately
  → DigitalDownloadAccess records created ← immediately
  → Buyer downloads now

Monthly payments continue auto-processing.

If buyer defaults:
  → Agreement DEFAULTED
  → Already-downloaded files CANNOT be revoked  ← seller accepted this risk
```

---

## 14. Escrow Behavior Summary

| Scenario | Inventory Held | Escrow Released |
|---|---|---|
| Physical — direct | Yes, at session creation | On delivery confirmation |
| Digital — direct | Yes, at session creation | Immediately at order creation |
| Physical — group | Yes, per participant | Per delivery confirmation |
| Digital — group | Yes, per participant | Immediately when group completes |
| Group — expired / failed | Released to stock | Refunded to all participants |
| Physical — installment IMMEDIATE | Yes, until down pmt | Per installment via direct ledger |
| Physical — installment AFTER_PAYMENT | Yes, until final pmt | After final payment |
| Digital — installment IMMEDIATE | Yes, until down pmt | After down payment |
| Digital — installment AFTER_PAYMENT | Yes, until final pmt | After final payment |

---

## 15. MinIO Bucket Architecture

```
nextgate-public  (world-readable)
  → Product images · shop logos · avatars · category images
  → URLs are permanent · no expiry

nextgate-digital-content  (private · no public access)
  → Paid digital product files only
  → Accessible only via presigned URLs (5-min TTL)
  → Generated by API after access verification
  → Raw object key never exposed to buyers
```

> This separation ensures a seller can never accidentally expose a paid file by uploading to the wrong location.

---

## 16. Key Invariants

```
- Every money movement goes through the double-entry ledger.
  No direct wallet balance adjustments exist.

- Escrow receives the full payment before any fulfillment action begins.

- Every product has a stockQuantity — physical and digital alike.
  There is no "unlimited" mode. Sellers set a real number.

- Inventory is held for both physical and digital products during the
  checkout session window. Released on expiry, cancellation, or payment.

- Orders are never created before:
    regular purchase  → payment completes
    group purchase    → group fills
    installment       → fulfillment trigger (down pmt or final pmt)

- Download links never expose the raw MinIO object key.
  Only opaque access identifiers resolve to presigned URLs via authenticated API.

- A checkout session produces orders exactly once.
  Idempotency checks prevent duplicates under retry or failure.

- Session expiry is enforced before payment processing.
  An expired session cannot be paid.

- For digital installment IMMEDIATE: already-downloaded files cannot be
  revoked on default. Sellers must explicitly accept this risk.

- For digital installment AFTER_PAYMENT: default means access records
  are never created and stockQuantity is restored. Clean outcome.
```