# E_commerce-nexgate-service(7)

# Cart Management

**Author**: Josh S. Sakweli, Backend Lead Team **Last Updated**: 2025-09-23 **Version**: v1.0

**Short Description**: The Cart Management API provides shopping cart functionality for the NextGate e-commerce platform. It supports adding products to cart, updating quantities, removing items, and real-time stock validation.

**Base URL**: `api/v1/e-commerce/cart`

**Hints**:

- All cart operations require user authentication via Bearer token
- Each user has one persistent cart that maintains state across sessions
- Real-time stock validation prevents overselling
- Adding an existing product updates its quantity (additive)
- Cart persistence survives user logout/login cycles

---

## Endpoints

## 1. Add Product to Cart

**Purpose**: Adds a product to the user's cart or updates quantity if already exists

**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> `{base}/add`

**Access Level**: 🔒 Protected (Requires authentication)

**Authentication**: Bearer Token

**Request Headers**:

<table id="bkmrk-header-type-required"><thead><tr><th>Header</th><th>Type</th><th>Required</th><th>Description</th></tr></thead><tbody><tr><td>Authorization</td><td>string</td><td>Yes</td><td>Bearer token for authentication</td></tr><tr><td>Content-Type</td><td>string</td><td>Yes</td><td>application/json</td></tr></tbody></table>

**Request JSON Sample**:

```json
{
  "productId": "456e7890-e89b-12d3-a456-426614174001",
  "quantity": 2
}

```

**Request Body Parameters**:

<table id="bkmrk-parameter-type-requi"><thead><tr><th>Parameter</th><th>Type</th><th>Required</th><th>Description</th><th>Validation</th></tr></thead><tbody><tr><td>productId</td><td>UUID</td><td>Yes</td><td>ID of the product to add</td><td>Not null, product must exist and be active</td></tr><tr><td>quantity</td><td>integer</td><td>Yes</td><td>Quantity to add to cart</td><td>Min: 1</td></tr></tbody></table>

**Response JSON Sample (New Item)**:

```json
{
  "success": true,
  "message": "Product added to cart successfully",
  "data": null
}

```

**Response JSON Sample (Updated Existing)**:

```json
{
  "success": true,
  "message": "Product quantity updated in cart successfully",
  "data": null
}

```

**Business Logic**:

- Creates cart automatically if one doesn't exist
- If product already in cart, adds to existing quantity
- Validates total quantity doesn't exceed available stock
- Only allows active products to be added

**Error Responses**:

- `400 Bad Request`: Invalid productId or quantity validation error
- `401 Unauthorized`: Authentication required
- `404 Not Found`: Product not found or not active
- `422 Unprocessable Entity`: Insufficient stock available

**Error Examples**:

```json
{
  "success": false,
  "message": "Insufficient stock for 'iPhone 15 Pro'. Only 3 units available",
  "data": null
}

```

```json
{
  "success": false,
  "message": "Cannot add more items. Total quantity (8) would exceed available stock (5) for 'MacBook Pro'",
  "data": null
}

```

---

## 2. Get Shopping Cart

**Purpose**: Retrieves the complete shopping cart with all items and pricing

**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> `{base}`

**Access Level**: 🔒 Protected (Requires authentication)

**Authentication**: Bearer Token

**Request Headers**:

<table id="bkmrk-header-type-required-1"><thead><tr><th>Header</th><th>Type</th><th>Required</th><th>Description</th></tr></thead><tbody><tr><td>Authorization</td><td>string</td><td>Yes</td><td>Bearer token for authentication</td></tr></tbody></table>

**Response JSON Sample**:

```json
{
  "success": true,
  "message": "Shopping cart retrieved successfully",
  "data": {
    "user": {
      "userId": "111e2222-e89b-12d3-a456-426614174003",
      "userName": "john.doe",
      "name": "John Doe"
    },
    "cartSummary": {
      "totalItems": 2,
      "totalQuantity": 3,
      "subtotal": 2198.00,
      "totalDiscount": 0.00,
      "totalAmount": 2198.00
    },
    "cartItems": [
      {
        "itemId": "cart001-e89b-12d3-a456-426614174004",
        "productId": "456e7890-e89b-12d3-a456-426614174001",
        "productName": "iPhone 15 Pro Max 512GB",
        "productSlug": "iphone-15-pro-max-512gb",
        "productImage": "https://example.com/images/iphone15-pro-max.jpg",
        "productType": "PHYSICAL",
        "unitPrice": 1199.00,
        "quantity": 2,
        "itemSubtotal": 2398.00,
        "totalPrice": 2398.00,
        "shop": {
          "shopId": "123e4567-e89b-12d3-a456-426614174000",
          "shopName": "TechStore Pro",
          "shopSlug": "techstore-pro",
          "logoUrl": "https://example.com/logos/techstore-pro.png"
        },
        "availability": {
          "inStock": true,
          "availableQuantity": 25,
          "maxPerCustomer": 5
        },
        "addedAt": "2025-09-23T10:30:00Z"
      },
      {
        "itemId": "cart002-e89b-12d3-a456-426614174005",
        "productId": "567e8901-e89b-12d3-a456-426614174002",
        "productName": "MacBook Air M3",
        "productSlug": "macbook-air-m3",
        "productImage": "https://example.com/images/macbook-air-m3.jpg",
        "productType": "PHYSICAL",
        "unitPrice": 999.00,
        "quantity": 1,
        "itemSubtotal": 999.00,
        "totalPrice": 999.00,
        "shop": {
          "shopId": "123e4567-e89b-12d3-a456-426614174000",
          "shopName": "TechStore Pro",
          "shopSlug": "techstore-pro",
          "logoUrl": "https://example.com/logos/techstore-pro.png"
        },
        "availability": {
          "inStock": true,
          "availableQuantity": 8,
          "maxPerCustomer": null
        },
        "addedAt": "2025-09-23T11:15:00Z"
      }
    ],
    "updatedAt": "2025-09-23T14:20:00Z"
  }
}

```

**Response Structure**:

### User Summary

<table id="bkmrk-field-description-us"><thead><tr><th>Field</th><th>Description</th></tr></thead><tbody><tr><td>userId</td><td>Unique identifier of the cart owner</td></tr><tr><td>userName</td><td>User's login username</td></tr><tr><td>name</td><td>User's full name (firstName + lastName)</td></tr></tbody></table>

### Cart Summary

<table id="bkmrk-field-description-to"><thead><tr><th>Field</th><th>Description</th></tr></thead><tbody><tr><td>totalItems</td><td>Number of distinct products in cart</td></tr><tr><td>totalQuantity</td><td>Total quantity of all items combined</td></tr><tr><td>subtotal</td><td>Total price before discounts</td></tr><tr><td>totalDiscount</td><td>Total discount amount applied</td></tr><tr><td>totalAmount</td><td>Final amount after discounts</td></tr></tbody></table>

### Cart Item Details

<table id="bkmrk-field-description-it"><thead><tr><th>Field</th><th>Description</th></tr></thead><tbody><tr><td>itemId</td><td>Unique identifier for the cart item</td></tr><tr><td>productId</td><td>Product identifier</td></tr><tr><td>productName</td><td>Current product name</td></tr><tr><td>productSlug</td><td>URL-friendly product identifier</td></tr><tr><td>productImage</td><td>Primary product image URL</td></tr><tr><td>productType</td><td>PHYSICAL or DIGITAL</td></tr><tr><td>unitPrice</td><td>Current product price per unit</td></tr><tr><td>quantity</td><td>Quantity of this product in cart</td></tr><tr><td>itemSubtotal</td><td>unitPrice × quantity</td></tr><tr><td>totalPrice</td><td>itemSubtotal after discounts</td></tr><tr><td>shop</td><td>Shop information (id, name, slug, logo)</td></tr><tr><td>availability</td><td>Real-time stock information</td></tr><tr><td>addedAt</td><td>When item was first added to cart</td></tr></tbody></table>

### Product Availability

<table id="bkmrk-field-description-in"><thead><tr><th>Field</th><th>Description</th></tr></thead><tbody><tr><td>inStock</td><td>Whether the product is currently in stock</td></tr><tr><td>availableQuantity</td><td>Current available inventory</td></tr><tr><td>maxPerCustomer</td><td>Maximum quantity a single customer can purchase (null if no limit)</td></tr></tbody></table>

**Error Responses**:

- `401 Unauthorized`: Authentication required
- `404 Not Found`: User not found

---

## 3. Update Cart Item Quantity

**Purpose**: Updates the quantity of a specific item in the cart

**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> `{base}/items/{itemId}`

**Access Level**: 🔒 Protected (Requires authentication)

**Authentication**: Bearer Token

**Request Headers**:

<table id="bkmrk-header-type-required-2"><thead><tr><th>Header</th><th>Type</th><th>Required</th><th>Description</th></tr></thead><tbody><tr><td>Authorization</td><td>string</td><td>Yes</td><td>Bearer token for authentication</td></tr><tr><td>Content-Type</td><td>string</td><td>Yes</td><td>application/json</td></tr></tbody></table>

**Path Parameters**:

<table id="bkmrk-parameter-type-requi-1"><thead><tr><th>Parameter</th><th>Type</th><th>Required</th><th>Description</th></tr></thead><tbody><tr><td>itemId</td><td>UUID</td><td>Yes</td><td>ID of the cart item to update</td></tr></tbody></table>

**Request JSON Sample**:

```json
{
  "quantity": 3
}

```

**Request Body Parameters**:

<table id="bkmrk-parameter-type-requi-2"><thead><tr><th>Parameter</th><th>Type</th><th>Required</th><th>Description</th><th>Validation</th></tr></thead><tbody><tr><td>quantity</td><td>integer</td><td>Yes</td><td>New quantity for the cart item</td><td>Min: 1</td></tr></tbody></table>

**Response JSON Sample**:

```json
{
  "success": true,
  "message": "Product quantity updated successfully",
  "data": null
}

```

**Business Logic**:

- Updates quantity to the exact specified value (not additive)
- Validates new quantity doesn't exceed current stock
- Only allows updating items in the authenticated user's own cart

**Error Responses**:

- `400 Bad Request`: Invalid quantity value
- `401 Unauthorized`: Authentication required
- `404 Not Found`: Cart item not found in user's cart
- `422 Unprocessable Entity`: Insufficient stock for requested quantity

**Error Example**:

```json
{
  "success": false,
  "message": "Insufficient stock for 'iPhone 15 Pro'. Only 2 units available",
  "data": null
}

```

---

## 4. Remove Cart Item

**Purpose**: Removes a specific item completely from the cart

**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}/items/{itemId}`

**Access Level**: 🔒 Protected (Requires authentication)

**Authentication**: Bearer Token

**Request Headers**:

<table id="bkmrk-header-type-required-3"><thead><tr><th>Header</th><th>Type</th><th>Required</th><th>Description</th></tr></thead><tbody><tr><td>Authorization</td><td>string</td><td>Yes</td><td>Bearer token for authentication</td></tr></tbody></table>

**Path Parameters**:

<table id="bkmrk-parameter-type-requi-3"><thead><tr><th>Parameter</th><th>Type</th><th>Required</th><th>Description</th></tr></thead><tbody><tr><td>itemId</td><td>UUID</td><td>Yes</td><td>ID of the cart item to remove</td></tr></tbody></table>

**Response JSON Sample**:

```json
{
  "success": true,
  "message": "Product removed from cart successfully",
  "data": null
}

```

**Business Logic**:

- Completely removes the cart item and all its quantity
- Only allows removing items from the authenticated user's own cart

**Error Responses**:

- `401 Unauthorized`: Authentication required
- `404 Not Found`: Cart item not found in user's cart

---

## 5. Clear Shopping Cart

**Purpose**: Removes all items from the user's cart

**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}/clear`

**Access Level**: 🔒 Protected (Requires authentication)

**Authentication**: Bearer Token

**Request Headers**:

<table id="bkmrk-header-type-required-4"><thead><tr><th>Header</th><th>Type</th><th>Required</th><th>Description</th></tr></thead><tbody><tr><td>Authorization</td><td>string</td><td>Yes</td><td>Bearer token for authentication</td></tr></tbody></table>

**Response JSON Sample**:

```json
{
  "success": true,
  "message": "Shopping cart cleared successfully",
  "data": null
}

```

**Business Logic**:

- Removes all cart items for the authenticated user
- Cart entity remains but becomes empty

**Error Responses**:

- `401 Unauthorized`: Authentication required
- `404 Not Found`: User not foundrl at

# Shops Management Service

The Shops Management Service provides comprehensive shop management capabilities for the NextGate social commerce platform. This service handles shop categories, shop creation and management, rating systems, and review functionality with role-based access controls and business logic enforcement.

**Hints**:

\- Shop Approval: All shops are auto-approved upon creation (isApproved: true)

\- Business Rules: Shop owners cannot rate/review their own shops

\- One Per User: Each user can submit only one rating and one review per shop

\- Admin Categories: Shop categories require SUPER\_ADMIN role for management

\- Soft Deletion: Shops use soft delete (isDeleted flag) to maintain data integrity

\- Slug Generation: Shop slugs are auto-generated from names with uniqueness validation

\- Rating Scale: Ratings use 1-5 star system with summary statistics

\- Review Moderation: Reviews support status management (ACTIVE, HIDDEN, FLAGGED, UNDER\_REVIEW)

# Shop Management

**Author**: Josh S. Sakweli, Backend Lead Team  
**Last Updated**: 2026-05-19  
**Version**: v2.0

**Short Description**: The Shop Management API provides endpoints for creating, managing, and retrieving shop information on the NextGate platform. Covers shop registration, updates, approvals, WABA (WhatsApp Business) integration, AI chatbot toggling, and conversation history.

**Hints**:
- All shop endpoints use the prefix `api/v1/e-commerce/shops`
- Shop creation requires authentication; public users can only read
- Pagination uses 1-based page numbering
- Shop approval operations require `ROLE_SUPER_ADMIN` or `ROLE_STAFF_ADMIN`
- Featured shops are randomized on each request
- Rating and review data is automatically included in shop responses (read-only — managed by separate review service)
- WABA = WhatsApp Business Account integration for the shop's AI-powered chatbot

---

## Standard Response Format

```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Operation completed successfully",
  "action_time": "2026-05-19T10:30:45",
  "data": { }
}
```

Error responses follow the same envelope with `"success": false` and `"data"` set to the error message string.

---

## Endpoints

## 1. Create Shop
**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`

**Access Level**: 🔒 Protected

**Request Body**:
| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| shopName | string | Yes | Name of the shop | Min: 2, Max: 100 chars |
| shopDescription | string | Yes | Detailed description | Max: 1000 chars |
| phoneNumber | string | Yes | Contact phone | Pattern: `^\+?[0-9]{10,15}$` |
| city | string | Yes | City | Min: 2, Max: 50 chars |
| region | string | Yes | Region/state | Min: 2, Max: 50 chars |
| logoUrl | string | No | Shop logo URL | Valid URL, max 1000 chars |
| bannerUrl | string | No | Shop banner URL | Valid URL, max 1000 chars |
| shopImages | array | No | Array of shop image URLs | Valid URLs, max 1000 chars each |
| email | string | No | Contact email | Valid email, max 100 chars |
| countryCode | string | No | Country code | Max: 3 chars, Default: `"TZ"` |
| streetAddress | string | No | Street address | Max: 255 chars |
| landmark | string | No | Landmark / location notes | Max: 300 chars |
| latitude | decimal | No | GPS latitude | Range: -90.0 to 90.0 |
| longitude | decimal | No | GPS longitude | Range: -180.0 to 180.0 |

**Request JSON Sample**:
```json
{
  "shopName": "Mama Lucy's Restaurant",
  "shopDescription": "Authentic Tanzanian cuisine in the heart of Dar es Salaam",
  "phoneNumber": "+255123456789",
  "city": "Dar es Salaam",
  "region": "Dar es Salaam",
  "logoUrl": "https://example.com/logo.jpg",
  "bannerUrl": "https://example.com/banner.jpg",
  "shopImages": ["https://example.com/shop1.jpg"],
  "email": "info@mamalucy.co.tz",
  "countryCode": "TZ",
  "streetAddress": "Msimbazi Street, Block 45",
  "landmark": "Near the main bus stop",
  "latitude": -6.7924,
  "longitude": 39.2083
}
```

**Response JSON Sample**:
```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Shop created successfully",
  "action_time": "2026-05-19T10:30:45",
  "data": {
    "shopId": "123e4567-e89b-12d3-a456-426614174000",
    "shopName": "Mama Lucy's Restaurant",
    "shopSlug": "mama-lucys-restaurant",
    "shopDescription": "Authentic Tanzanian cuisine...",
    "logoUrl": "https://example.com/logo.jpg",
    "bannerUrl": "https://example.com/banner.jpg",
    "shopImages": ["https://example.com/shop1.jpg"],
    "ownerId": "456e7890-e89b-12d3-a456-426614174001",
    "ownerName": "Lucy Mwalimu",
    "status": "PENDING",
    "phoneNumber": "+255123456789",
    "email": "info@mamalucy.co.tz",
    "streetAddress": "Msimbazi Street, Block 45",
    "city": "Dar es Salaam",
    "region": "Dar es Salaam",
    "countryCode": "TZ",
    "latitude": -6.7924,
    "longitude": 39.2083,
    "landmark": "Near the main bus stop",
    "isVerified": false,
    "verificationBadge": null,
    "trustScore": 0.00,
    "isApproved": true,
    "createdAt": "2026-05-19T10:30:45",
    "updatedAt": "2026-05-19T10:30:45",
    "approvedAt": null,
    "averageRating": null,
    "totalRatings": 0,
    "totalActiveReviews": 0,
    "reviews": []
  }
}
```

**Error Responses**:
- `400`: Shop name already exists
- `401`: Authentication required
- `422`: Validation errors

---

## 2. Get All Shops
**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/all`

**Access Level**: 🌐 Public

Returns a list of all shops with summary info and top 5 reviews per shop.

---

## 3. Get All Shops (Paginated)
**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/all-paged`

**Access Level**: 🌐 Public

**Query Parameters**:
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| page | integer | 1 | Page number (1-based) |
| size | integer | 10 | Items per page (max: 100) |

**Response JSON Sample**:
```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Shops retrieved successfully",
  "action_time": "2026-05-19T10:30:45",
  "data": {
    "shops": [ { "...ShopSummary fields..." } ],
    "currentPage": 1,
    "pageSize": 10,
    "totalElements": 150,
    "totalPages": 15,
    "hasNext": true,
    "hasPrevious": false,
    "isFirst": true,
    "isLast": false
  }
}
```

---

## 4. Update Shop
**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}`

**Access Level**: 🔒 Protected (Shop Owner only)

**Path Parameters**:
| Parameter | Type | Description |
|-----------|------|-------------|
| shopId | UUID | ID of the shop |

All request body fields are optional — only provided fields are updated:

| Parameter | Type | Validation |
|-----------|------|------------|
| shopName | string | Min: 2, Max: 100 chars |
| shopDescription | string | Max: 1000 chars |
| logoUrl | string | Valid URL |
| bannerUrl | string | Valid URL |
| shopImages | array | Valid URLs |
| phoneNumber | string | `^\+?[0-9]{10,15}$` |
| email | string | Valid email |
| streetAddress | string | Max: 255 chars |
| city | string | Min: 2, Max: 50 chars |
| region | string | Min: 2, Max: 50 chars |
| countryCode | string | Max: 3 chars |
| latitude | decimal | -90.0 to 90.0 |
| longitude | decimal | -180.0 to 180.0 |
| landmark | string | Max: 300 chars |

**Response**: Full `ShopResponse` (same shape as Create response)

**Error Responses**:
- `400`: Not the shop owner, or shop is deleted
- `401`: Authentication required
- `404`: Shop not found

---

## 5. Get Shop by ID (Summary)
**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}`

**Access Level**: 🌐 Public

Returns `ShopSummaryListResponse` — public-facing fields including top 5 reviews and rating.

---

## 6. Get Shop by ID (Detailed)
**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}/detailed`

**Access Level**: 🔒 Protected (Shop Owner or Admin)

Returns full `ShopResponse` including all reviews and management fields.

---

## 7. Get My Shops
**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/my-shops`

**Access Level**: 🔒 Protected

Returns all shops owned by the authenticated user (`ShopSummaryListResponse` list).

---

## 8. Get My Shops (Paginated)
**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/my-shops-paged`

**Access Level**: 🔒 Protected

**Query Parameters**:
| Parameter | Default | Description |
|-----------|---------|-------------|
| page | 1 | Page number (1-based) |
| size | 10 | Items per page (max: 100) |

---

## 9. Approve / Reject Shop
**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}/approve-shop`

**Access Level**: 🔒 Protected (`ROLE_SUPER_ADMIN` or `ROLE_STAFF_ADMIN`)

**Query Parameters**:
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| approve | boolean | Yes | `true` to approve, `false` to reject |

**Response JSON Sample**:
```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Shop approval status changed successfully",
  "action_time": "2026-05-19T10:30:45",
  "data": {
    "shopId": "123e4567-e89b-12d3-a456-426614174000",
    "shopName": "Mama Lucy's Restaurant",
    "isApproved": true,
    "approvedAt": "2026-05-19T10:30:45"
  }
}
```

---

## 10. Get Featured Shops
**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/featured`

**Access Level**: 🌐 Public

Returns up to 20 randomly selected featured shops (`ShopSummaryListResponse` list).

---

## 11. Get Featured Shops (Paginated)
**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/featured-paged`

**Access Level**: 🌐 Public

**Query Parameters**:
| Parameter | Default | Description |
|-----------|---------|-------------|
| page | 1 | Page number |
| size | 10 | Items per page (max: 100) |

---

## 12. Search Shops
**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/search`

**Access Level**: 🌐 Public

**Query Parameters**:
| Parameter | Type | Required | Default | Description |
|-----------|------|----------|---------|-------------|
| q | string | No | — | Search query |
| page | integer | No | 1 | Page number |
| size | integer | No | 10 | Items per page |

**Response JSON Sample**:
```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Shops retrieved successfully",
  "action_time": "2026-05-19T10:30:45",
  "data": {
    "content": [ { "...ShopSearchResponse fields..." } ],
    "totalElements": 8,
    "totalPages": 1,
    "currentPage": 1,
    "pageSize": 10,
    "hasNext": false,
    "hasPrevious": false
  }
}
```

---

## 13. Get Shop Summary Stats
**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}/summary-stats`

**Access Level**: 🌐 Public

Returns aggregated review and rating statistics for a shop, including per-user activity.

**Response JSON Sample**:
```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Shop summary stats retrieved successfully",
  "data": {
    "shopId": "123e4567-e89b-12d3-a456-426614174000",
    "shopName": "Mama Lucy's Restaurant",
    "averageRating": 4.5,
    "totalRatings": 25,
    "ratingDistribution": { "1": 1, "2": 2, "3": 5, "4": 7, "5": 10 },
    "totalReviews": 15,
    "activeReviews": 12,
    "hiddenReviews": 2,
    "flaggedReviews": 1,
    "userActivities": [
      {
        "userId": "user-123",
        "userName": "John Doe",
        "feedbackId": "rev-123",
        "reviewText": "Amazing food and service!",
        "reviewStatus": "ACTIVE",
        "ratingValue": 5,
        "date": "2026-05-19T14:30:00",
        "hasReview": true,
        "hasRating": true
      }
    ]
  }
}
```

---

## 14. WABA (WhatsApp Business) Integration

WABA allows shops to receive and respond to WhatsApp customer messages via an AI-powered chatbot. The flow is: shop registers a WABA → admin approves with Meta credentials → shop toggles AI on/off and monitors conversation history.

**Base path for all WABA endpoints**: `api/v1/e-commerce/shops/{shopId}/waba`

**Shared Path Parameter**:
| Parameter | Type | Description |
|-----------|------|-------------|
| shopId | UUID | ID of the shop |

---

### 14a. Register WABA
**Purpose**: Shop owner submits a WhatsApp number and display name to begin WABA registration.

**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}/waba/register`

**Access Level**: 🔒 Protected (Shop Owner)

**Request Body**:
| Parameter | Type | Required | Validation |
|-----------|------|----------|------------|
| phoneNumber | string | Yes | Max: 20 chars |
| displayName | string | Yes | Max: 100 chars |

**Request JSON Sample**:
```json
{
  "phoneNumber": "+255712345678",
  "displayName": "Mama Lucy's Restaurant"
}
```

---

### 14b. Approve WABA
**Purpose**: Admin approves a pending WABA registration by supplying Meta WABA credentials.

**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}/waba/approve`

**Access Level**: 🔒 Protected (`ROLE_SUPER_ADMIN` or `ROLE_STAFF_ADMIN`)

**Request Body**:
| Parameter | Type | Required | Validation |
|-----------|------|----------|------------|
| wabaId | string | Yes | Max: 64 chars (Meta WABA ID) |
| phoneNumberId | string | Yes | Max: 64 chars (Meta Phone Number ID) |
| phoneNumber | string | Yes | Max: 20 chars |

---

### 14c. Resubmit WABA
**Purpose**: Shop owner resubmits a rejected or pending WABA with corrected info.

**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}/waba/resubmit`

**Access Level**: 🔒 Protected (Shop Owner)

**Request Body**:
| Parameter | Type | Validation |
|-----------|------|------------|
| phoneNumber | string | Max: 20 chars |
| displayName | string | Max: 100 chars |

---

### 14d. Admin Update WABA
**Purpose**: Admin updates Meta credentials on an existing WABA account.

**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}/waba/admin-update`

**Access Level**: 🔒 Protected (`ROLE_SUPER_ADMIN` or `ROLE_STAFF_ADMIN`)

Same request body as [Resubmit WABA](#14c-resubmit-waba).

---

### 14e. Update WABA Status
**Purpose**: Admin changes the status of a WABA account (e.g., suspend or reactivate).

**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}/waba/status`

**Access Level**: 🔒 Protected (`ROLE_SUPER_ADMIN` or `ROLE_STAFF_ADMIN`)

**Query Parameters**:
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| status | ShopWabaStatus | Yes | `PENDING`, `ACTIVE`, `SUSPENDED`, `REJECTED` |

---

### 14f. Toggle AI Chatbot
**Purpose**: Shop owner enables or disables the AI chatbot for their WABA.

**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}/waba/toggle-ai`

**Access Level**: 🔒 Protected (Shop Owner)

**Query Parameters**:
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| enabled | boolean | Yes | `true` to enable, `false` to disable |

**Response JSON Sample**:
```json
{
  "success": true,
  "message": "AI enabled successfully",
  "data": {
    "shopId": "...",
    "aiEnabled": true,
    "updatedAt": "2026-05-19T11:00:00"
  }
}
```

---

### 14g. Get WABA Conversations
**Purpose**: Retrieves paginated WhatsApp conversation sessions for the shop.

**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}/waba/conversations`

**Access Level**: 🔒 Protected (Shop Owner or Admin)

**Query Parameters**:
| Parameter | Default | Description |
|-----------|---------|-------------|
| page | 1 | Page number |
| size | 10 | Items per page |

**Response JSON Sample**:
```json
{
  "success": true,
  "message": "Conversations retrieved",
  "data": {
    "content": [ { "...WabaSessionResponse fields..." } ],
    "currentPage": 1,
    "pageSize": 10,
    "totalElements": 42,
    "totalPages": 5,
    "hasNext": true,
    "hasPrevious": false
  }
}
```

---

### 14h. Get Session Messages
**Purpose**: Retrieves paginated messages within a specific conversation session.

**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}/waba/conversations/{sessionId}/messages`

**Access Level**: 🔒 Protected (Shop Owner or Admin)

**Additional Path Parameter**:
| Parameter | Type | Description |
|-----------|------|-------------|
| sessionId | UUID | ID of the conversation session |

**Query Parameters**:
| Parameter | Default | Description |
|-----------|---------|-------------|
| page | 1 | Page number |
| size | 20 | Items per page |

---

### 14i. Get Session Messages by Date Range
**Purpose**: Retrieves messages in a session filtered by date range.

**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}/waba/conversations/{sessionId}/messages/by-date`

**Access Level**: 🔒 Protected (Shop Owner or Admin)

**Additional Path Parameter**:
| Parameter | Type | Description |
|-----------|------|-------------|
| sessionId | UUID | ID of the conversation session |

**Query Parameters**:
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| from | LocalDateTime | Yes | Start datetime (ISO 8601) |
| to | LocalDateTime | Yes | End datetime (ISO 8601) |
| page | integer | No | Default: 1 |
| size | integer | No | Default: 20 |

---

## Quick Reference

### Enums

**ShopStatus**: `PENDING`, `ACTIVE`, `SUSPENDED`, `CLOSED`, `UNDER_REVIEW`

**ShopType**: `PHYSICAL`, `ONLINE`, `HYBRID`

**VerificationBadge**: `BRONZE`, `SILVER`, `GOLD`, `PREMIUM`

**ShopWabaStatus**: `PENDING`, `ACTIVE`, `SUSPENDED`, `REJECTED`

### Error Response Codes

| Code | Meaning |
|------|---------|
| `400` | Business logic violation or item already exists |
| `401` | Authentication required or token invalid/expired |
| `403` | Insufficient permissions |
| `404` | Resource not found |
| `422` | Field-level validation errors |

**Validation Error (422)**:
```json
{
  "success": false,
  "httpStatus": "UNPROCESSABLE_ENTITY",
  "message": "Validation failed",
  "data": {
    "shopName": "Shop name must be between 2 and 100 characters",
    "phoneNumber": "Phone number must be between 10-15 digits and may start with +"
  }
}
```

### Access Control Summary
| Role | Capabilities |
|------|-------------|
| Public | Read shops, search, featured, summary stats |
| Authenticated user | All public + create shop, view own shops |
| Shop Owner | All authenticated + update own shop, WABA management, view conversations |
| `ROLE_SUPER_ADMIN` / `ROLE_STAFF_ADMIN` | All + approve/reject shops, approve/update/status WABA |

### WABA Registration Flow
```
1. POST /{shopId}/waba/register       — shop owner submits phone + display name
2. PATCH /{shopId}/waba/approve       — admin supplies Meta wabaId + phoneNumberId
3. PATCH /{shopId}/waba/toggle-ai     — shop owner enables AI chatbot
4. GET  /{shopId}/waba/conversations  — shop owner monitors customer chats
```

# Shop Review

**Author**: Josh S. Sakweli, Backend Lead Team  
**Last Updated**: 2026-05-19  
**Version**: v2.0

**Short Description**: The Shop Review API allows users to write, manage, and retrieve reviews (text + star rating) for shops. Supports create, update, delete, listing with pagination, and summary statistics.

**Hints**:
- All endpoints use the prefix `api/v1/e-commerce/shops/reviews/{shopId}`
- All operations require Bearer token authentication
- One review per user per shop — use PUT to update an existing one
- Shop owners cannot review their own shops
- `reviewText` and `ratingValue` are both optional fields, but at least one should be provided
- `ratingValue` must be 1–5 if provided
- `reviewText` must be 10–1000 characters if provided
- `isMyReview` flag is only populated when the authenticated user is included in the response builder

---

## Standard Response Format

```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "...",
  "action_time": "2026-05-19T10:30:45",
  "data": { }
}
```

---

## Endpoints

## 1. Create Review (Feedback)
**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/reviews/{shopId}`

**Access Level**: 🔒 Protected

**Path Parameters**:
| Parameter | Type | Description |
|-----------|------|-------------|
| shopId | UUID | ID of the shop to review |

**Request Body**:
| Parameter | Type | Required | Validation |
|-----------|------|----------|------------|
| reviewText | string | No | Min: 10, Max: 1000 chars |
| ratingValue | integer | No | Min: 1, Max: 5 |

**Request JSON Sample**:
```json
{
  "reviewText": "Amazing food and excellent service! Highly recommend the local dishes.",
  "ratingValue": 5
}
```

**Response JSON Sample**:
```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Feedback submitted successfully",
  "action_time": "2026-05-19T10:30:45",
  "data": {
    "reviewId": "123e4567-e89b-12d3-a456-426614174000",
    "shopId": "456e7890-e89b-12d3-a456-426614174001",
    "shopName": "Mama Lucy's Restaurant",
    "userId": "789e0123-e89b-12d3-a456-426614174002",
    "userName": "John Doe",
    "reviewText": "Amazing food and excellent service! Highly recommend the local dishes.",
    "ratingValue": 5,
    "status": "ACTIVE",
    "createdAt": "2026-05-19T10:30:45",
    "updatedAt": "2026-05-19T10:30:45",
    "isMyReview": false
  }
}
```

**Error Responses**:
- `400`: Already reviewed this shop, or shop owner reviewing own shop
- `401`: Authentication required
- `404`: Shop not found

---

## 2. Update Review (Feedback)
**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/reviews/{shopId}`

**Access Level**: 🔒 Protected (Review Owner only)

**Path Parameters**:
| Parameter | Type | Description |
|-----------|------|-------------|
| shopId | UUID | ID of the reviewed shop |

**Request Body** (same as Create — all fields optional):
| Parameter | Type | Validation |
|-----------|------|------------|
| reviewText | string | Min: 10, Max: 1000 chars |
| ratingValue | integer | Min: 1, Max: 5 |

**Response JSON Sample**:
```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Feedback updated successfully",
  "action_time": "2026-05-19T11:15:30",
  "data": {
    "reviewId": "123e4567-e89b-12d3-a456-426614174000",
    "shopId": "456e7890-e89b-12d3-a456-426614174001",
    "shopName": "Mama Lucy's Restaurant",
    "userId": "789e0123-e89b-12d3-a456-426614174002",
    "userName": "John Doe",
    "reviewText": "Still amazing food. Local dishes are worth it.",
    "ratingValue": 4,
    "status": "ACTIVE",
    "createdAt": "2026-05-19T10:30:45",
    "updatedAt": "2026-05-19T11:15:30",
    "isMyReview": false
  }
}
```

**Error Responses**:
- `401`: Authentication required
- `404`: Review not found (create one first)

---

## 3. Delete Review (Feedback)
**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/reviews/{shopId}`

**Access Level**: 🔒 Protected (Review Owner only)

**Path Parameters**:
| Parameter | Type | Description |
|-----------|------|-------------|
| shopId | UUID | ID of the reviewed shop |

Soft-deletes the review. Deleted reviews no longer appear in listings or counts.

**Response JSON Sample**:
```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Feedback deleted successfully",
  "action_time": "2026-05-19T10:30:45",
  "data": null
}
```

**Error Responses**:
- `401`: Authentication required
- `404`: Review not found

---

## 4. Get Active Reviews for Shop
**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/reviews/{shopId}`

**Access Level**: 🔒 Protected (auth required to populate `isMyReview`)

**Path Parameters**:
| Parameter | Type | Description |
|-----------|------|-------------|
| shopId | UUID | ID of the shop |

Returns all `ACTIVE` reviews. `isMyReview` is set to `true` for the authenticated user's own review.

**Response JSON Sample**:
```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Reviews retrieved successfully",
  "action_time": "2026-05-19T10:30:45",
  "data": [
    {
      "reviewId": "123e4567-e89b-12d3-a456-426614174000",
      "shopId": "456e7890-e89b-12d3-a456-426614174001",
      "shopName": "Mama Lucy's Restaurant",
      "userId": "789e0123-e89b-12d3-a456-426614174002",
      "userName": "John Doe",
      "reviewText": "Amazing food!",
      "ratingValue": 5,
      "status": "ACTIVE",
      "createdAt": "2026-05-19T10:30:45",
      "updatedAt": "2026-05-19T10:30:45",
      "isMyReview": true
    }
  ]
}
```

**Error Responses**:
- `401`: Authentication required
- `404`: Shop not found

---

## 5. Get Active Reviews (Paginated)
**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/reviews/{shopId}/paged`

**Access Level**: 🔒 Protected

**Query Parameters**:
| Parameter | Default | Description |
|-----------|---------|-------------|
| page | 1 | Page number (1-based) |
| size | 10 | Items per page (max: 100) |

**Response JSON Sample**:
```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Reviews retrieved successfully",
  "action_time": "2026-05-19T10:30:45",
  "data": {
    "reviews": [
      {
        "reviewId": "123e4567-e89b-12d3-a456-426614174000",
        "shopName": "Mama Lucy's Restaurant",
        "userName": "John Doe",
        "reviewText": "Amazing food!",
        "ratingValue": 5,
        "status": "ACTIVE",
        "createdAt": "2026-05-19T10:30:45"
      }
    ],
    "currentPage": 1,
    "pageSize": 10,
    "totalElements": 25,
    "totalPages": 3,
    "hasNext": true,
    "hasPrevious": false,
    "isFirst": true,
    "isLast": false
  }
}
```

**Error Responses**:
- `401`: Authentication required
- `404`: Shop not found

---

## 6. Get My Review for Shop
**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/reviews/{shopId}/my-review`

**Access Level**: 🔒 Protected

Returns the authenticated user's review for the shop, or `null` if they haven't reviewed it yet.

**Response JSON Sample (has review)**:
```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Your feedback retrieved successfully",
  "data": {
    "reviewId": "123e4567-e89b-12d3-a456-426614174000",
    "shopId": "456e7890-e89b-12d3-a456-426614174001",
    "shopName": "Mama Lucy's Restaurant",
    "userId": "789e0123-e89b-12d3-a456-426614174002",
    "userName": "John Doe",
    "reviewText": "Amazing food!",
    "ratingValue": 5,
    "status": "ACTIVE",
    "createdAt": "2026-05-19T10:30:45",
    "updatedAt": "2026-05-19T10:30:45",
    "isMyReview": false
  }
}
```

**Response when no review exists**:
```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Your feedback retrieved successfully",
  "data": null
}
```

---

## 7. Get Shop Review Summary
**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/reviews/{shopId}/summary`

**Access Level**: 🌐 Public

Returns aggregated rating and review counts for a shop.

**Response JSON Sample**:
```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Shop feedback summary retrieved successfully",
  "data": {
    "shopId": "456e7890-e89b-12d3-a456-426614174001",
    "shopName": "Mama Lucy's Restaurant",
    "averageRating": 4.5,
    "totalRatings": 25,
    "ratingDistribution": { "1": 1, "2": 2, "3": 5, "4": 7, "5": 10 },
    "totalReviews": 15,
    "activeReviews": 12,
    "hiddenReviews": 2,
    "flaggedReviews": 1
  }
}
```

---

## Quick Reference

### ReviewStatus Enum
| Value | Description |
|-------|-------------|
| `ACTIVE` | Visible in public listings |
| `HIDDEN` | Hidden from public view |
| `FLAGGED` | Flagged for admin review |
| `UNDER_REVIEW` | Under admin review |

### ReviewResponse Fields
| Field | Type | Description |
|-------|------|-------------|
| reviewId | UUID | Unique review ID |
| shopId | UUID | Shop that was reviewed |
| shopName | string | Shop name |
| userId | UUID | Reviewer's user ID |
| userName | string | Reviewer's display name |
| reviewText | string | Review text content |
| ratingValue | integer | Star rating (1–5), nullable |
| status | ReviewStatus | Current review status |
| createdAt | LocalDateTime | Creation timestamp |
| updatedAt | LocalDateTime | Last update timestamp |
| isMyReview | boolean | True if this is the current user's review |

### Error Codes
| Code | Meaning |
|------|---------|
| `400` | Business rule violation (duplicate review, owner self-review) |
| `401` | Authentication required |
| `404` | Shop or review not found |
| `422` | Field validation errors |

# Shop Subscription

**Author**: Josh S. Sakweli, Backend Lead Team  
**Last Updated**: 2026-06-04  
**Version**: v1.0

**Base URL**: `api/v1/e-commerce/shops`

**Short Description**: The Shop Subscription API allows users to subscribe and unsubscribe from shops on the Nexgate marketplace. Subscriptions drive personalised product discovery — subscribed shops receive a ranking boost in the Trending and For You feeds, and a dedicated "from your shops" signal in the relevance formula. Shop owners can view who has subscribed to their shop.

**Hints**:
- The action is called **Subscribe** — not Follow. This is intentional to distinguish from the social module's user→user **Follow** feature. Users follow people, they subscribe to shops.
- `POST /{shopId}/subscribe` is a **toggle** — calling it once subscribes, calling it again unsubscribes. No separate unsubscribe endpoint is needed.
- Subscription status (`isSubscribed`) is automatically included in `ShopResponse` for all shop detail endpoints. Authenticated users see their real status; anonymous users always receive `false`.
- Subscriptions directly feed into the Marketplace `FOR_YOU` and `TRENDING` personalisation — see `marketplace_api_doc.md` for formula details.
- Pages are **1-based** (`page=1` returns the first page).

---

## Standard Response Format

All API responses follow a consistent structure using the Globe Response Builder pattern:

### Success Response Structure
```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Operation completed successfully",
  "action_time": "2026-06-04T10:30:45",
  "data": { }
}
```

### Error Response Structure
```json
{
  "success": false,
  "httpStatus": "BAD_REQUEST",
  "message": "Error description",
  "action_time": "2026-06-04T10:30:45",
  "data": "Error description"
}
```

### Standard Response Fields
| Field | Type | Description |
|-------|------|-------------|
| `success` | boolean | `true` for success, `false` for errors |
| `httpStatus` | string | HTTP status name (OK, BAD_REQUEST, NOT_FOUND, etc.) |
| `message` | string | Human-readable operation result |
| `action_time` | string | ISO 8601 timestamp of response generation |
| `data` | object | Response payload for success, error detail for failures |

---

## HTTP Method Badge Standards

- **GET** — <span style="background-color: #28a745; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">GET</span> Green
- **POST** — <span style="background-color: #007bff; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">POST</span> Blue

---

## Subscription State Reference

```
┌─────────────────────────────────────────────────────────────────────┐
│                    SUBSCRIPTION STATE FLOW                          │
├─────────────────────────────────────────────────────────────────────┤
│                                                                     │
│   User not subscribed                                               │
│         │                                                           │
│         ▼  POST /{shopId}/subscribe                                 │
│   User subscribed  ──────────────────────────────────────────────► │
│         │               subscriberCount + 1                        │
│         │               subscribed: true                           │
│         │                                                           │
│         ▼  POST /{shopId}/subscribe  (toggle again)                 │
│   User not subscribed ◄──────────────────────────────────────────  │
│                         subscriberCount - 1                        │
│                         subscribed: false                          │
│                                                                     │
│  Side effects:                                                      │
│  - Shop's subscriberCount is updated atomically                    │
│  - Marketplace FOR_YOU feed recalculates on next request           │
│  - Marketplace TRENDING feed gives +0.25 boost to subscribed shops │
└─────────────────────────────────────────────────────────────────────┘
```

---

## How Subscriptions Affect the Marketplace

```
┌─────────────────────────────────────────────────────────────────────┐
│              SUBSCRIPTION → MARKETPLACE SIGNALS                     │
├─────────────────────────────────────────────────────────────────────┤
│                                                                     │
│  TRENDING feed:                                                     │
│    personalizedScore = trendingScore + 0.25                        │
│    (applied to every product from a subscribed shop)               │
│                                                                     │
│  FOR YOU feed:                                                      │
│    relevanceScore = categoryMatch × 0.40                           │
│                   + favShopBoost  × 0.40  ← subscription signal    │
│                   + trendingScore × 0.20                           │
│    favShopBoost = 1.0 if subscribed, 0.0 otherwise                 │
│                                                                     │
│  Result: products from subscribed shops surface near the top of    │
│  both feeds while still allowing genuinely trending products to     │
│  compete (soft priority, not guaranteed top position).              │
└─────────────────────────────────────────────────────────────────────┘
```

---

## Endpoints

---

## 1. Toggle Subscribe / Unsubscribe

**Purpose**: Subscribes the authenticated user to a shop if they are not already subscribed, or unsubscribes them if they are. Returns the new subscription state and the updated subscriber count.

**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> `api/v1/e-commerce/shops/{shopId}/subscribe`

**Access Level**: 🔒 Protected (authentication required)

**Authentication**: Bearer Token (required)

**Request Headers**:
| Header | Type | Required | Description |
|--------|------|----------|-------------|
| `Authorization` | string | Yes | `Bearer <token>` |

**Path Parameters**:
| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| `shopId` | UUID | Yes | The shop to subscribe or unsubscribe from | Must be a valid UUID of an active, non-deleted shop |

**Success Response JSON Sample** — Subscribe:
```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Subscribed to shop successfully",
  "action_time": "2026-06-04T10:30:45",
  "data": {
    "subscribed": true,
    "subscriberCount": 1284
  }
}
```

**Success Response JSON Sample** — Unsubscribe (same endpoint, called again):
```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Unsubscribed from shop successfully",
  "action_time": "2026-06-04T10:30:45",
  "data": {
    "subscribed": false,
    "subscriberCount": 1283
  }
}
```

**Success Response Fields**:
| Field | Type | Description |
|-------|------|-------------|
| `subscribed` | boolean | Current subscription state after the toggle — `true` if now subscribed, `false` if now unsubscribed |
| `subscriberCount` | long | Updated total number of subscribers for this shop |

**Error Response JSON Samples**:

*Unauthorized (401):*
```json
{
  "success": false,
  "httpStatus": "UNAUTHORIZED",
  "message": "Token has expired",
  "action_time": "2026-06-04T10:30:45",
  "data": "Token has expired"
}
```

*Shop Not Found (404):*
```json
{
  "success": false,
  "httpStatus": "NOT_FOUND",
  "message": "Shop not found",
  "action_time": "2026-06-04T10:30:45",
  "data": "Shop not found"
}
```

**Standard Error Types**:
- `401 UNAUTHORIZED`: Token missing, expired, or invalid
- `404 NOT_FOUND`: Shop does not exist or has been deleted
- `500 INTERNAL_SERVER_ERROR`: Unexpected server failure

---

## 2. My Subscriptions

**Purpose**: Returns a paginated list of all shops the authenticated user is currently subscribed to, ordered by most recently subscribed first. Each entry includes shop details and the current subscriber 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> `api/v1/e-commerce/shops/my-subscriptions`

**Access Level**: 🔒 Protected (authentication required)

**Authentication**: Bearer Token (required)

**Request Headers**:
| Header | Type | Required | Description |
|--------|------|----------|-------------|
| `Authorization` | string | Yes | `Bearer <token>` |

**Query Parameters**:
| Parameter | Type | Required | Description | Default |
|-----------|------|----------|-------------|---------|
| `page` | integer | No | Page number (1-based) | `1` |
| `size` | integer | No | Items per page | `10` |

**Success Response JSON Sample**:
```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "My subscriptions retrieved successfully",
  "action_time": "2026-06-04T10:30:45",
  "data": {
    "content": [
      {
        "subscriptionId": "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d",
        "shopId": "7cb3a812-1234-4abc-b3fc-9d84f55bce12",
        "shopName": "TechStore Tanzania",
        "shopSlug": "techstore-tanzania",
        "logoUrl": "https://cdn.nexgate.com/shops/techstore-logo.jpg",
        "bannerUrl": "https://cdn.nexgate.com/shops/techstore-banner.jpg",
        "status": "ACTIVE",
        "isVerified": true,
        "verificationBadge": "GOLD",
        "trustScore": 4.80,
        "subscriberCount": 1284,
        "subscribedAt": "2026-05-20T14:32:00"
      }
    ],
    "currentPage": 1,
    "pageSize": 10,
    "totalElements": 7,
    "totalPages": 1,
    "hasNext": false,
    "hasPrevious": false
  }
}
```

**Success Response Fields**:
| Field | Description |
|-------|-------------|
| `content[].subscriptionId` | Unique identifier of this subscription record |
| `content[].shopId` | Shop's unique identifier |
| `content[].shopName` | Shop display name |
| `content[].shopSlug` | Shop URL slug — use for navigating to the shop page |
| `content[].logoUrl` | Shop logo image URL |
| `content[].bannerUrl` | Shop banner image URL |
| `content[].status` | Shop status — `ACTIVE`, `TEMPORARILY_OFFLINE`, etc. |
| `content[].isVerified` | Whether the shop has passed platform verification |
| `content[].verificationBadge` | Verification badge level — `BRONZE`, `SILVER`, `GOLD` |
| `content[].trustScore` | Shop trust rating `0.00–5.00` |
| `content[].subscriberCount` | Total subscribers this shop currently has |
| `content[].subscribedAt` | ISO 8601 timestamp of when the user subscribed |
| `currentPage` | Current page number (1-based) |
| `totalElements` | Total shops the user is subscribed to |
| `hasNext` / `hasPrevious` | Pagination navigation flags |

**Error Response JSON Samples**:

*Unauthorized (401):*
```json
{
  "success": false,
  "httpStatus": "UNAUTHORIZED",
  "message": "Token has expired",
  "action_time": "2026-06-04T10:30:45",
  "data": "Token has expired"
}
```

**Standard Error Types**:
- `401 UNAUTHORIZED`: Token missing, expired, or invalid
- `500 INTERNAL_SERVER_ERROR`: Unexpected server failure

---

## 3. Shop Subscribers (Owner Only)

**Purpose**: Returns a paginated list of users who have subscribed to a specific shop, ordered by most recently subscribed first. Only the shop owner can access this endpoint — calling it for a shop you do not own returns a 403 error.

**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> `api/v1/e-commerce/shops/{shopId}/subscribers`

**Access Level**: 🔒 Protected (shop owner only)

**Authentication**: Bearer Token (required — must be the owner of the shop)

**Request Headers**:
| Header | Type | Required | Description |
|--------|------|----------|-------------|
| `Authorization` | string | Yes | `Bearer <token>` |

**Path Parameters**:
| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| `shopId` | UUID | Yes | The shop whose subscriber list to retrieve | Caller must own this shop |

**Query Parameters**:
| Parameter | Type | Required | Description | Default |
|-----------|------|----------|-------------|---------|
| `page` | integer | No | Page number (1-based) | `1` |
| `size` | integer | No | Items per page | `10` |

**Success Response JSON Sample**:
```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Subscribers retrieved successfully",
  "action_time": "2026-06-04T10:30:45",
  "data": {
    "content": [
      {
        "userId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
        "fullName": "Amina Hassan",
        "userName": "amina.h",
        "avatarUrl": "https://cdn.nexgate.com/avatars/amina-h.jpg",
        "subscribedAt": "2026-06-01T09:45:00"
      },
      {
        "userId": "1cb2e847-3f1a-4bcd-a3fc-9e84f22bce99",
        "fullName": "John Mwangi",
        "userName": "jmwangi",
        "avatarUrl": null,
        "subscribedAt": "2026-05-29T16:12:00"
      }
    ],
    "currentPage": 1,
    "pageSize": 10,
    "totalElements": 1284,
    "totalPages": 129,
    "hasNext": true,
    "hasPrevious": false
  }
}
```

**Success Response Fields**:
| Field | Description |
|-------|-------------|
| `content[].userId` | Subscriber's unique account identifier |
| `content[].fullName` | Subscriber's first + last name |
| `content[].userName` | Subscriber's public username |
| `content[].avatarUrl` | Subscriber's profile picture URL — `null` if no avatar set |
| `content[].subscribedAt` | ISO 8601 timestamp of when this user subscribed |
| `currentPage` | Current page number (1-based) |
| `totalElements` | Total subscriber count for this shop |
| `hasNext` / `hasPrevious` | Pagination navigation flags |

**Error Response JSON Samples**:

*Unauthorized (401):*
```json
{
  "success": false,
  "httpStatus": "UNAUTHORIZED",
  "message": "Token has expired",
  "action_time": "2026-06-04T10:30:45",
  "data": "Token has expired"
}
```

*Forbidden — Not the shop owner (403):*
```json
{
  "success": false,
  "httpStatus": "FORBIDDEN",
  "message": "You do not own this shop",
  "action_time": "2026-06-04T10:30:45",
  "data": "You do not own this shop"
}
```

*Shop Not Found (404):*
```json
{
  "success": false,
  "httpStatus": "NOT_FOUND",
  "message": "Shop not found",
  "action_time": "2026-06-04T10:30:45",
  "data": "Shop not found"
}
```

**Standard Error Types**:
- `401 UNAUTHORIZED`: Token missing, expired, or invalid
- `403 FORBIDDEN`: Authenticated user does not own this shop
- `404 NOT_FOUND`: Shop does not exist or has been deleted
- `500 INTERNAL_SERVER_ERROR`: Unexpected server failure

---

## isSubscribed & subscriberCount in ShopResponse

Subscription state is automatically embedded in the standard `ShopResponse` returned by all shop detail endpoints — no separate call needed.

```json
{
  "shopId": "7cb3a812-...",
  "shopName": "TechStore Tanzania",
  "isVerified": true,
  "trustScore": 4.80,
  "isSubscribed": true,
  "subscriberCount": 1284,
  "productCount": 47
}
```

| Field | Type | Description |
|-------|------|-------------|
| `isSubscribed` | boolean | `true` if the requesting authenticated user is subscribed to this shop. Always `false` for anonymous users |
| `subscriberCount` | long | Total number of subscribers this shop currently has |
| `productCount` | long | Number of active (published) products in this shop |

---

## Quick Reference — All Subscription Endpoints

| # | Endpoint | Method | Auth | Who can call |
|---|----------|--------|------|--------------|
| 1 | `api/v1/e-commerce/shops/{shopId}/subscribe` | <span style="background-color: #007bff; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">POST</span> | Required | Any authenticated user |
| 2 | `api/v1/e-commerce/shops/my-subscriptions` | <span style="background-color: #28a745; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">GET</span> | Required | Any authenticated user |
| 3 | `api/v1/e-commerce/shops/{shopId}/subscribers` | <span style="background-color: #28a745; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">GET</span> | Required | Shop owner only |

# 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.
```

# Checkout Session

**Author**: Josh S. Sakweli, Backend Lead Team  
**Last Updated**: 2026-05-23  
**Version**: v2.0

**Base URL**: `https://apinexgate.glueauth.com/api/v1/`

**Short Description**: The Checkout Session API manages the complete checkout process for both e-commerce and event transactions. It handles session creation (with upfront wallet balance validation), inventory holds, payment processing via Wallet, Cash, and Free flows, group purchasing, and installment payments. Each session maintains state, inventory holds, and payment attempt tracking with automatic expiration handling.

**Hints**:

- **Wallet balance is validated at session creation time** — if insufficient, no session is created and a rich balance response is returned so the frontend can guide the user to top up
- All checkout sessions expire after 15 minutes by default
- Inventory is automatically held during active sessions and released upon expiration or cancellation
- Maximum 5 payment retry attempts allowed per session
- Group purchase sessions require WALLET payment method only
- Sessions in PAYMENT\_PROCESSING status cannot be modified
- Installment payment only charges the down payment at checkout; monthly payments are handled by the scheduler
- All monetary values are in TZS (Tanzanian Shillings)
- Use ISO 8601 format for all datetime fields
- Sessions can only be updated when in PENDING\_PAYMENT or PAYMENT\_FAILED status

---

## Full Checkout Flow Diagram

```
CLIENT                          BACKEND                         SYSTEMS
  |                               |                               |
  |  POST /checkout-sessions      |                               |
  |------------------------------>|                               |
  |                               |-- Validate request            |
  |                               |-- Calculate pricing           |
  |                               |                               |
  |                               |-- assertSufficientBalance()   |
  |                               |        |                      |
  |                               |        |-- GET wallet balance -|-> Ledger
  |                               |        |                      |
  |                               |   .......................      |
  |                               |   . BALANCE INSUFFICIENT .    |
  |                               |   .......................      |
  |    422 + rich balance data    |        |                      |
  |<------------------------------|<-------'                      |
  |  {                            |                               |
  |    walletBalance: 5000,       |                               |
  |    sessionTotal: 12000,       |                               |
  |    shortfall: 7000,           |                               |
  |    recommendedTopUp: 7000     |                               |
  |  }                            |                               |
  |                               |                               |
  |                               |   .......................      |
  |                               |   .  BALANCE SUFFICIENT  .    |
  |                               |   .......................      |
  |                               |        |                      |
  |                               |-- Hold inventory ------------>|-> Inventory
  |                               |-- Save session                |
  |                               |-- Schedule expiry job ------->|-> JobRunr
  |                               |                               |
  |    201 + session data         |                               |
  |<------------------------------|                               |
  |  { sessionId, status:         |                               |
  |    PENDING_PAYMENT, ... }     |                               |
  |                               |                               |
  |  ...15 min window...          |                               |
  |                               |                               |
  |  POST /{sessionId}/payment    |                               |
  |------------------------------>|                               |
  |                               |-- Validate status             |
  |                               |-- Check expiry                |
  |                               |                               |
  |                               |   .......................      |
  |                               |   .    FREE checkout     .    |
  |                               |   .......................      |
  |                               |        |                      |
  |                               |-- Create booking/order        |
  |                               |-- Track free transaction      |
  |    200 SUCCESS                |                               |
  |<------------------------------|                               |
  |                               |                               |
  |                               |   .......................      |
  |                               |   .    CASH checkout     .    |
  |                               |   .......................      |
  |                               |        |                      |
  |                               |-- Create booking/order        |
  |                               |-- Track cash transaction      |
  |    200 SUCCESS                |                               |
  |<------------------------------|                               |
  |                               |                               |
  |                               |   .......................      |
  |                               |   .   WALLET checkout    .    |
  |                               |   .......................      |
  |                               |        |                      |
  |                               |-- Set PAYMENT_PROCESSING      |
  |                               |-- holdMoney() --------------->|-> Escrow
  |                               |                               |
  |                               |   .......................      |
  |                               |   .     SUCCESS          .    |
  |                               |   .......................      |
  |                               |        |                      |
  |                               |-- Set PAYMENT_COMPLETED       |
  |                               |-- Create order/booking        |
  |                               |-- Commit inventory hold ----->|-> Inventory
  |                               |-- Publish payment event ----->|-> Notifications
  |    200 SUCCESS                |                               |
  |<------------------------------|                               |
  |  { orderId, escrowId,         |                               |
  |    amountPaid, ... }          |                               |
  |                               |                               |
  |                               |   .......................      |
  |                               |   .     FAILED           .    |
  |                               |   .......................      |
  |                               |        |                      |
  |                               |-- recordFailedAttempt()       |
  |                               |-- Release reservation         |
  |                               |   (events only)               |
  |    200 FAILED                 |                               |
  |<------------------------------|                               |
  |  { success: false,            |                               |
  |    canRetry: true/false }     |                               |
  |                               |                               |
  |  POST /{sessionId}/retry      |                               |
  |------------------------------>|                               |
  |                               |-- Check status = FAILED       |
  |                               |-- Check attempts < 5          |
  |                               |-- Re-validate inventory       |
  |                               |-- Check wallet balance        |
  |                               |-- Re-hold inventory           |
  |                               |-- Reset to PENDING_PAYMENT    |
  |                               |-- processPayment() ...        |
  |                               |   (same wallet flow above)    |

```

---

## Session Type Routing

```
POST /checkout-sessions
  sessionType?
  .
  ├─ REGULAR_DIRECTLY ──> validate 1 item ──> balance check ──> hold inventory ──> session
  |
  ├─ REGULAR_CART ──────> validate cart ───> balance check ──> hold inventory ──> session
  |
  ├─ GROUP_PURCHASE ────> validate group ──> balance check ──> session (no hold, group-level)
  |
  └─ INSTALLMENT ───────> calculate down payment ──> balance check ──> hold inventory ──> session


POST /e-events/checkout
  ticketPricingType?
  .
  ├─ PAID ──> balance check ──> create payment intent ──> reserve ticket ──> session
  |
  └─ FREE ──> (no balance check) ──> reserve ticket ──> session

```

---

## Standard Response Format

All API responses follow a consistent structure using our Globe Response Builder pattern:

### Success Response Structure

```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Operation completed successfully",
  "action_time": "2025-10-02T10:30:45",
  "data": {}
}

```

### Error Response Structure

```json
{
  "success": false,
  "httpStatus": "BAD_REQUEST",
  "message": "Error description",
  "action_time": "2025-10-02T10:30:45",
  "data": "Error description"
}

```

### Insufficient Balance Error Structure (422)

This is a special structured error returned when wallet balance is insufficient **at session creation time**. The `data` field contains rich balance details so the frontend can guide the user to top up.

```json
{
  "success": false,
  "httpStatus": "UNPROCESSABLE_ENTITY",
  "message": "Insufficient wallet balance to complete checkout",
  "action_time": "2025-10-02T10:30:45",
  "data": {
    "walletBalance": 5000.00,
    "sessionTotal": 12000.00,
    "shortfall": 7000.00,
    "hasSufficientBalance": false,
    "recommendedTopUp": 7000.00,
    "pspMinimum": 500.00,
    "currency": "TZS"
  }
}

```

<table id="bkmrk-field-description-wa"><thead><tr><th>Field</th><th>Description</th></tr></thead><tbody><tr><td>`walletBalance`</td><td>Current wallet balance of the user</td></tr><tr><td>`sessionTotal`</td><td>Total amount required for this checkout</td></tr><tr><td>`shortfall`</td><td>How much is missing (`sessionTotal - walletBalance`)</td></tr><tr><td>`hasSufficientBalance`</td><td>Always `false` when this error is returned</td></tr><tr><td>`recommendedTopUp`</td><td>Suggested top-up amount (at least `shortfall`, rounded up to PSP minimum)</td></tr><tr><td>`pspMinimum`</td><td>Minimum top-up amount accepted by the payment provider</td></tr><tr><td>`currency`</td><td>Always `TZS`</td></tr></tbody></table>

### Standard Response Fields

<table id="bkmrk-field-type-descripti"><thead><tr><th>Field</th><th>Type</th><th>Description</th></tr></thead><tbody><tr><td>`success`</td><td>boolean</td><td>Always `true` for successful operations, `false` for errors</td></tr><tr><td>`httpStatus`</td><td>string</td><td>HTTP status name (OK, CREATED, BAD\_REQUEST, NOT\_FOUND, etc.)</td></tr><tr><td>`message`</td><td>string</td><td>Human-readable message describing the operation result</td></tr><tr><td>`action_time`</td><td>string</td><td>ISO 8601 timestamp of when the response was generated</td></tr><tr><td>`data`</td><td>object/string</td><td>Response payload for success, error details for failures</td></tr></tbody></table>

---

## HTTP Method Badge Standards

For better visual clarity, all endpoints use colored badges for HTTP methods with the following standard colors:

- **GET** - <span style="background-color: #28a745; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">GET</span> - Green (Safe, read-only operations)
- **POST** - <span style="background-color: #007bff; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">POST</span> - Blue (Create new resources)
- **PATCH** - <span style="background-color: #fd7e14; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">PATCH</span> - Orange (Partial updates)
- **DELETE** - <span style="background-color: #dc3545; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">DELETE</span> - Red (Remove resources)

---

## Endpoints

## 1. Create Checkout Session

**Purpose**: Creates a new checkout session for processing a purchase. Wallet balance is validated upfront — if insufficient, no session is created and the caller receives rich balance data to guide the user. Supports multiple checkout types: direct product purchase, cart checkout, group purchasing, and installment payments.

**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}/checkout-sessions`

**Access Level**: 🔒 Protected (Requires Authentication)

**Authentication**: Bearer Token required in Authorization header

**Request Headers**:

<table id="bkmrk-header-type-required"><thead><tr><th>Header</th><th>Type</th><th>Required</th><th>Description</th></tr></thead><tbody><tr><td>Authorization</td><td>string</td><td>Yes</td><td>Bearer token for authenticated user</td></tr><tr><td>Content-Type</td><td>string</td><td>Yes</td><td>Must be `application/json`</td></tr></tbody></table>

**Request JSON Sample**:

```json
{
  "sessionType": "REGULAR_DIRECTLY",
  "items": [
    {
      "productId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
      "quantity": 2
    }
  ],
  "shippingAddressId": "f1e2d3c4-b5a6-7890-cdef-123456789abc",
  "shippingMethodId": "standard-shipping",
  "metadata": {
    "couponCode": "SAVE20",
    "referralCode": "REF123",
    "notes": "Please handle with care"
  },
  "installmentPlanId": null,
  "groupInstanceId": null,
  "downPaymentPercent": 20
}

```

**Request Body Parameters**:

<table id="bkmrk-parameter-type-requi"><thead><tr><th>Parameter</th><th>Type</th><th>Required</th><th>Description</th><th>Validation</th></tr></thead><tbody><tr><td>sessionType</td><td>string</td><td>Yes</td><td>Type of checkout session</td><td>enum: REGULAR\_DIRECTLY, REGULAR\_CART, GROUP\_PURCHASE, INSTALLMENT</td></tr><tr><td>items</td><td>array</td><td>Conditional</td><td>Array of items to checkout. Required for REGULAR\_DIRECTLY and GROUP\_PURCHASE</td><td>For REGULAR\_DIRECTLY: exactly 1 item. For GROUP\_PURCHASE: exactly 1 item. Not used for REGULAR\_CART</td></tr><tr><td>items\[\].productId</td><td>string (UUID)</td><td>Yes (if items provided)</td><td>Product identifier</td><td>Valid UUID format</td></tr><tr><td>items\[\].quantity</td><td>integer</td><td>Yes (if items provided)</td><td>Quantity to purchase</td><td>Min: 1, must not exceed product constraints</td></tr><tr><td>shippingAddressId</td><td>string (UUID)</td><td>Yes</td><td>User's shipping address identifier</td><td>Valid UUID format, must belong to authenticated user</td></tr><tr><td>shippingMethodId</td><td>string</td><td>Yes</td><td>Selected shipping method identifier</td><td>Must be valid shipping method</td></tr><tr><td>metadata</td><td>object</td><td>No</td><td>Additional metadata for the session</td><td>Key-value pairs for coupons, referrals, notes, etc.</td></tr><tr><td>installmentPlanId</td><td>string (UUID)</td><td>Conditional</td><td>Installment plan identifier (INSTALLMENT type only)</td><td>Valid UUID format</td></tr><tr><td>downPaymentPercent</td><td>integer</td><td>Conditional</td><td>Down payment percentage (INSTALLMENT type only)</td><td>Must satisfy plan's min/max constraints</td></tr><tr><td>groupInstanceId</td><td>string (UUID)</td><td>No</td><td>Group instance to join (GROUP\_PURCHASE type only)</td><td>Valid UUID format, group must be joinable. Null = create new group</td></tr><tr><td>groupName</td><td>string</td><td>Conditional</td><td>Name for new group (GROUP\_PURCHASE, groupInstanceId null)</td><td>Must be unique per product</td></tr></tbody></table>

**Success Response JSON Sample**:

```json
{
  "success": true,
  "httpStatus": "CREATED",
  "message": "Checkout session created successfully",
  "action_time": "2025-10-02T14:30:45",
  "data": {
    "sessionId": "c1d2e3f4-a5b6-7890-cdef-123456789abc",
    "sessionType": "REGULAR_DIRECTLY",
    "status": "PENDING_PAYMENT",
    "customerId": "u1s2e3r4-i5d6-7890-abcd-ef1234567890",
    "customerUserName": "john_doe",
    "items": [
      {
        "productId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
        "productName": "Premium Wireless Headphones",
        "productSlug": "premium-wireless-headphones",
        "productImage": "https://cdn.nextgate.com/products/headphones-001.jpg",
        "quantity": 2,
        "unitPrice": 150000.00,
        "discountAmount": 20000.00,
        "subtotal": 300000.00,
        "tax": 0.00,
        "total": 280000.00,
        "shopId": "s1h2o3p4-i5d6-7890-abcd-ef1234567890",
        "shopName": "TechWorld Electronics",
        "shopLogo": "https://cdn.nextgate.com/shops/techworld-logo.jpg",
        "availableForCheckout": true,
        "availableQuantity": 50
      }
    ],
    "pricing": {
      "subtotal": 300000.00,
      "discount": 20000.00,
      "shippingCost": 5000.00,
      "tax": 0.00,
      "total": 285000.00,
      "currency": "TZS"
    },
    "shippingAddress": {
      "fullName": "John Doe",
      "addressLine1": "123 Main Street",
      "addressLine2": "Apartment 4B",
      "city": "Dar es Salaam",
      "state": "Dar es Salaam Region",
      "postalCode": "12345",
      "country": "Tanzania",
      "phone": "+255123456789"
    },
    "shippingMethod": {
      "id": "standard-shipping",
      "name": "Standard Shipping",
      "carrier": "DHL",
      "cost": 5000.00,
      "estimatedDays": "3-5 business days",
      "estimatedDelivery": "2025-10-07T14:30:45"
    },
    "paymentIntent": {
      "provider": "WALLET",
      "clientSecret": null,
      "paymentMethods": ["WALLET"],
      "status": "READY"
    },
    "paymentAttempts": [],
    "inventoryHeld": true,
    "inventoryHoldExpiresAt": "2025-10-02T14:45:45",
    "metadata": {
      "couponCode": "SAVE20",
      "referralCode": "REF123",
      "notes": "Please handle with care"
    },
    "expiresAt": "2025-10-02T14:45:45",
    "createdAt": "2025-10-02T14:30:45",
    "updatedAt": "2025-10-02T14:30:45",
    "completedAt": null,
    "createdOrderId": null,
    "cartId": null
  }
}

```

**Success Response Fields**:

<table id="bkmrk-field-description-se"><thead><tr><th>Field</th><th>Description</th></tr></thead><tbody><tr><td>sessionId</td><td>Unique identifier for the checkout session</td></tr><tr><td>sessionType</td><td>Type of checkout (REGULAR\_DIRECTLY, REGULAR\_CART, GROUP\_PURCHASE, INSTALLMENT)</td></tr><tr><td>status</td><td>Current status of the session (always PENDING\_PAYMENT on creation)</td></tr><tr><td>customerId</td><td>User ID who created the session</td></tr><tr><td>customerUserName</td><td>Username of the customer</td></tr><tr><td>items</td><td>Array of checkout items with product details, pricing, and availability</td></tr><tr><td>pricing</td><td>Summary of all pricing calculations</td></tr><tr><td>pricing.total</td><td>Final amount to be charged (what was validated against wallet balance)</td></tr><tr><td>pricing.currency</td><td>Always TZS</td></tr><tr><td>shippingAddress</td><td>Complete shipping address details</td></tr><tr><td>shippingMethod</td><td>Selected shipping method details</td></tr><tr><td>paymentIntent</td><td>Payment processing details</td></tr><tr><td>paymentAttempts</td><td>Empty array on creation</td></tr><tr><td>inventoryHeld</td><td>Whether inventory is currently held (false for GROUP\_PURCHASE)</td></tr><tr><td>inventoryHoldExpiresAt</td><td>When the inventory hold will be released</td></tr><tr><td>expiresAt</td><td>When this checkout session expires (15 minutes from creation)</td></tr><tr><td>createdOrderId</td><td>null until payment succeeds</td></tr><tr><td>cartId</td><td>Cart ID if session was created from cart, null otherwise</td></tr></tbody></table>

**Error Responses**:

*Insufficient Wallet Balance (422) — includes rich top-up data:*

```json
{
  "success": false,
  "httpStatus": "UNPROCESSABLE_ENTITY",
  "message": "Insufficient wallet balance to complete checkout",
  "action_time": "2025-10-02T14:30:45",
  "data": {
    "walletBalance": 150000.00,
    "sessionTotal": 285000.00,
    "shortfall": 135000.00,
    "hasSufficientBalance": false,
    "recommendedTopUp": 135000.00,
    "pspMinimum": 500.00,
    "currency": "TZS"
  }
}

```

*Bad Request - Invalid Session Type (400):*

```json
{
  "success": false,
  "httpStatus": "BAD_REQUEST",
  "message": "REGULAR_DIRECTLY checkout supports only 1 item. Use REGULAR_CART for multiple items.",
  "action_time": "2025-10-02T14:30:45",
  "data": "REGULAR_DIRECTLY checkout supports only 1 item. Use REGULAR_CART for multiple items."
}

```

*Bad Request - Insufficient Inventory (400):*

```json
{
  "success": false,
  "httpStatus": "BAD_REQUEST",
  "message": "Insufficient stock. Available: 3, Requested: 5",
  "action_time": "2025-10-02T14:30:45",
  "data": "Insufficient stock. Available: 3, Requested: 5"
}

```

*Validation Error (422):*

```json
{
  "success": false,
  "httpStatus": "UNPROCESSABLE_ENTITY",
  "message": "Validation failed",
  "action_time": "2025-10-02T14:30:45",
  "data": {
    "sessionType": "must not be null",
    "items[0].quantity": "must be greater than or equal to 1",
    "shippingAddressId": "must not be null"
  }
}

```

*Not Found - Product Not Found (404):*

```json
{
  "success": false,
  "httpStatus": "NOT_FOUND",
  "message": "Product not found",
  "action_time": "2025-10-02T14:30:45",
  "data": "Product not found"
}

```

*Unauthorized (401):*

```json
{
  "success": false,
  "httpStatus": "UNAUTHORIZED",
  "message": "Authentication token is required",
  "action_time": "2025-10-02T14:30:45",
  "data": "Authentication token is required"
}

```

---

## 2. Get Checkout Session by ID

**Purpose**: Retrieves detailed information about a specific checkout session by its ID. Only the session owner can access their session.

**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}/checkout-sessions/{sessionId}`

**Access Level**: 🔒 Protected (Requires Authentication and Ownership)

**Authentication**: Bearer Token required in Authorization header

**Path Parameters**:

<table id="bkmrk-parameter-type-requi-1"><thead><tr><th>Parameter</th><th>Type</th><th>Required</th><th>Description</th><th>Validation</th></tr></thead><tbody><tr><td>sessionId</td><td>string (UUID)</td><td>Yes</td><td>Unique identifier of the checkout session</td><td>Valid UUID format</td></tr></tbody></table>

**Success Response JSON Sample**:

```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Checkout session retrieved successfully",
  "action_time": "2025-10-02T14:35:45",
  "data": {
    "sessionId": "c1d2e3f4-a5b6-7890-cdef-123456789abc",
    "sessionType": "REGULAR_DIRECTLY",
    "status": "PENDING_PAYMENT",
    "customerId": "u1s2e3r4-i5d6-7890-abcd-ef1234567890",
    "customerUserName": "john_doe",
    "items": [],
    "pricing": {},
    "shippingAddress": {},
    "shippingMethod": {},
    "paymentIntent": {},
    "paymentAttempts": [],
    "inventoryHeld": true,
    "inventoryHoldExpiresAt": "2025-10-02T14:45:45",
    "metadata": { "couponCode": "SAVE20" },
    "expiresAt": "2025-10-02T14:45:45",
    "createdAt": "2025-10-02T14:30:45",
    "updatedAt": "2025-10-02T14:30:45",
    "completedAt": null,
    "createdOrderId": null,
    "cartId": null
  }
}

```

**Error Responses**:

*Not Found - Session Not Found or No Permission (404):*

```json
{
  "success": false,
  "httpStatus": "NOT_FOUND",
  "message": "Checkout session not found or you don't have permission to access it",
  "action_time": "2025-10-02T14:35:45",
  "data": "Checkout session not found or you don't have permission to access it"
}

```

---

## 3. Get My Checkout Sessions

**Purpose**: Retrieves all checkout sessions belonging to the authenticated user, ordered by creation date (newest first).

**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}/checkout-sessions`

**Access Level**: 🔒 Protected (Requires Authentication)

**Success Response Fields**:

<table id="bkmrk-field-description-se-1"><thead><tr><th>Field</th><th>Description</th></tr></thead><tbody><tr><td>sessionId</td><td>Unique identifier for the checkout session</td></tr><tr><td>sessionType</td><td>Type of checkout session</td></tr><tr><td>status</td><td>Current status of the session</td></tr><tr><td>itemCount</td><td>Number of items in the checkout</td></tr><tr><td>totalAmount</td><td>Total amount to be paid in TZS</td></tr><tr><td>currency</td><td>Currency code (TZS)</td></tr><tr><td>expiresAt</td><td>When this session expires</td></tr><tr><td>createdAt</td><td>When this session was created</td></tr><tr><td>isExpired</td><td>Whether the session has expired</td></tr><tr><td>canRetryPayment</td><td>true only if status is PAYMENT\_FAILED and not expired and attempts &lt; 5</td></tr><tr><td>itemPreviews</td><td>Array of preview information for items</td></tr></tbody></table>

---

## 4. Get My Active Checkout Sessions

**Purpose**: Retrieves only active checkout sessions (PENDING\_PAYMENT or PAYMENT\_FAILED status) that haven't expired yet, ordered by creation date (newest first).

**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}/checkout-sessions/active`

**Access Level**: 🔒 Protected (Requires Authentication)

**Notes**: Response structure is the same as Get My Checkout Sessions but filtered to active sessions only. `isExpired` is always `false` in this response.

---

## 5. Update Checkout Session

**Purpose**: Updates an existing checkout session. Can modify shipping address, shipping method, or metadata. Only sessions in PENDING\_PAYMENT or PAYMENT\_FAILED status can be updated.

**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}/checkout-sessions/{sessionId}`

**Access Level**: 🔒 Protected (Requires Authentication and Ownership)

**Path Parameters**:

<table id="bkmrk-parameter-type-requi-2"><thead><tr><th>Parameter</th><th>Type</th><th>Required</th><th>Description</th></tr></thead><tbody><tr><td>sessionId</td><td>string (UUID)</td><td>Yes</td><td>Unique identifier of the checkout session</td></tr></tbody></table>

**Request Body Parameters**:

<table id="bkmrk-parameter-type-requi-3"><thead><tr><th>Parameter</th><th>Type</th><th>Required</th><th>Description</th></tr></thead><tbody><tr><td>shippingAddressId</td><td>string (UUID)</td><td>No</td><td>New shipping address identifier</td></tr><tr><td>shippingMethodId</td><td>string</td><td>No</td><td>New shipping method (triggers pricing recalculation)</td></tr><tr><td>metadata</td><td>object</td><td>No</td><td>Key-value pairs, merged with existing metadata</td></tr></tbody></table>

**Error Responses**:

*Cannot Update Completed Session (400):*

```json
{
  "success": false,
  "httpStatus": "BAD_REQUEST",
  "message": "Cannot update a completed checkout session",
  "action_time": "2025-10-02T14:50:45",
  "data": "Cannot update a completed checkout session"
}

```

---

## 6. Cancel Checkout Session

**Purpose**: Cancels an existing checkout session and releases held inventory. Cannot cancel sessions that are completed or have successful payment.

**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}/checkout-sessions/{sessionId}/cancel`

**Access Level**: 🔒 Protected (Requires Authentication and Ownership)

**Success Response**:

```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Checkout session cancelled successfully",
  "action_time": "2025-10-02T14:55:45",
  "data": null
}

```

**Error Responses**:

*Cannot Cancel Completed (400):*

```json
{
  "success": false,
  "httpStatus": "BAD_REQUEST",
  "message": "Cannot cancel - payment has been completed. Please contact support.",
  "action_time": "2025-10-02T14:55:45",
  "data": "Cannot cancel - payment has been completed. Please contact support."
}

```

*Already Cancelled (400):*

```json
{
  "success": false,
  "httpStatus": "BAD_REQUEST",
  "message": "Checkout session is already cancelled",
  "action_time": "2025-10-02T14:55:45",
  "data": "Checkout session is already cancelled"
}

```

---

## 7. Process Payment

**Purpose**: Initiates payment processing for a checkout session in PENDING\_PAYMENT status. Routes to the appropriate payment processor (WALLET, CASH, FREE) based on the session amount and payment method.

**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}/checkout-sessions/{sessionId}/process-payment`

**Access Level**: 🔒 Protected (Requires Authentication and Ownership)

**Path Parameters**:

<table id="bkmrk-parameter-type-requi-4"><thead><tr><th>Parameter</th><th>Type</th><th>Required</th><th>Description</th></tr></thead><tbody><tr><td>sessionId</td><td>string (UUID)</td><td>Yes</td><td>Unique identifier of the checkout session</td></tr></tbody></table>

**Success Response JSON Sample**:

```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Payment completed successfully. Your order is being processed.",
  "action_time": "2025-10-02T15:00:45",
  "data": {
    "success": true,
    "status": "SUCCESS",
    "message": "Payment completed successfully. Your order is being processed.",
    "checkoutSessionId": "c1d2e3f4-a5b6-7890-cdef-123456789abc",
    "escrowId": "esc-uuid",
    "escrowNumber": "ESC-20251002-001",
    "orderId": "ord-uuid",
    "paymentMethod": "WALLET",
    "amountPaid": 285000.00,
    "platformFee": 5700.00,
    "sellerAmount": 279300.00,
    "currency": "TZS"
  }
}

```

**Success Response Fields**:

<table id="bkmrk-field-description-su"><thead><tr><th>Field</th><th>Description</th></tr></thead><tbody><tr><td>success</td><td>Whether the payment was successful</td></tr><tr><td>status</td><td>SUCCESS, PENDING, or FAILED</td></tr><tr><td>checkoutSessionId</td><td>The session that was paid</td></tr><tr><td>escrowId</td><td>Escrow account holding the funds</td></tr><tr><td>escrowNumber</td><td>Human-readable escrow reference</td></tr><tr><td>orderId</td><td>Order or booking ID created after payment</td></tr><tr><td>paymentMethod</td><td>WALLET, CASH, or FREE</td></tr><tr><td>amountPaid</td><td>Total amount charged in TZS</td></tr><tr><td>platformFee</td><td>Platform fee deducted</td></tr><tr><td>sellerAmount</td><td>Amount the seller receives</td></tr><tr><td>currency</td><td>Always TZS</td></tr></tbody></table>

**Error Responses**:

*Session Not in PENDING\_PAYMENT status (400):*

```json
{
  "success": false,
  "httpStatus": "BAD_REQUEST",
  "message": "Cannot process payment - session is not pending: PAYMENT_COMPLETED",
  "action_time": "2025-10-02T15:00:45",
  "data": "Cannot process payment - session is not pending: PAYMENT_COMPLETED"
}

```

*Session Expired (400):*

```json
{
  "success": false,
  "httpStatus": "BAD_REQUEST",
  "message": "Checkout session has expired",
  "action_time": "2025-10-02T15:00:45",
  "data": "Checkout session has expired"
}

```

---

## 8. Retry Payment

**Purpose**: Retries payment for a session in PAYMENT\_FAILED status. Re-validates inventory availability, checks wallet balance, re-holds inventory, and extends session expiration before retrying. Maximum 5 total attempts.

**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}/checkout-sessions/{sessionId}/retry-payment`

**Access Level**: 🔒 Protected (Requires Authentication and Ownership)

**Error Responses**:

*Invalid Status (400):*

```json
{
  "success": false,
  "httpStatus": "BAD_REQUEST",
  "message": "Cannot retry payment - session status: PENDING_PAYMENT. Expected: PAYMENT_FAILED",
  "action_time": "2025-10-02T15:10:45",
  "data": "Cannot retry payment - session status: PENDING_PAYMENT. Expected: PAYMENT_FAILED"
}

```

*Max Attempts Exceeded (400):*

```json
{
  "success": false,
  "httpStatus": "BAD_REQUEST",
  "message": "Maximum payment attempts (5) exceeded. Please create a new checkout session.",
  "action_time": "2025-10-02T15:10:45",
  "data": "Maximum payment attempts (5) exceeded. Please create a new checkout session."
}

```

*Insufficient Wallet Balance on Retry (400):*

```json
{
  "success": false,
  "httpStatus": "BAD_REQUEST",
  "message": "Insufficient wallet balance. Required: 285000 TZS, Available: 150000 TZS. Please top up your wallet.",
  "action_time": "2025-10-02T15:10:45",
  "data": "Insufficient wallet balance. Required: 285000 TZS, Available: 150000 TZS. Please top up your wallet."
}

```

*Product No Longer Available (400):*

```json
{
  "success": false,
  "httpStatus": "BAD_REQUEST",
  "message": "Product 'Premium Wireless Headphones' is no longer available in requested quantity. Please create a new checkout session.",
  "action_time": "2025-10-02T15:10:45",
  "data": "Product 'Premium Wireless Headphones' is no longer available in requested quantity. Please create a new checkout session."
}

```

---

## Checkout Session Types

### REGULAR\_DIRECTLY

Direct product purchase without using a cart. Must include exactly 1 item.

- Best for "Buy Now" buttons
- Balance is checked against `pricing.total` (includes shipping)
- Inventory is held immediately on session creation

### REGULAR\_CART

Checkout from existing shopping cart. Items are fetched automatically from the user's active cart.

- Multi-item purchase flow
- Items array not required in request
- Balance is checked against `pricing.total` (includes shipping)
- Inventory is held for all cart items

### GROUP\_PURCHASE

Group buying checkout where multiple users purchase the same product at a discounted group price.

- Single item at `product.groupPrice`
- Balance is checked against `groupPrice × quantity`
- WALLET payment method only
- Can join existing group (provide `groupInstanceId`) or create new group (provide `groupName`)
- No session-level inventory hold — inventory is held at the group level after payment

**Create New Group:**

```json
{
  "sessionType": "GROUP_PURCHASE",
  "items": [{ "productId": "prod-uuid", "quantity": 2 }],
  "shippingAddressId": "addr-uuid",
  "shippingMethodId": "standard",
  "groupName": "My Winning Group"
}

```

**Join Existing Group:**

```json
{
  "sessionType": "GROUP_PURCHASE",
  "items": [{ "productId": "prod-uuid", "quantity": 3 }],
  "shippingAddressId": "addr-uuid",
  "shippingMethodId": "standard",
  "groupInstanceId": "group-uuid-to-join"
}

```

### INSTALLMENT

Split payment over multiple months. Only the down payment is charged at checkout; remaining monthly payments are processed automatically by the scheduler.

- Balance is checked against `downPaymentAmount` only (not the full product price)
- `installmentPlanId` and `downPaymentPercent` are required
- `pricing.total` in the session response reflects the down payment only
- Inventory is held immediately on session creation

```json
{
  "sessionType": "INSTALLMENT",
  "items": [{ "productId": "prod-uuid", "quantity": 1 }],
  "shippingAddressId": "addr-uuid",
  "shippingMethodId": "standard",
  "installmentPlanId": "plan-uuid",
  "downPaymentPercent": 20
}

```

---

## Checkout Session Status Flow

### Status Definitions

<table id="bkmrk-status-description-c"><thead><tr><th>Status</th><th>Description</th><th>Can Update?</th><th>Can Cancel?</th><th>Can Pay?</th><th>Can Retry?</th></tr></thead><tbody><tr><td>PENDING\_PAYMENT</td><td>Session created, awaiting payment</td><td>Yes</td><td>Yes</td><td>Yes</td><td>No</td></tr><tr><td>PAYMENT\_PROCESSING</td><td>Payment in progress</td><td>No</td><td>No</td><td>No</td><td>No</td></tr><tr><td>PAYMENT\_FAILED</td><td>Payment failed, can retry</td><td>Yes</td><td>Yes</td><td>No</td><td>Yes</td></tr><tr><td>PAYMENT\_COMPLETED</td><td>Payment successful</td><td>No</td><td>No</td><td>No</td><td>No</td></tr><tr><td>EXPIRED</td><td>Session expired</td><td>No</td><td>No</td><td>No</td><td>No</td></tr><tr><td>CANCELLED</td><td>User cancelled</td><td>No</td><td>No</td><td>No</td><td>No</td></tr><tr><td>COMPLETED</td><td>Free/cash order completed</td><td>No</td><td>No</td><td>No</td><td>No</td></tr></tbody></table>

### Status Transition Flow

```
[Session Creation]
        |
        | balance check passes
        v
PENDING_PAYMENT
    |
    |-- user cancels ──────────────────> CANCELLED
    |                                    (inventory released)
    |
    |-- 15 min timeout ────────────────> EXPIRED
    |                                    (inventory released)
    |
    |-- processPayment()
        |
        |-- amount = 0 ────────────────> COMPLETED (free)
        |
        |-- CASH ──────────────────────> COMPLETED (cash)
        |
        |-- WALLET ────────────────────> PAYMENT_PROCESSING
                                                |
                                                |-- success ──> PAYMENT_COMPLETED
                                                |                (order/booking created,
                                                |                 inventory committed)
                                                |
                                                |-- failure ──> PAYMENT_FAILED
                                                                (inventory released for events,
                                                                 max 5 retries)
                                                                |
                                                                |-- retryPayment() ──> PENDING_PAYMENT
                                                                |                     (inventory re-held)
                                                                |
                                                                |-- 5 attempts ──────> EXPIRED

```

---

## Wallet Balance Check Endpoint

For cases where the frontend wants to proactively check balance against an existing session (e.g., before showing the "Pay Now" button), use:

**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}/wallet/checkout-balance-check?sessionId={id}&domain={PRODUCT|EVENT}`

**Success Response**:

```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Checkout balance check completed",
  "data": {
    "walletBalance": 150000.00,
    "sessionTotal": 285000.00,
    "shortfall": 135000.00,
    "hasSufficientBalance": false,
    "recommendedTopUp": 135000.00,
    "pspMinimum": 500.00,
    "currency": "TZS"
  }
}

```

**Note:** This endpoint always returns 200 — it never throws. Use `hasSufficientBalance` to determine if the user can pay. This is the "soft check" for an existing session; the "hard check" (which blocks session creation) happens automatically inside `POST /checkout-sessions`.

---

## Payment Methods Supported

### WALLET (Default)

Internal wallet system. Default if no payment method is specified.

- Balance validated at **session creation** (hard block) and again at **payment time** (safety net)
- Instant processing via escrow
- Funds held in escrow until order is confirmed/delivered

### CASH

Pay in cash on delivery or at the point of event check-in.

- No pre-payment required
- Order/booking created immediately
- Applicable to both product and event checkouts

### FREE

Zero-amount checkout (free products or free event tickets).

- Handled automatically when `pricing.total = 0`
- No payment method required
- Order/booking created immediately

### CREDIT\_CARD / MOBILE\_MONEY

**Status:** Planned — not yet implemented.

---

## Inventory Management

### Hold Mechanism

<table id="bkmrk-session-type-hold-cr"><thead><tr><th>Session Type</th><th>Hold Created At</th><th>Hold Released At</th></tr></thead><tbody><tr><td>REGULAR\_DIRECTLY</td><td>Session creation</td><td>Expiry, cancellation, or payment success (committed)</td></tr><tr><td>REGULAR\_CART</td><td>Session creation</td><td>Expiry, cancellation, or payment success (committed)</td></tr><tr><td>GROUP\_PURCHASE</td><td>After payment, at group level</td><td>Group expiry or group failure</td></tr><tr><td>INSTALLMENT</td><td>Session creation</td><td>Expiry, cancellation, or payment success (committed)</td></tr></tbody></table>

On successful payment, holds are **committed** (stock permanently deducted). On failure/expiry/cancellation, holds are **released** (stock returned to available).

---

## Session Expiration

**Default:** 15 minutes from creation.

**Extended when:** Payment retry is initiated (adds 15 minutes).

**On expiry:**

1. Status → EXPIRED
2. Held inventory released
3. Session cannot be updated, paid, or cancelled
4. User must create a new checkout session

---

## Payment Attempts Tracking

Maximum 5 attempts per session. Each attempt records:

- Attempt number (1–5)
- Payment method used
- Status: SUCCESS, FAILED, or RETRY\_INITIATED
- Error message (if failed)
- Timestamp and transaction ID

After 5 failed attempts, session status moves to EXPIRED and inventory is released.

---

## Error Handling Best Practices

### Frontend Checklist

**Before calling POST /checkout-sessions:**

- Ensure the user has a shipping address saved
- No need to pre-check balance — the API returns rich balance data if insufficient

**On 422 Insufficient Balance response:**

- Read `data.shortfall` to show how much the user is short
- Read `data.recommendedTopUp` to pre-fill a top-up amount
- Navigate the user to the wallet top-up screen
- Once topped up, retry `POST /checkout-sessions` — do not store the failed session

**During active session (PENDING\_PAYMENT):**

- Show a countdown timer using `expiresAt`
- On expiry, prompt user to create a new session

**On payment failure:**

- Show `canRetryPayment` to decide whether to show a retry button
- Show remaining attempts (`5 - paymentAttemptCount`)
- On retry, call `POST /{sessionId}/retry-payment` — no need to create a new session

---

## Integration Examples

### Example 1: Direct Product Purchase

```
1. POST /checkout-sessions
   → 422 if balance insufficient (show top-up screen with data.recommendedTopUp)
   → 201 with sessionId if balance OK

2. POST /checkout-sessions/{sessionId}/process-payment
   → 200 with orderId on success

```

### Example 2: Cart Checkout

```
1. POST /checkout-sessions  { sessionType: REGULAR_CART, ... }
   → 422 or 201

2. (optional) PATCH /checkout-sessions/{sessionId}  { shippingMethodId: "express" }

3. POST /checkout-sessions/{sessionId}/process-payment

```

### Example 3: Group Purchase

```
1. POST /checkout-sessions  { sessionType: GROUP_PURCHASE, groupName: "...", ... }
   → 422 if balance < groupPrice × qty
   → 201 with sessionId

2. POST /checkout-sessions/{sessionId}/process-payment
   (WALLET only)

```

### Example 4: Installment

```
1. POST /checkout-sessions  { sessionType: INSTALLMENT, installmentPlanId: "...", downPaymentPercent: 20 }
   → 422 if balance < downPaymentAmount
   → 201 with sessionId (pricing.total = down payment only)

2. POST /checkout-sessions/{sessionId}/process-payment
   → charges down payment only; monthly payments handled by scheduler

```

### Example 5: Payment Retry

```
1. GET /checkout-sessions/active
   → find session with status: PAYMENT_FAILED, canRetryPayment: true

2. POST /checkout-sessions/{sessionId}/retry-payment
   → re-validates inventory + balance
   → re-holds inventory
   → processes payment

```

---

## Rate Limiting

<table id="bkmrk-endpoint-limit-creat"><thead><tr><th>Endpoint</th><th>Limit</th></tr></thead><tbody><tr><td>Create Checkout</td><td>20 req/min per user</td></tr><tr><td>Get Sessions</td><td>60 req/min per user</td></tr><tr><td>Process / Retry Payment</td><td>10 req/min per user</td></tr><tr><td>Update / Cancel</td><td>30 req/min per user</td></tr></tbody></table>

---

# Group Purchase

**Author**: Josh S. Sakweli, Backend Lead Team  
**Last Updated**: 2025-10-02  
**Version**: v1.0

**Base URL**: `https://apinexgate.glueauth.com/api/v1/`

**Short Description**: The Group Purchase API enables collaborative buying where multiple users join together to purchase products at discounted group prices. Users can create new groups, join existing groups, transfer between groups, and track their participations. The system automatically handles seat management, expiration, and order creation when groups are completed.

**Hints**:

- Groups automatically expire based on product's `groupTimeLimitHours` setting
- Groups complete automatically when all seats are filled
- Users can join the same group multiple times to buy more seats (Hybrid Approach)
- Transfer between groups only allowed for same product, shop, and price
- Empty groups are automatically soft-deleted after all participants transfer out
- Group codes are auto-generated with format: GP-XXXXXX (6 random characters)
- Only WALLET payment method supported for group purchases
- Purchase and transfer history tracked for each participant
- Seats are released when users transfer out of groups

---

## Standard Response Format

All API responses follow a consistent structure using our Globe Response Builder pattern:

### Success Response Structure

```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Operation completed successfully",
  "action_time": "2025-10-02T10:30:45",
  "data": {
    // Actual response data goes here
  }
}

```

### Error Response Structure

```json
{
  "success": false,
  "httpStatus": "BAD_REQUEST",
  "message": "Error description",
  "action_time": "2025-10-02T10:30:45",
  "data": "Error description"
}

```

### Standard Response Fields

<table id="bkmrk-field-type-descripti"><thead><tr><th>Field</th><th>Type</th><th>Description</th></tr></thead><tbody><tr><td>`success`</td><td>boolean</td><td>Always `true` for successful operations, `false` for errors</td></tr><tr><td>`httpStatus`</td><td>string</td><td>HTTP status name (OK, BAD\_REQUEST, NOT\_FOUND, etc.)</td></tr><tr><td>`message`</td><td>string</td><td>Human-readable message describing the operation result</td></tr><tr><td>`action_time`</td><td>string</td><td>ISO 8601 timestamp of when the response was generated</td></tr><tr><td>`data`</td><td>object/string</td><td>Response payload for success, error details for failures</td></tr></tbody></table>

---

## HTTP Method Badge Standards

- **GET** - <span style="background-color: #28a745; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">GET</span> - Green (Safe, read-only operations)
- **POST** - <span style="background-color: #007bff; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">POST</span> - Blue (Create new resources)

---

## Endpoints

## 1. Get Available Groups for Product

**Purpose**: Retrieves all available (open, not expired, not full) group purchase instances for a specific product.

**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}/group-purchases/product/{productId}/available`

**Access Level**: 🌐 Public (No Authentication Required)

**Authentication**: None

**Request Headers**:

<table id="bkmrk-header-type-required"><thead><tr><th>Header</th><th>Type</th><th>Required</th><th>Description</th></tr></thead><tbody><tr><td>Authorization</td><td>string</td><td>Yes</td><td>Bearer token for authenticated user</td></tr></tbody></table>

**Path Parameters**:

<table id="bkmrk-parameter-type-requi"><thead><tr><th>Parameter</th><th>Type</th><th>Required</th><th>Description</th><th>Validation</th></tr></thead><tbody><tr><td>productId</td><td>string (UUID)</td><td>Yes</td><td>Unique identifier of the product</td><td>Valid UUID format</td></tr></tbody></table>

**Success Response JSON Sample**:

```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Available groups retrieved successfully",
  "action_time": "2025-10-02T14:30:45",
  "data": [
    {
      "groupInstanceId": "gp123456-7890-abcd-ef12-345678901234",
      "groupCode": "GP-A3X7K9",
      "productName": "Premium Wireless Headphones",
      "productImage": "https://cdn.nextgate.com/products/headphones-001.jpg",
      "shopName": "TechWorld Electronics",
      "groupPrice": 80000.00,
      "savingsPercentage": 46.67,
      "currency": "TZS",
      "totalSeats": 10,
      "seatsOccupied": 4,
      "seatsRemaining": 6,
      "totalParticipants": 3,
      "progressPercentage": 40.00,
      "status": "OPEN",
      "expiresAt": "2025-10-02T20:30:45",
      "isExpired": false,
      "isUserMember": false,
      "participants": [
        {
          "userId": "user1234-5678-90ab-cdef-123456789012",
          "userName": "john_doe",
          "userProfilePicture": "https://cdn.nextgate.com/profiles/john.jpg",
          "quantity": 2,
          "contributionPercentage": 50.00
        },
        {
          "userId": "user2345-6789-01bc-def1-234567890123",
          "userName": "jane_smith",
          "userProfilePicture": "https://cdn.nextgate.com/profiles/jane.jpg",
          "quantity": 1,
          "contributionPercentage": 25.00
        },
        {
          "userId": "user3456-7890-12cd-ef12-345678901234",
          "userName": "bob_wilson",
          "userProfilePicture": null,
          "quantity": 1,
          "contributionPercentage": 25.00
        }
      ]
    },
    {
      "groupInstanceId": "gp234567-8901-bcde-f123-456789012345",
      "groupCode": "GP-B2Y8M5",
      "productName": "Premium Wireless Headphones",
      "productImage": "https://cdn.nextgate.com/products/headphones-001.jpg",
      "shopName": "TechWorld Electronics",
      "groupPrice": 80000.00,
      "savingsPercentage": 46.67,
      "currency": "TZS",
      "totalSeats": 10,
      "seatsOccupied": 7,
      "seatsRemaining": 3,
      "totalParticipants": 5,
      "progressPercentage": 70.00,
      "status": "OPEN",
      "expiresAt": "2025-10-02T18:45:30",
      "isExpired": false,
      "isUserMember": true,
      "participants": [
        {
          "userId": "user4567-8901-23de-f234-567890123456",
          "userName": "alice_brown",
          "userProfilePicture": "https://cdn.nextgate.com/profiles/alice.jpg",
          "quantity": 3,
          "contributionPercentage": 42.86
        }
      ]
    }
  ]
}

```

**Success Response Fields**:

<table id="bkmrk-field-description-gr"><thead><tr><th>Field</th><th>Description</th></tr></thead><tbody><tr><td>groupInstanceId</td><td>Unique identifier for the group</td></tr><tr><td>groupCode</td><td>Human-readable group code (e.g., GP-A3X7K9)</td></tr><tr><td>productName</td><td>Name of the product in this group</td></tr><tr><td>productImage</td><td>Product image URL</td></tr><tr><td>shopName</td><td>Shop selling this product</td></tr><tr><td>groupPrice</td><td>Discounted price per unit in TZS</td></tr><tr><td>savingsPercentage</td><td>Percentage saved vs regular price</td></tr><tr><td>currency</td><td>Currency code (TZS)</td></tr><tr><td>totalSeats</td><td>Maximum number of seats in this group</td></tr><tr><td>seatsOccupied</td><td>Number of seats currently filled</td></tr><tr><td>seatsRemaining</td><td>Available seats remaining</td></tr><tr><td>totalParticipants</td><td>Number of unique participants</td></tr><tr><td>progressPercentage</td><td>Group completion percentage (0-100)</td></tr><tr><td>status</td><td>Group status (OPEN, COMPLETED, FAILED, DELETED)</td></tr><tr><td>expiresAt</td><td>When the group expires</td></tr><tr><td>isExpired</td><td>Whether the group has expired</td></tr><tr><td>isUserMember</td><td>Whether authenticated user is in this group</td></tr><tr><td>participants</td><td>Array of participant previews</td></tr><tr><td>participants\[\].userId</td><td>Participant's user ID</td></tr><tr><td>participants\[\].userName</td><td>Participant's username</td></tr><tr><td>participants\[\].userProfilePicture</td><td>Participant's profile picture URL</td></tr><tr><td>participants\[\].quantity</td><td>Number of seats this participant holds</td></tr><tr><td>participants\[\].contributionPercentage</td><td>Percentage of total seats this participant holds</td></tr></tbody></table>

**Error Response Examples**:

*Not Found - Product Not Found (404):*

```json
{
  "success": false,
  "httpStatus": "NOT_FOUND",
  "message": "Product not found",
  "action_time": "2025-10-02T14:30:45",
  "data": "Product not found"
}

```

---

## 2. Get Group by ID

**Purpose**: Retrieves detailed information about a specific group purchase instance including all participants and their histories.

**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}/group-purchases/{groupId}`

**Access Level**: 🔒 Protected (Requires Authentication)

**Authentication**: Bearer Token required in Authorization header

**Request Headers**:

<table id="bkmrk-header-type-required-1"><thead><tr><th>Header</th><th>Type</th><th>Required</th><th>Description</th></tr></thead><tbody><tr><td>Authorization</td><td>string</td><td>Yes</td><td>Bearer token for authenticated user</td></tr></tbody></table>

**Path Parameters**:

<table id="bkmrk-parameter-type-requi-1"><thead><tr><th>Parameter</th><th>Type</th><th>Required</th><th>Description</th><th>Validation</th></tr></thead><tbody><tr><td>groupId</td><td>string (UUID)</td><td>Yes</td><td>Unique identifier of the group</td><td>Valid UUID format</td></tr></tbody></table>

**Success Response JSON Sample**:

```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Group retrieved successfully",
  "action_time": "2025-10-02T14:35:45",
  "data": {
    "groupInstanceId": "gp123456-7890-abcd-ef12-345678901234",
    "groupCode": "GP-A3X7K9",
    "productId": "prod1234-5678-90ab-cdef-123456789012",
    "productName": "Premium Wireless Headphones",
    "productImage": "https://cdn.nextgate.com/products/headphones-001.jpg",
    "shopId": "shop1234-5678-90ab-cdef-123456789012",
    "shopName": "TechWorld Electronics",
    "shopLogo": "https://cdn.nextgate.com/shops/techworld-logo.jpg",
    "regularPrice": 150000.00,
    "groupPrice": 80000.00,
    "savingsAmount": 70000.00,
    "savingsPercentage": 46.67,
    "currency": "TZS",
    "totalSeats": 10,
    "seatsOccupied": 4,
    "seatsRemaining": 6,
    "totalParticipants": 3,
    "progressPercentage": 40.00,
    "status": "OPEN",
    "isExpired": false,
    "isFull": false,
    "initiatorId": "user1234-5678-90ab-cdef-123456789012",
    "initiatorName": "john_doe",
    "durationHours": 24,
    "createdAt": "2025-10-01T20:30:45",
    "expiresAt": "2025-10-02T20:30:45",
    "completedAt": null,
    "maxPerCustomer": 5,
    "isUserMember": true,
    "myParticipantId": "part1234-5678-90ab-cdef-123456789012",
    "myQuantity": 2,
    "participants": [
      {
        "participantId": "part1234-5678-90ab-cdef-123456789012",
        "userId": "user1234-5678-90ab-cdef-123456789012",
        "userName": "john_doe",
        "userProfilePicture": "https://cdn.nextgate.com/profiles/john.jpg",
        "quantity": 2,
        "totalPaid": 160000.00,
        "status": "ACTIVE",
        "joinedAt": "2025-10-01T20:30:45",
        "contributionPercentage": 50.00,
        "purchaseCount": 1,
        "hasTransferred": false,
        "purchaseHistory": [
          {
            "checkoutSessionId": "checkout1-2345-6789-0abc-def123456789",
            "quantity": 2,
            "amountPaid": 160000.00,
            "purchasedAt": "2025-10-01T20:30:45",
            "transactionId": "txn_1234567890abcdef"
          }
        ],
        "transferHistory": []
      },
      {
        "participantId": "part2345-6789-01bc-def1-234567890123",
        "userId": "user2345-6789-01bc-def1-234567890123",
        "userName": "jane_smith",
        "userProfilePicture": "https://cdn.nextgate.com/profiles/jane.jpg",
        "quantity": 1,
        "totalPaid": 80000.00,
        "status": "ACTIVE",
        "joinedAt": "2025-10-01T21:15:20",
        "contributionPercentage": 25.00,
        "purchaseCount": 1,
        "hasTransferred": false
      },
      {
        "participantId": "part3456-7890-12cd-ef12-345678901234",
        "userId": "user3456-7890-12cd-ef12-345678901234",
        "userName": "bob_wilson",
        "userProfilePicture": null,
        "quantity": 1,
        "totalPaid": 80000.00,
        "status": "ACTIVE",
        "joinedAt": "2025-10-02T08:45:10",
        "contributionPercentage": 25.00,
        "purchaseCount": 1,
        "hasTransferred": false
      }
    ]
  }
}

```

**Success Response Fields**:

<table id="bkmrk-field-description-gr-1"><thead><tr><th>Field</th><th>Description</th></tr></thead><tbody><tr><td>groupInstanceId</td><td>Unique identifier for the group</td></tr><tr><td>groupCode</td><td>Human-readable group code</td></tr><tr><td>productId</td><td>Product unique identifier</td></tr><tr><td>productName</td><td>Product name</td></tr><tr><td>productImage</td><td>Product image URL</td></tr><tr><td>shopId</td><td>Shop identifier</td></tr><tr><td>shopName</td><td>Shop name</td></tr><tr><td>shopLogo</td><td>Shop logo URL</td></tr><tr><td>regularPrice</td><td>Original product price in TZS</td></tr><tr><td>groupPrice</td><td>Discounted group price in TZS</td></tr><tr><td>savingsAmount</td><td>Amount saved per unit (regularPrice - groupPrice)</td></tr><tr><td>savingsPercentage</td><td>Percentage saved</td></tr><tr><td>currency</td><td>Currency code (TZS)</td></tr><tr><td>totalSeats</td><td>Maximum seats in group</td></tr><tr><td>seatsOccupied</td><td>Filled seats</td></tr><tr><td>seatsRemaining</td><td>Available seats</td></tr><tr><td>totalParticipants</td><td>Unique participants count</td></tr><tr><td>progressPercentage</td><td>Completion percentage</td></tr><tr><td>status</td><td>OPEN, COMPLETED, FAILED, or DELETED</td></tr><tr><td>isExpired</td><td>Whether group expired</td></tr><tr><td>isFull</td><td>Whether all seats filled</td></tr><tr><td>initiatorId</td><td>User who created the group</td></tr><tr><td>initiatorName</td><td>Initiator's username</td></tr><tr><td>durationHours</td><td>Group duration in hours</td></tr><tr><td>createdAt</td><td>Group creation timestamp</td></tr><tr><td>expiresAt</td><td>Expiration timestamp</td></tr><tr><td>completedAt</td><td>Completion timestamp (null if not completed)</td></tr><tr><td>maxPerCustomer</td><td>Maximum seats per customer</td></tr><tr><td>isUserMember</td><td>Whether authenticated user is member</td></tr><tr><td>myParticipantId</td><td>User's participant ID (if member)</td></tr><tr><td>myQuantity</td><td>User's seat quantity (if member)</td></tr><tr><td>participants</td><td>Array of detailed participant information</td></tr><tr><td>participants\[\].participantId</td><td>Participant unique identifier</td></tr><tr><td>participants\[\].userId</td><td>User ID</td></tr><tr><td>participants\[\].userName</td><td>Username</td></tr><tr><td>participants\[\].userProfilePicture</td><td>Profile picture URL</td></tr><tr><td>participants\[\].quantity</td><td>Number of seats held</td></tr><tr><td>participants\[\].totalPaid</td><td>Total amount paid in TZS</td></tr><tr><td>participants\[\].status</td><td>ACTIVE, TRANSFERRED\_OUT, or REFUNDED</td></tr><tr><td>participants\[\].joinedAt</td><td>Join timestamp</td></tr><tr><td>participants\[\].contributionPercentage</td><td>Percentage of total seats</td></tr><tr><td>participants\[\].purchaseCount</td><td>Number of purchases made</td></tr><tr><td>participants\[\].hasTransferred</td><td>Whether participated in transfers</td></tr><tr><td>participants\[\].purchaseHistory</td><td>Purchase records (only shown to participant owner)</td></tr><tr><td>participants\[\].transferHistory</td><td>Transfer records (only shown to participant owner)</td></tr></tbody></table>

**Error Response Examples**:

*Not Found (404):*

```json
{
  "success": false,
  "httpStatus": "NOT_FOUND",
  "message": "Group not found with ID: gp123456-7890-abcd-ef12-345678901234",
  "action_time": "2025-10-02T14:35:45",
  "data": "Group not found with ID: gp123456-7890-abcd-ef12-345678901234"
}

```

---

## 3. Get Group by Code

**Purpose**: Retrieves group information using the human-readable group code instead of UUID.

**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}/group-purchases/code/{groupCode}`

**Access Level**: 🔒 Protected (Requires Authentication)

**Authentication**: Bearer Token required in Authorization header

**Request Headers**:

<table id="bkmrk-header-type-required-2"><thead><tr><th>Header</th><th>Type</th><th>Required</th><th>Description</th></tr></thead><tbody><tr><td>Authorization</td><td>string</td><td>Yes</td><td>Bearer token for authenticated user</td></tr></tbody></table>

**Path Parameters**:

<table id="bkmrk-parameter-type-requi-2"><thead><tr><th>Parameter</th><th>Type</th><th>Required</th><th>Description</th><th>Validation</th></tr></thead><tbody><tr><td>groupCode</td><td>string</td><td>Yes</td><td>Group code (e.g., GP-A3X7K9)</td><td>Format: GP-XXXXXX (6 alphanumeric characters)</td></tr></tbody></table>

**Success Response JSON Sample**:

```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Group retrieved successfully",
  "action_time": "2025-10-02T14:40:45",
  "data": {
    "groupInstanceId": "gp123456-7890-abcd-ef12-345678901234",
    "groupCode": "GP-A3X7K9",
    "productId": "prod1234-5678-90ab-cdef-123456789012",
    "productName": "Premium Wireless Headphones",
    "productImage": "https://cdn.nextgate.com/products/headphones-001.jpg",
    "shopId": "shop1234-5678-90ab-cdef-123456789012",
    "shopName": "TechWorld Electronics",
    "shopLogo": "https://cdn.nextgate.com/shops/techworld-logo.jpg",
    "regularPrice": 150000.00,
    "groupPrice": 80000.00,
    "savingsAmount": 70000.00,
    "savingsPercentage": 46.67,
    "currency": "TZS",
    "totalSeats": 10,
    "seatsOccupied": 4,
    "seatsRemaining": 6,
    "totalParticipants": 3,
    "progressPercentage": 40.00,
    "status": "OPEN",
    "isExpired": false,
    "isFull": false,
    "initiatorId": "user1234-5678-90ab-cdef-123456789012",
    "initiatorName": "john_doe",
    "durationHours": 24,
    "createdAt": "2025-10-01T20:30:45",
    "expiresAt": "2025-10-02T20:30:45",
    "completedAt": null,
    "maxPerCustomer": 5,
    "isUserMember": false,
    "myParticipantId": null,
    "myQuantity": null,
    "participants": []
  }
}

```

**Success Response Fields**:

<table id="bkmrk-field-description-al"><thead><tr><th>Field</th><th>Description</th></tr></thead><tbody><tr><td>All fields</td><td>Same as "Get Group by ID" response</td></tr></tbody></table>

**Error Response Examples**:

*Not Found (404):*

```json
{
  "success": false,
  "httpStatus": "NOT_FOUND",
  "message": "Group not found with code: GP-INVALID",
  "action_time": "2025-10-02T14:40:45",
  "data": "Group not found with code: GP-INVALID"
}

```

---

## 4. Get My Groups

**Purpose**: Retrieves all groups that the authenticated user is a member of, optionally filtered by status.

**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}/group-purchases/my-groups`

**Access Level**: 🔒 Protected (Requires Authentication)

**Authentication**: Bearer Token required in Authorization header

**Request Headers**:

<table id="bkmrk-header-type-required-3"><thead><tr><th>Header</th><th>Type</th><th>Required</th><th>Description</th></tr></thead><tbody><tr><td>Authorization</td><td>string</td><td>Yes</td><td>Bearer token for authenticated user</td></tr></tbody></table>

**Query Parameters**:

<table id="bkmrk-parameter-type-requi-3"><thead><tr><th>Parameter</th><th>Type</th><th>Required</th><th>Description</th><th>Validation</th><th>Default</th></tr></thead><tbody><tr><td>status</td><td>string</td><td>No</td><td>Filter by group status</td><td>enum: OPEN, COMPLETED, FAILED, DELETED</td><td>null (all statuses)</td></tr></tbody></table>

**Success Response JSON Sample**:

```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "My groups retrieved successfully",
  "action_time": "2025-10-02T14:45:45",
  "data": [
    {
      "groupInstanceId": "gp123456-7890-abcd-ef12-345678901234",
      "groupCode": "GP-A3X7K9",
      "productName": "Premium Wireless Headphones",
      "productImage": "https://cdn.nextgate.com/products/headphones-001.jpg",
      "shopName": "TechWorld Electronics",
      "groupPrice": 80000.00,
      "savingsPercentage": 46.67,
      "currency": "TZS",
      "totalSeats": 10,
      "seatsOccupied": 8,
      "seatsRemaining": 2,
      "totalParticipants": 5,
      "progressPercentage": 80.00,
      "status": "OPEN",
      "expiresAt": "2025-10-02T20:30:45",
      "isExpired": false,
      "isUserMember": true,
      "participants": [
        {
          "userId": "user1234-5678-90ab-cdef-123456789012",
          "userName": "john_doe",
          "userProfilePicture": "https://cdn.nextgate.com/profiles/john.jpg",
          "quantity": 2,
          "contributionPercentage": 25.00
        }
      ]
    },
    {
      "groupInstanceId": "gp234567-8901-bcde-f123-456789012345",
      "groupCode": "GP-B2Y8M5",
      "productName": "Smart Watch Series 5",
      "productImage": "https://cdn.nextgate.com/products/watch-005.jpg",
      "shopName": "Gadget Hub",
      "groupPrice": 250000.00,
      "savingsPercentage": 28.57,
      "currency": "TZS",
      "totalSeats": 15,
      "seatsOccupied": 15,
      "seatsRemaining": 0,
      "totalParticipants": 8,
      "progressPercentage": 100.00,
      "status": "COMPLETED",
      "expiresAt": "2025-10-01T18:20:30",
      "isExpired": false,
      "isUserMember": true,
      "participants": []
    }
  ]
}

```

**Success Response Fields**:

<table id="bkmrk-field-description-al-1"><thead><tr><th>Field</th><th>Description</th></tr></thead><tbody><tr><td>All fields</td><td>Same as "Get Available Groups for Product" response</td></tr></tbody></table>

---

## 5. Get My Participations

**Purpose**: Retrieves all active participations of the authenticated user across all groups with detailed participant information.

**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}/group-purchases/my-participations`

**Access Level**: 🔒 Protected (Requires Authentication)

**Authentication**: Bearer Token required in Authorization header

**Request Headers**:

<table id="bkmrk-header-type-required-4"><thead><tr><th>Header</th><th>Type</th><th>Required</th><th>Description</th></tr></thead><tbody><tr><td>Authorization</td><td>string</td><td>Yes</td><td>Bearer token for authenticated user</td></tr></tbody></table>

**Success Response JSON Sample**:

```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "My participations retrieved successfully",
  "action_time": "2025-10-02T14:50:45",
  "data": [
    {
      "participantId": "part1234-5678-90ab-cdef-123456789012",
      "userId": "user1234-5678-90ab-cdef-123456789012",
      "userName": "john_doe",
      "userProfilePicture": "https://cdn.nextgate.com/profiles/john.jpg",
      "quantity": 2,
      "totalPaid": 160000.00,
      "status": "ACTIVE",
      "joinedAt": "2025-10-01T20:30:45",
      "purchaseCount": 1,
      "hasTransferred": false,
      "checkoutSessionId": "checkout1-2345-6789-0abc-def123456789",
      "purchaseHistory": [
        {
          "checkoutSessionId": "checkout1-2345-6789-0abc-def123456789",
          "quantity": 2,
          "amountPaid": 160000.00,
          "purchasedAt": "2025-10-01T20:30:45",
          "transactionId": "txn_1234567890abcdef"
        }
      ],
      "transferHistory": []
    },
    {
      "participantId": "part2345-6789-01bc-def1-234567890123",
      "userId": "user1234-5678-90ab-cdef-123456789012",
      "userName": "john_doe",
      "userProfilePicture": "https://cdn.nextgate.com/profiles/john.jpg",
      "quantity": 3,
      "totalPaid": 750000.00,
      "status": "ACTIVE",
      "joinedAt": "2025-10-02T10:15:20",
      "purchaseCount": 2,
      "hasTransferred": true,
      "checkoutSessionId": "checkout2-3456-7890-1bcd-ef2345678901",
      "purchaseHistory": [
        {
          "checkoutSessionId": "checkout2-3456-7890-1bcd-ef2345678901",
          "quantity": 1,
          "amountPaid": 250000.00,
          "purchasedAt": "2025-10-02T10:15:20",
          "transactionId": "txn_2345678901bcdef0"
        },
        {
          "checkoutSessionId": "checkout3-4567-8901-2cde-f34567890123",
          "quantity": 2,
          "amountPaid": 500000.00,
          "purchasedAt": "2025-10-02T12:30:15",
          "transactionId": "txn_3456789012cdef01"
        }
      ],
      "transferHistory": [
        {
          "fromGroupId": "gp345678-9012-cdef-1234-567890123456",
          "fromGroupCode": null,
          "toGroupId": "gp234567-8901-bcde-f123-456789012345",
          "toGroupCode": null,
          "transferredAt": "2025-10-02T11:45:30",
          "reason": "Transferred 1 seats from group GP-C3Z9N7"
        }
      ]
    }
  ]
}

```

**Success Response Fields**:

<table id="bkmrk-field-description-pa"><thead><tr><th>Field</th><th>Description</th></tr></thead><tbody><tr><td>participantId</td><td>Participant unique identifier</td></tr><tr><td>userId</td><td>User ID of the participant</td></tr><tr><td>userName</td><td>Username</td></tr><tr><td>userProfilePicture</td><td>Profile picture URL</td></tr><tr><td>quantity</td><td>Total seats held</td></tr><tr><td>totalPaid</td><td>Total amount paid in TZS</td></tr><tr><td>status</td><td>ACTIVE, TRANSFERRED\_OUT, or REFUNDED</td></tr><tr><td>joinedAt</td><td>When user joined this group</td></tr><tr><td>purchaseCount</td><td>Number of purchases made in this group</td></tr><tr><td>hasTransferred</td><td>Whether user has transfer history</td></tr><tr><td>checkoutSessionId</td><td>Original checkout session ID</td></tr><tr><td>purchaseHistory</td><td>Array of all purchases in this group</td></tr><tr><td>purchaseHistory\[\].checkoutSessionId</td><td>Checkout session for this purchase</td></tr><tr><td>purchaseHistory\[\].quantity</td><td>Seats purchased</td></tr><tr><td>purchaseHistory\[\].amountPaid</td><td>Amount paid for this purchase</td></tr><tr><td>purchaseHistory\[\].purchasedAt</td><td>Purchase timestamp</td></tr><tr><td>purchaseHistory\[\].transactionId</td><td>Payment transaction ID</td></tr><tr><td>transferHistory</td><td>Array of all transfers involving this participation</td></tr><tr><td>transferHistory\[\].fromGroupId</td><td>Source group ID</td></tr><tr><td>transferHistory\[\].toGroupId</td><td>Target group ID</td></tr><tr><td>transferHistory\[\].transferredAt</td><td>Transfer timestamp</td></tr><tr><td>transferHistory\[\].reason</td><td>Transfer reason/description</td></tr></tbody></table>

---

## 6. Transfer Seats Between Groups

**Purpose**: Transfers seats from one group to another. Allows users to move their purchases between compatible groups (same product, shop, and price).

**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}/group-purchases/transfer`

**Access Level**: 🔒 Protected (Requires Authentication)

**Authentication**: Bearer Token required in Authorization header

**Request Headers**:

<table id="bkmrk-header-type-required-5"><thead><tr><th>Header</th><th>Type</th><th>Required</th><th>Description</th></tr></thead><tbody><tr><td>Authorization</td><td>string</td><td>Yes</td><td>Bearer token for authenticated user</td></tr><tr><td>Content-Type</td><td>string</td><td>Yes</td><td>Must be `application/json`</td></tr></tbody></table>

**Request JSON Sample**:

```json
{
  "sourceGroupId": "gp123456-7890-abcd-ef12-345678901234",
  "targetGroupId": "gp234567-8901-bcde-f123-456789012345",
  "quantity": 2
}

```

**Request Body Parameters**:

<table id="bkmrk-parameter-type-requi-4"><thead><tr><th>Parameter</th><th>Type</th><th>Required</th><th>Description</th><th>Validation</th></tr></thead><tbody><tr><td>sourceGroupId</td><td>string (UUID)</td><td>Yes</td><td>Group to transfer from</td><td>Valid UUID, user must be active member</td></tr><tr><td>targetGroupId</td><td>string (UUID)</td><td>Yes</td><td>Group to transfer to</td><td>Valid UUID, must be different from source</td></tr><tr><td>quantity</td><td>integer</td><td>Yes</td><td>Number of seats to transfer</td><td>Min: 1, cannot exceed user's quantity in source group</td></tr></tbody></table>

**Success Response JSON Sample**:

```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Seats transferred successfully",
  "action_time": "2025-10-02T15:00:45",
  "data": {
    "participantId": "part2345-6789-01bc-def1-234567890123",
    "userId": "user1234-5678-90ab-cdef-123456789012",
    "userName": "john_doe",
    "userProfilePicture": "https://cdn.nextgate.com/profiles/john.jpg",
    "quantity": 3,
    "totalPaid": 0.00,
    "status": "ACTIVE",
    "joinedAt": "2025-10-02T15:00:45",
    "purchaseCount": 0,
    "hasTransferred": true,
    "checkoutSessionId": "checkout1-2345-6789-0abc-def123456789",
    "purchaseHistory": [],
    "transferHistory": [
      {
        "fromGroupId": "gp123456-7890-abcd-ef12-345678901234",
        "fromGroupCode": null,
        "toGroupId": "gp234567-8901-bcde-f123-456789012345",
        "toGroupCode": null,
        "transferredAt": "2025-10-02T15:00:45",
        "reason": "Transferred 2 seats from group GP-A3X7K9"
      }
    ]
  }
}

```

**Success Response Fields**:

<table id="bkmrk-field-description-pa-1"><thead><tr><th>Field</th><th>Description</th></tr></thead><tbody><tr><td>participantId</td><td>Updated participant ID in target group</td></tr><tr><td>userId</td><td>User ID</td></tr><tr><td>userName</td><td>Username</td></tr><tr><td>userProfilePicture</td><td>Profile picture URL</td></tr><tr><td>quantity</td><td>New total quantity in target group</td></tr><tr><td>totalPaid</td><td>Total paid (0 for transfers)</td></tr><tr><td>status</td><td>Participant status (ACTIVE)</td></tr><tr><td>joinedAt</td><td>Join timestamp (current time if new to target)</td></tr><tr><td>purchaseCount</td><td>Purchase count (0 for pure transfers)</td></tr><tr><td>hasTransferred</td><td>Always true for transferred participants</td></tr><tr><td>checkoutSessionId</td><td>Original checkout session ID</td></tr><tr><td>purchaseHistory</td><td>Purchase history (shown only to owner)</td></tr><tr><td>transferHistory</td><td>Transfer history including this transfer</td></tr></tbody></table>

**Error Response Examples**:

*Bad Request - Same Source and Target (400):*

```json
{
  "success": false,
  "httpStatus": "BAD_REQUEST",
  "message": "Source and target groups must be different",
  "action_time": "2025-10-02T15:00:45",
  "data": "Source and target groups must be different"
}

```

*Bad Request - Insufficient Seats (400):*

```json
{
  "success": false,
  "httpStatus": "BAD_REQUEST",
  "message": "Not enough seats to transfer. You have: 1, requested: 2",
  "action_time": "2025-10-02T15:00:45",
  "data": "Not enough seats to transfer. You have: 1, requested: 2"
}

```

*Bad Request - Target Group Full (400):*

```json
{
  "success": false,
  "httpStatus": "BAD_REQUEST",
  "message": "Not enough seats available. Requested: 2, Available: 1",
  "action_time": "2025-10-02T15:00:45",
  "data": "Not enough seats available. Requested: 2, Available: 1"
}

```

*Bad Request - Product Mismatch (400):*

```json
{
  "success": false,
  "httpStatus": "BAD_REQUEST",
  "message": "Cannot transfer between groups with different products",
  "action_time": "2025-10-02T15:00:45",
  "data": "Cannot transfer between groups with different products"
}

```

*Bad Request - Price Mismatch (400):*

```json
{
  "success": false,
  "httpStatus": "BAD_REQUEST",
  "message": "Cannot transfer. Price mismatch: 80000.00 vs 75000.00",
  "action_time": "2025-10-02T15:00:45",
  "data": "Cannot transfer. Price mismatch: 80000.00 vs 75000.00"
}

```

*Not Found - Not a Participant (404):*

```json
{
  "success": false,
  "httpStatus": "NOT_FOUND",
  "message": "You are not a participant in the source group",
  "action_time": "2025-10-02T15:00:45",
  "data": "You are not a participant in the source group"
}

```

---

## 7. Update Group Name

**Purpose**: Allow the group initiator to update the group name. Group names must be unique among active (OPEN) groups.

**Endpoint**: <span style="background-color: #50e3c2; color: black; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">PATCH</span> `{base_url}/api/v1/groups/{groupId}/name`

**Access Level**: 🔒 Protected (Requires Authentication - Group Initiator Only)

**Authentication**: Bearer Token

**Request Headers**:

<table id="bkmrk-header-type-required-6"><thead><tr><th>Header</th><th>Type</th><th>Required</th><th>Description</th></tr></thead><tbody><tr><td>Authorization</td><td>string</td><td>Yes</td><td>Bearer token for authentication</td></tr><tr><td>Content-Type</td><td>string</td><td>Yes</td><td>application/json</td></tr></tbody></table>

**Path Parameters**:

<table id="bkmrk-parameter-type-requi-5"><thead><tr><th>Parameter</th><th>Type</th><th>Required</th><th>Description</th><th>Validation</th></tr></thead><tbody><tr><td>groupId</td><td>UUID</td><td>Yes</td><td>Unique identifier of the group</td><td>Valid UUID format</td></tr></tbody></table>

**Request Body**:

<table id="bkmrk-field-type-required-"><thead><tr><th>Field</th><th>Type</th><th>Required</th><th>Description</th><th>Validation</th></tr></thead><tbody><tr><td>groupName</td><td>string</td><td>Yes</td><td>New name for the group</td><td>3-100 characters, unique among active groups</td></tr></tbody></table>

**Request JSON Sample**:

```json
{
  "groupName": "iPhone 15 Pro - Dar es Salaam Deal"
}

```

**Success Response JSON Sample**:

```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Group name updated successfully",
  "action_time": "2025-12-30T10:00:00",
  "data": {
    "groupInstanceId": "550e8400-e29b-41d4-a716-446655440000",
    "groupCode": "GP-ABC123",
    "groupName": "iPhone 15 Pro - Dar es Salaam Deal",
    "productName": "iPhone 15 Pro",
    "productImage": "https://storage.example.com/products/iphone15.jpg",
    "regularPrice": 2500000.00,
    "groupPrice": 2200000.00,
    "totalSeats": 5,
    "seatsOccupied": 3,
    "seatsRemaining": 2,
    "totalParticipants": 3,
    "status": "OPEN",
    "createdAt": "2025-12-30T08:00:00",
    "expiresAt": "2025-12-31T08:00:00",
    "updatedAt": "2025-12-30T10:00:00"
  }
}

```

**Success Response Fields**:

<table id="bkmrk-field-description-gr-2"><thead><tr><th>Field</th><th>Description</th></tr></thead><tbody><tr><td>groupInstanceId</td><td>Unique identifier of the group</td></tr><tr><td>groupCode</td><td>Auto-generated group code (immutable)</td></tr><tr><td>groupName</td><td>Updated group name</td></tr><tr><td>productName</td><td>Name of the product</td></tr><tr><td>productImage</td><td>Product image URL</td></tr><tr><td>regularPrice</td><td>Regular product price</td></tr><tr><td>groupPrice</td><td>Discounted group price</td></tr><tr><td>totalSeats</td><td>Total seats available in the group</td></tr><tr><td>seatsOccupied</td><td>Number of seats currently occupied</td></tr><tr><td>seatsRemaining</td><td>Number of seats still available</td></tr><tr><td>totalParticipants</td><td>Number of participants in the group</td></tr><tr><td>status</td><td>Group status (OPEN)</td></tr><tr><td>createdAt</td><td>Group creation timestamp</td></tr><tr><td>expiresAt</td><td>Group expiration timestamp</td></tr><tr><td>updatedAt</td><td>Last update timestamp</td></tr></tbody></table>

**Error Response JSON Samples**:

*Group not found:*

```json
{
  "success": false,
  "httpStatus": "NOT_FOUND",
  "message": "Group not found",
  "action_time": "2025-12-30T10:00:00",
  "data": null
}

```

*Not the initiator:*

```json
{
  "success": false,
  "httpStatus": "BAD_REQUEST",
  "message": "Only the group initiator can change the group name",
  "action_time": "2025-12-30T10:00:00",
  "data": null
}

```

*Group not active:*

```json
{
  "success": false,
  "httpStatus": "BAD_REQUEST",
  "message": "Cannot rename group with status: COMPLETED",
  "action_time": "2025-12-30T10:00:00",
  "data": null
}

```

*Group expired:*

```json
{
  "success": false,
  "httpStatus": "BAD_REQUEST",
  "message": "Cannot rename expired group",
  "action_time": "2025-12-30T10:00:00",
  "data": null
}

```

*Invalid name length:*

```json
{
  "success": false,
  "httpStatus": "BAD_REQUEST",
  "message": "Group name must be between 3 and 100 characters",
  "action_time": "2025-12-30T10:00:00",
  "data": null
}

```

*Name already taken:*

```json
{
  "success": false,
  "httpStatus": "BAD_REQUEST",
  "message": "Group name already taken: iPhone 15 Pro - Dar es Salaam Deal",
  "action_time": "2025-12-30T10:00:00",
  "data": null
}

```

**Standard Error Types**:

<table id="bkmrk-status-message-cause"><thead><tr><th>Status</th><th>Message</th><th>Cause</th></tr></thead><tbody><tr><td>`400 BAD_REQUEST`</td><td>Only the group initiator can change the group name</td><td>User is not the group initiator</td></tr><tr><td>`400 BAD_REQUEST`</td><td>Cannot rename group with status: {status}</td><td>Group is not OPEN</td></tr><tr><td>`400 BAD_REQUEST`</td><td>Cannot rename deleted group</td><td>Group has been deleted</td></tr><tr><td>`400 BAD_REQUEST`</td><td>Cannot rename expired group</td><td>Group has expired</td></tr><tr><td>`400 BAD_REQUEST`</td><td>Group name must be between 3 and 100 characters</td><td>Invalid name length</td></tr><tr><td>`400 BAD_REQUEST`</td><td>Group name already taken: {name}</td><td>Name exists on another active group</td></tr><tr><td>`401 UNAUTHORIZED`</td><td>User not authenticated</td><td>Missing or invalid token</td></tr><tr><td>`404 NOT_FOUND`</td><td>Group not found</td><td>Invalid group ID</td></tr></tbody></table>

**Business Rules**:

<table id="bkmrk-rule-description-ini"><thead><tr><th>Rule</th><th>Description</th></tr></thead><tbody><tr><td>Initiator only</td><td>Only the user who created the group can rename it</td></tr><tr><td>Active groups only</td><td>Group must have status `OPEN`</td></tr><tr><td>Not expired</td><td>Group must not be past its expiration time</td></tr><tr><td>Not deleted</td><td>Group must not be soft-deleted</td></tr><tr><td>Unique name</td><td>Name must be unique among all active (OPEN) groups</td></tr><tr><td>Name length</td><td>Must be between 3 and 100 characters</td></tr><tr><td>Trimmed</td><td>Leading/trailing whitespace is automatically removed</td></tr></tbody></table>

**Default Group Name**: When a group is created, the name is auto-generated as:

```
{groupCode}-{productName}

```

Example: `GP-ABC123-iPhone 15 Pro`

**Usage Example**:

```bash
curl -X PATCH \
  'https://api.nexgate.com/api/v1/groups/550e8400-e29b-41d4-a716-446655440000/name' \
  -H 'Authorization: Bearer eyJhbGciOiJIUzI1NiIs...' \
  -H 'Content-Type: application/json' \
  -d '{
    "groupName": "iPhone 15 Pro - Dar es Salaam Deal"
  }'

```

## Group Purchase Workflow

### 1. Creating a New Group

When a user makes a GROUP\_PURCHASE checkout and no `groupInstanceId` is provided in metadata:

**Flow:**

1. User creates checkout session with `sessionType: GROUP_PURCHASE`
2. User processes payment (WALLET only)
3. Payment completes successfully
4. System automatically creates new group instance
5. User becomes first participant (initiator)
6. Group gets unique code (e.g., GP-A3X7K9)
7. Group status set to OPEN
8. Expiration set based on product's `groupTimeLimitHours`

### 2. Joining an Existing Group

When a user makes a GROUP\_PURCHASE checkout with `groupInstanceId` in metadata:

**Flow:**

1. User finds available group (via product page or group code)
2. User creates checkout session with `sessionType: GROUP_PURCHASE`
3. User includes `groupInstanceId` in checkout metadata
4. User processes payment (WALLET only)
5. Payment completes successfully
6. System adds user to existing group
7. Group's `seatsOccupied` increases
8. Group's `totalParticipants` increases (if new member)

**Note:** User can join same group multiple times to buy more seats (Hybrid Approach)

### 3. Group Completion

When a group fills all seats:

**Automatic Actions:**

1. Group status changes to COMPLETED
2. `completedAt` timestamp recorded
3. Orders created for all participants
4. Inventory permanently deducted
5. No new participants allowed

### 4. Group Expiration

When a group reaches expiration time without filling:

**Automatic Actions:**

1. Group status changes to FAILED
2. All participants refunded
3. Inventory holds released
4. Participant status changes to REFUNDED

### 5. Transferring Between Groups

Users can transfer seats between compatible groups:

**Compatibility Requirements:**

- Same product
- Same shop
- Same group price
- Target group must be OPEN
- Target group not expired
- Target group has available seats

**Transfer Types:**

**Partial Transfer:**

- Transfer some seats, keep some in source
- User remains ACTIVE in both groups
- Source group seats reduced
- Target group seats increased

**Full Transfer:**

- Transfer all seats from source
- Source participant status → TRANSFERRED\_OUT
- Source group participants count decreased
- If source group becomes empty → soft deleted

---

## Group Status Definitions

<table id="bkmrk-status-description-c"><thead><tr><th>Status</th><th>Description</th><th>Can Join?</th><th>Can Transfer From?</th><th>Can Transfer To?</th></tr></thead><tbody><tr><td>OPEN</td><td>Active, accepting participants</td><td>Yes</td><td>Yes</td><td>Yes</td></tr><tr><td>COMPLETED</td><td>All seats filled, orders created</td><td>No</td><td>No</td><td>No</td></tr><tr><td>FAILED</td><td>Expired without filling, refunds issued</td><td>No</td><td>No</td><td>No</td></tr><tr><td>DELETED</td><td>Soft deleted (empty or admin action)</td><td>No</td><td>No</td><td>No</td></tr></tbody></table>

---

## Participant Status Definitions

<table id="bkmrk-status-description-s"><thead><tr><th>Status</th><th>Description</th><th>Still in Group?</th><th>Can Buy More?</th><th>Can Transfer?</th></tr></thead><tbody><tr><td>ACTIVE</td><td>Currently participating in group</td><td>Yes</td><td>Yes</td><td>Yes</td></tr><tr><td>TRANSFERRED\_OUT</td><td>Left this group via transfer</td><td>No</td><td>No</td><td>No</td></tr><tr><td>REFUNDED</td><td>Group failed, money refunded</td><td>No</td><td>No</td><td>No</td></tr></tbody></table>

---

## Purchase History Tracking

Each participant maintains detailed purchase history:

**Tracked Information:**

- Checkout session ID
- Quantity purchased
- Amount paid
- Purchase timestamp
- Transaction ID

**Use Cases:**

- User buys 2 seats initially
- User joins same group again, buys 3 more seats
- Purchase history shows 2 separate purchases
- Total quantity: 5 seats
- Total paid: sum of both purchases

---

## Transfer History Tracking

Each transfer is recorded in participant history:

**Tracked Information:**

- Source group ID and code
- Target group ID and code
- Transfer timestamp
- Transfer reason/description

**Transfer Scenarios:**

**Scenario 1: Partial Transfer**

- User has 5 seats in Group A
- Transfers 2 seats to Group B
- Group A participant: 3 seats, ACTIVE status
- Group B participant: 2 seats, ACTIVE status, transfer history added

**Scenario 2: Full Transfer**

- User has 3 seats in Group A
- Transfers all 3 seats to Group B
- Group A participant: 0 seats, TRANSFERRED\_OUT status
- Group B participant: 3 seats, ACTIVE status, transfer history added

**Scenario 3: Multiple Transfers**

- User transfers from Group A to Group B
- Later transfers from Group B to Group C
- Full transfer history maintained in each participation

---

## Group Expiration and Cleanup

### Automatic Expiration

**Scheduled Job runs periodically to:**

1. Find groups with status=OPEN and expiresAt &lt; now
2. Change status to FAILED
3. Initiate refunds for all participants
4. Update participant status to REFUNDED
5. Release inventory holds

### Soft Deletion

**Groups are soft-deleted when:**

1. All participants transfer out (empty group)
2. Admin manually deletes group

**Soft Delete Actions:**

- `isDeleted` set to true
- `status` changed to DELETED
- `deletedAt` timestamp recorded
- `deletedBy` user ID recorded
- `deleteReason` stored
- Group still queryable but excluded from active lists

---

## Business Rules

### Maximum Seats Per Customer

If product has `maxPerCustomer` limit:

- Single purchase cannot exceed limit
- Multiple purchases in same group respect limit
- Transfer to group validates combined quantity

**Example:**

- Product has maxPerCustomer = 5
- User has 3 seats in Group A
- Tries to transfer to Group B where they have 3 seats
- Transfer rejected (3 + 3 = 6 &gt; 5)

### Group Price Lock

Group price is snapshot at creation:

- Price stored in group instance
- Transfers validate price match
- Product price changes don't affect existing groups

### Inventory Management

**During Group Lifecycle:**

- Seats held in inventory when purchased
- Holds maintained until group completes or fails
- Completed: inventory permanently deducted
- Failed: inventory holds released

**Transfer Impact:**

- No inventory change during transfer
- Total inventory hold remains same
- Just moves between groups

---

## Integration with Checkout Sessions

### Creating New Group

**Checkout Session Requirements:**

- `sessionType: GROUP_PURCHASE`
- Exactly 1 item
- WALLET payment only
- No `groupInstanceId` in metadata
- Status: PAYMENT\_COMPLETED

**After Payment Success:**

```java
GroupPurchaseInstanceEntity group = groupPurchaseService.createGroupInstance(checkoutSession);

```

### Joining Existing Group

**Checkout Session Requirements:**

- `sessionType: GROUP_PURCHASE`
- Exactly 1 item
- WALLET payment only
- `groupInstanceId` in metadata
- Status: PAYMENT\_COMPLETED

**After Payment Success:**

```java
UUID groupId = (UUID) checkoutSession.getMetadata().get("groupInstanceId");
GroupPurchaseInstanceEntity group = groupPurchaseService.joinGroup(groupId, checkoutSession);

```

---

## Error Handling Best Practices

### Common Error Scenarios

**Product Not Available for Group Buying:**

```json
{
  "success": false,
  "httpStatus": "BAD_REQUEST",
  "message": "Group buying is not enabled for this product"
}

```

**Action:** Check product has `groupBuyingEnabled: true`

**Group Expired:**

```json
{
  "success": false,
  "httpStatus": "BAD_REQUEST",
  "message": "Group has expired at: 2025-10-02T20:30:45"
}

```

**Action:** Find another available group or create new group

**Group Full:**

```json
{
  "success": false,
  "httpStatus": "BAD_REQUEST",
  "message": "Group is full. Seats occupied: 10/10"
}

```

**Action:** Find another available group or create new group

**Quantity Exceeds Group Size:**

```json
{
  "success": false,
  "httpStatus": "BAD_REQUEST",
  "message": "Quantity (8) exceeds group max size (5)"
}

```

**Action:** Reduce quantity or create multiple purchases

**Transfer Between Incompatible Groups:**

```json
{
  "success": false,
  "httpStatus": "BAD_REQUEST",
  "message": "Cannot transfer between groups from different shops"
}

```

**Action:** Only transfer between compatible groups

---

## Quick Reference Guide

### Endpoint Summary

<table id="bkmrk-endpoint-method-purp"><thead><tr><th>Endpoint</th><th>Method</th><th>Purpose</th></tr></thead><tbody><tr><td>`/group-purchases/product/{productId}/available`</td><td>GET</td><td>Get available groups for product</td></tr><tr><td>`/group-purchases/{groupId}`</td><td>GET</td><td>Get group details by ID</td></tr><tr><td>`/group-purchases/code/{groupCode}`</td><td>GET</td><td>Get group details by code</td></tr><tr><td>`/group-purchases/my-groups`</td><td>GET</td><td>Get user's groups</td></tr><tr><td>`/group-purchases/my-participations`</td><td>GET</td><td>Get user's participations</td></tr><tr><td>`/group-purchases/transfer`</td><td>POST</td><td>Transfer seats between groups</td></tr></tbody></table>

### Common HTTP Status Codes

- `200 OK`: Successful operation
- `400 Bad Request`: Validation error or business rule violation
- `401 Unauthorized`: Authentication required
- `404 Not Found`: Group, product, or participation not found

### Group Code Format

- Pattern: `GP-XXXXXX`
- Length: 9 characters (GP- + 6 alphanumeric)
- Example: `GP-A3X7K9`, `GP-B2Y8M5`
- Auto-generated at group creation

### Participant Contribution Calculation

```
contributionPercentage = (participantQuantity / totalSeatsOccupied) × 100

```

### Progress Calculation

```
progressPercentage = (seatsOccupied / totalSeats) × 100

```

### Savings Calculation

```
savingsAmount = regularPrice - groupPrice
savingsPercentage = (savingsAmount / regularPrice) × 100

```

---

## Testing

### Test Scenarios

**Scenario 1: Create and Complete Group**

1. User A creates group (2 seats)
2. User B joins group (3 seats)
3. User C joins group (5 seats)
4. Group auto-completes
5. Orders created for all participants

**Scenario 2: Transfer Between Groups**

1. User A in Group 1 (3 seats)
2. User A transfers 2 seats to Group 2
3. User A remains in Group 1 (1 seat)
4. User A now in Group 2 (2 seats)

**Scenario 3: Group Expiration**

1. Create group with 1-minute expiration
2. Wait for expiration
3. Scheduled job processes
4. Status → FAILED
5. Participants refunded

**Scenario 4: Hybrid Approach - Multiple Purchases**

1. User A creates group (2 seats)
2. User A joins same group again (3 seats)
3. User A total: 5 seats
4. Purchase history shows 2 records

---

**© 2025 NexGate. All rights reserved.**

# Installment Purchase

Flexible payment plans with 7+ frequencies, custom intervals, and 2-4 plans per product. Features transparent amortization schedules, 75% early payoff interest discount, and two fulfillment options (immediate or after payment). Includes configurable 0-60 day grace periods, automated payment processing via JobRunr, up to 5 retry attempts for failed payments, real-time tracking of payment history and agreement status, and full admin control for plan management.

# Installment Purchase - Customer Endpoints

**Author**: Josh S. Sakweli, Backend Lead Team  
**Last Updated**: 2025-10-18  
**Version**: v1.0

**Base URL**: `https://api.nextgate.com/api/v1/installments`

**Short Description**: This API provides endpoints for customers to manage their installment purchase agreements. Customers can view their agreements, track payment schedules, make manual payments, retry failed payments, calculate and process early payoffs, and cancel agreements. All endpoints require authentication and operate on the principle that customers can only access their own agreement data.

**Hints**: 
- All customer endpoints require authentication via Bearer token
- Customers can only access their own agreements (ownership validation applied)
- Payment processing uses wallet-based transactions
- Early payoff provides 75% discount on remaining interest
- Agreements can only be cancelled before the first payment is completed
- Failed payments can be retried up to 5 times
- Grace periods apply before first payment due date
- All amounts are in TZS (Tanzanian Shillings)

---

## Standard Response Format

All API responses follow a consistent structure using our Globe Response Builder pattern:

### Success Response Structure
```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Operation completed successfully",
  "action_time": "2025-10-18T10:30:45",
  "data": {
    // Actual response data goes here
  }
}
```

### Error Response Structure
```json
{
  "success": false,
  "httpStatus": "BAD_REQUEST",
  "message": "Error description",
  "action_time": "2025-10-18T10:30:45",
  "data": "Error description"
}
```

### Standard Response Fields
| Field | Type | Description |
|-------|------|-------------|
| `success` | boolean | Always `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 operation result |
| `action_time` | string | ISO 8601 timestamp of when the response was generated |
| `data` | object/string | Response payload for success, error details for failures |

---

## HTTP Method Badge Standards

For better visual clarity, all endpoints use colored badges for HTTP methods with the following standard colors:

- **GET** - <span style="background-color: #28a745; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">GET</span> - Green (Safe, read-only operations)
- **POST** - <span style="background-color: #007bff; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">POST</span> - Blue (Create new resources)
- **DELETE** - <span style="background-color: #dc3545; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">DELETE</span> - Red (Remove resources)

---

## Endpoints

## 1. Get My Agreements
**Purpose**: Retrieve all installment agreements for the authenticated customer, optionally filtered by status

**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}/my-agreements`

**Access Level**: 🔒 Protected (Requires Authentication)

**Authentication**: Bearer Token

**Request Headers**:
| Header | Type | Required | Description |
|--------|------|----------|-------------|
| Authorization | string | Yes | Bearer token for authentication |

**Query Parameters**:
| Parameter | Type | Required | Description | Validation | Default |
|-----------|------|----------|-------------|------------|---------|
| status | string | No | Filter by agreement status | enum: PENDING_FIRST_PAYMENT, ACTIVE, COMPLETED, DEFAULTED, CANCELLED | null (all statuses) |

**Success Response JSON Sample**:
```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Agreements retrieved successfully",
  "action_time": "2025-10-18T10:30:45",
  "data": [
    {
      "agreementId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
      "agreementNumber": "INST-2025-12345",
      "productId": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
      "productName": "Samsung Galaxy S24 Ultra",
      "productImage": "https://cdn.example.com/products/samsung-s24.jpg",
      "shopId": "8d3a7b12-9c4e-4f8a-b5d2-3e6f7a8b9c0d",
      "shopName": "Tech World Store",
      "totalAmount": 2500000.00,
      "amountPaid": 500000.00,
      "amountRemaining": 2000000.00,
      "currency": "TZS",
      "paymentsCompleted": 1,
      "paymentsRemaining": 11,
      "totalPayments": 12,
      "progressPercentage": 20.0,
      "nextPaymentDate": "2025-11-18T00:00:00",
      "nextPaymentAmount": 166666.67,
      "agreementStatus": "ACTIVE",
      "agreementStatusDisplay": "ACTIVE",
      "createdAt": "2025-10-18T09:00:00",
      "completedAt": null,
      "canMakeEarlyPayment": true,
      "canCancel": false
    }
  ]
}
```

**Success Response Fields**:
| Field | Description |
|-------|-------------|
| agreementId | Unique identifier for the agreement |
| agreementNumber | Human-readable agreement number (format: INST-YYYY-NNNNN) |
| productId | ID of the product being purchased |
| productName | Name of the product |
| productImage | URL to product image |
| shopId | ID of the shop selling the product |
| shopName | Name of the shop |
| totalAmount | Total amount to be paid including interest |
| amountPaid | Amount paid so far |
| amountRemaining | Amount still owed |
| currency | Currency code (TZS) |
| paymentsCompleted | Number of payments made |
| paymentsRemaining | Number of payments left |
| totalPayments | Total number of scheduled payments |
| progressPercentage | Percentage of payments completed |
| nextPaymentDate | Date when next payment is due |
| nextPaymentAmount | Amount of next payment |
| agreementStatus | Current status of the agreement |
| agreementStatusDisplay | Human-readable status |
| createdAt | When agreement was created |
| completedAt | When agreement was completed (null if not completed) |
| canMakeEarlyPayment | Whether early payoff is allowed |
| canCancel | Whether agreement can be cancelled |

**Error Response JSON Sample**:
```json
{
  "success": false,
  "httpStatus": "UNAUTHORIZED",
  "message": "Token has expired",
  "action_time": "2025-10-18T10:30:45",
  "data": "Token has expired"
}
```

**Standard Error Types**:

### Application-Level Exceptions (400-499)
- `401 UNAUTHORIZED`: Authentication issues (empty, invalid, expired, or malformed tokens)
- `404 NOT_FOUND`: User not found or not authenticated

**Error Response Examples**:

*Unauthorized - Token Issues (401):*
```json
{
  "success": false,
  "httpStatus": "UNAUTHORIZED",
  "message": "Token has expired",
  "action_time": "2025-10-18T10:30:45",
  "data": "Token has expired"
}
```

*Not Found (404):*
```json
{
  "success": false,
  "httpStatus": "NOT_FOUND",
  "message": "User not found",
  "action_time": "2025-10-18T10:30:45",
  "data": "User not found"
}
```

---

## 2. Get My Active Agreements
**Purpose**: Retrieve only active installment agreements for the authenticated customer

**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}/my-agreements/active`

**Access Level**: 🔒 Protected (Requires Authentication)

**Authentication**: Bearer Token

**Request Headers**:
| Header | Type | Required | Description |
|--------|------|----------|-------------|
| Authorization | string | Yes | Bearer token for authentication |

**Success Response JSON Sample**:
```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Active agreements retrieved successfully",
  "action_time": "2025-10-18T10:30:45",
  "data": [
    {
      "agreementId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
      "agreementNumber": "INST-2025-12345",
      "productId": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
      "productName": "Samsung Galaxy S24 Ultra",
      "productImage": "https://cdn.example.com/products/samsung-s24.jpg",
      "shopId": "8d3a7b12-9c4e-4f8a-b5d2-3e6f7a8b9c0d",
      "shopName": "Tech World Store",
      "totalAmount": 2500000.00,
      "amountPaid": 500000.00,
      "amountRemaining": 2000000.00,
      "currency": "TZS",
      "paymentsCompleted": 1,
      "paymentsRemaining": 11,
      "totalPayments": 12,
      "progressPercentage": 20.0,
      "nextPaymentDate": "2025-11-18T00:00:00",
      "nextPaymentAmount": 166666.67,
      "agreementStatus": "ACTIVE",
      "agreementStatusDisplay": "ACTIVE",
      "createdAt": "2025-10-18T09:00:00",
      "completedAt": null,
      "canMakeEarlyPayment": true,
      "canCancel": false
    }
  ]
}
```

**Success Response Fields**:
Same as Get My Agreements endpoint

**Error Response Examples**:
Same error types as Get My Agreements endpoint

---

## 3. Get Agreement By ID
**Purpose**: Retrieve detailed information about a specific installment agreement by its ID

**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}/agreements/{agreementId}`

**Access Level**: 🔒 Protected (Requires Authentication, Owner Only)

**Authentication**: Bearer Token

**Request Headers**:
| Header | Type | Required | Description |
|--------|------|----------|-------------|
| Authorization | string | Yes | Bearer token for authentication |

**Path Parameters**:
| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| agreementId | UUID | Yes | Unique identifier of the agreement | Valid UUID format |

**Success Response JSON Sample**:
```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Agreement details retrieved successfully",
  "action_time": "2025-10-18T10:30:45",
  "data": {
    "agreementId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
    "agreementNumber": "INST-2025-12345",
    "customerId": "9b2e4d56-7c8a-4f9b-a3d1-5e6f7a8b9c0d",
    "customerName": "John Doe",
    "customerEmail": "john.doe@example.com",
    "productId": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
    "productName": "Samsung Galaxy S24 Ultra",
    "productImage": "https://cdn.example.com/products/samsung-s24.jpg",
    "productPrice": 2000000.00,
    "quantity": 1,
    "shopId": "8d3a7b12-9c4e-4f8a-b5d2-3e6f7a8b9c0d",
    "shopName": "Tech World Store",
    "selectedPlanId": "4b5c6d7e-8f9a-4b1c-9d2e-3f4a5b6c7d8e",
    "planName": "12 Month Standard Plan",
    "paymentFrequency": "MONTHLY",
    "paymentFrequencyDisplay": "Monthly",
    "customFrequencyDays": null,
    "numberOfPayments": 12,
    "duration": "12 months",
    "apr": 15.00,
    "gracePeriodDays": 30,
    "downPaymentAmount": 400000.00,
    "financedAmount": 1600000.00,
    "monthlyPaymentAmount": 166666.67,
    "totalInterestAmount": 400000.00,
    "totalAmount": 2400000.00,
    "currency": "TZS",
    "paymentsCompleted": 1,
    "paymentsRemaining": 11,
    "amountPaid": 566666.67,
    "amountRemaining": 1833333.33,
    "progressPercentage": 8.33,
    "nextPaymentDate": "2025-11-18T00:00:00",
    "nextPaymentAmount": 166666.67,
    "agreementStatus": "ACTIVE",
    "defaultCount": 0,
    "createdAt": "2025-10-18T09:00:00",
    "firstPaymentDate": "2025-11-18T00:00:00",
    "lastPaymentDate": "2026-10-18T00:00:00",
    "completedAt": null,
    "fulfillmentTiming": "IMMEDIATE",
    "shippedAt": "2025-10-18T14:00:00",
    "deliveredAt": null,
    "orderId": "1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d",
    "shippingAddress": {
      "fullName": "John Doe",
      "phoneNumber": "+255712345678",
      "street": "123 Main Street",
      "city": "Dar es Salaam",
      "state": "Dar es Salaam",
      "postalCode": "12345",
      "country": "Tanzania"
    },
    "billingAddress": {
      "fullName": "John Doe",
      "phoneNumber": "+255712345678",
      "street": "123 Main Street",
      "city": "Dar es Salaam",
      "state": "Dar es Salaam",
      "postalCode": "12345",
      "country": "Tanzania"
    },
    "payments": [
      {
        "paymentId": "5c6d7e8f-9a0b-4c1d-8e2f-3a4b5c6d7e8f",
        "paymentNumber": 1,
        "scheduledAmount": 166666.67,
        "paidAmount": 166666.67,
        "principalPortion": 146666.67,
        "interestPortion": 20000.00,
        "remainingBalance": 1453333.33,
        "lateFee": null,
        "currency": "TZS",
        "paymentStatus": "COMPLETED",
        "paymentStatusDisplay": "COMPLETED",
        "dueDate": "2025-11-18T00:00:00",
        "paidAt": "2025-11-18T10:30:00",
        "attemptedAt": "2025-11-18T10:29:45",
        "paymentMethod": "WALLET",
        "transactionId": "TXN-2025-67890",
        "failureReason": null,
        "retryCount": 0,
        "daysUntilDue": null,
        "daysOverdue": null,
        "canPay": false,
        "canRetry": false
      }
    ],
    "canMakeEarlyPayment": true,
    "canCancel": false,
    "canUpdatePaymentMethod": false
  }
}
```

**Success Response Fields**:
| Field | Description |
|-------|-------------|
| agreementId | Unique identifier for the agreement |
| agreementNumber | Human-readable agreement number |
| customerId | ID of the customer |
| customerName | Customer's full name |
| customerEmail | Customer's email address |
| productId | ID of the product |
| productName | Name of the product |
| productImage | URL to product image |
| productPrice | Original product price |
| quantity | Quantity purchased |
| shopId | ID of the shop |
| shopName | Name of the shop |
| selectedPlanId | ID of the selected installment plan |
| planName | Name of the installment plan |
| paymentFrequency | Payment frequency enum value |
| paymentFrequencyDisplay | Human-readable payment frequency |
| customFrequencyDays | Custom frequency in days (if applicable) |
| numberOfPayments | Total number of payments |
| duration | Human-readable duration |
| apr | Annual Percentage Rate |
| gracePeriodDays | Grace period before first payment |
| downPaymentAmount | Down payment amount |
| financedAmount | Amount being financed |
| monthlyPaymentAmount | Amount per payment |
| totalInterestAmount | Total interest to be paid |
| totalAmount | Grand total (product + interest) |
| currency | Currency code |
| paymentsCompleted | Number of completed payments |
| paymentsRemaining | Number of remaining payments |
| amountPaid | Total amount paid so far |
| amountRemaining | Total amount remaining |
| progressPercentage | Percentage of completion |
| nextPaymentDate | Next payment due date |
| nextPaymentAmount | Next payment amount |
| agreementStatus | Current agreement status |
| defaultCount | Number of missed payments |
| createdAt | Agreement creation timestamp |
| firstPaymentDate | First payment due date |
| lastPaymentDate | Last payment due date |
| completedAt | Completion timestamp (null if not completed) |
| fulfillmentTiming | When product is shipped (IMMEDIATE or AFTER_PAYMENT) |
| shippedAt | Shipment timestamp |
| deliveredAt | Delivery timestamp |
| orderId | Associated order ID |
| shippingAddress | Shipping address object |
| billingAddress | Billing address object |
| payments | Array of payment objects with full details |
| canMakeEarlyPayment | Whether early payoff is allowed |
| canCancel | Whether agreement can be cancelled |
| canUpdatePaymentMethod | Whether payment method can be updated |

**Error Response Examples**:

*Bad Request (400):*
```json
{
  "success": false,
  "httpStatus": "BAD_REQUEST",
  "message": "You do not have access to this agreement",
  "action_time": "2025-10-18T10:30:45",
  "data": "You do not have access to this agreement"
}
```

*Not Found (404):*
```json
{
  "success": false,
  "httpStatus": "NOT_FOUND",
  "message": "Agreement not found with ID: 3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "action_time": "2025-10-18T10:30:45",
  "data": "Agreement not found with ID: 3fa85f64-5717-4562-b3fc-2c963f66afa6"
}
```

---

## 4. Get Agreement By Number
**Purpose**: Retrieve detailed information about a specific installment agreement by its agreement number

**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}/agreements/number/{agreementNumber}`

**Access Level**: 🔒 Protected (Requires Authentication, Owner Only)

**Authentication**: Bearer Token

**Request Headers**:
| Header | Type | Required | Description |
|--------|------|----------|-------------|
| Authorization | string | Yes | Bearer token for authentication |

**Path Parameters**:
| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| agreementNumber | string | Yes | Agreement number (format: INST-YYYY-NNNNN) | Pattern: INST-\d{4}-\d{5} |

**Success Response JSON Sample**:
Same as Get Agreement By ID endpoint

**Success Response Fields**:
Same as Get Agreement By ID endpoint

**Error Response Examples**:
Same as Get Agreement By ID endpoint, with agreement number in error messages instead of ID

---

## 5. Get Agreement Payments
**Purpose**: Retrieve all payment records for a specific installment agreement

**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}/agreements/{agreementId}/payments`

**Access Level**: 🔒 Protected (Requires Authentication, Owner Only)

**Authentication**: Bearer Token

**Request Headers**:
| Header | Type | Required | Description |
|--------|------|----------|-------------|
| Authorization | string | Yes | Bearer token for authentication |

**Path Parameters**:
| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| agreementId | UUID | Yes | Unique identifier of the agreement | Valid UUID format |

**Success Response JSON Sample**:
```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Payment history retrieved successfully",
  "action_time": "2025-10-18T10:30:45",
  "data": [
    {
      "paymentId": "5c6d7e8f-9a0b-4c1d-8e2f-3a4b5c6d7e8f",
      "paymentNumber": 1,
      "scheduledAmount": 166666.67,
      "paidAmount": 166666.67,
      "principalPortion": 146666.67,
      "interestPortion": 20000.00,
      "remainingBalance": 1453333.33,
      "lateFee": null,
      "currency": "TZS",
      "paymentStatus": "COMPLETED",
      "paymentStatusDisplay": "COMPLETED",
      "dueDate": "2025-11-18T00:00:00",
      "paidAt": "2025-11-18T10:30:00",
      "attemptedAt": "2025-11-18T10:29:45",
      "paymentMethod": "WALLET",
      "transactionId": "TXN-2025-67890",
      "failureReason": null,
      "retryCount": 0,
      "daysUntilDue": null,
      "daysOverdue": null,
      "canPay": false,
      "canRetry": false
    },
    {
      "paymentId": "6d7e8f9a-0b1c-4d2e-9f3a-4b5c6d7e8f9a",
      "paymentNumber": 2,
      "scheduledAmount": 166666.67,
      "paidAmount": null,
      "principalPortion": 148533.33,
      "interestPortion": 18133.34,
      "remainingBalance": 1304800.00,
      "lateFee": null,
      "currency": "TZS",
      "paymentStatus": "SCHEDULED",
      "paymentStatusDisplay": "SCHEDULED",
      "dueDate": "2025-12-18T00:00:00",
      "paidAt": null,
      "attemptedAt": null,
      "paymentMethod": null,
      "transactionId": null,
      "failureReason": null,
      "retryCount": 0,
      "daysUntilDue": 61,
      "daysOverdue": null,
      "canPay": false,
      "canRetry": false
    }
  ]
}
```

**Success Response Fields**:
| Field | Description |
|-------|-------------|
| paymentId | Unique identifier for the payment |
| paymentNumber | Sequential payment number (1, 2, 3...) |
| scheduledAmount | Amount scheduled to be paid |
| paidAmount | Amount actually paid (null if not paid) |
| principalPortion | Amount going toward principal |
| interestPortion | Amount going toward interest |
| remainingBalance | Balance after this payment |
| lateFee | Late fee if applicable |
| currency | Currency code |
| paymentStatus | Current payment status |
| paymentStatusDisplay | Human-readable status |
| dueDate | When payment is due |
| paidAt | When payment was made (null if not paid) |
| attemptedAt | Last payment attempt timestamp |
| paymentMethod | Payment method used |
| transactionId | Transaction reference ID |
| status | Payment status after processing |
| processedAt | When payment was processed |
| message | Success message |
| agreementUpdate | Updated agreement information |
| agreementUpdate.paymentsCompleted | Updated number of completed payments |
| agreementUpdate.paymentsRemaining | Updated number of remaining payments |
| agreementUpdate.amountPaid | Updated total amount paid |
| agreementUpdate.amountRemaining | Updated remaining amount |
| agreementUpdate.nextPaymentDate | Next payment due date |
| agreementUpdate.nextPaymentAmount | Next payment amount |
| agreementUpdate.agreementStatus | Updated agreement status |
| agreementUpdate.isCompleted | Whether agreement is now completed |

**Error Response Examples**:

*Bad Request - Insufficient Balance (400):*
```json
{
  "success": false,
  "httpStatus": "BAD_REQUEST",
  "message": "Insufficient wallet balance. Required: 166666.67 TZS, Available: 50000.00 TZS. Please top up your wallet before the next payment attempt.",
  "action_time": "2025-10-18T10:30:45",
  "data": "Insufficient wallet balance. Required: 166666.67 TZS, Available: 50000.00 TZS. Please top up your wallet before the next payment attempt."
}
```

*Bad Request - Payment Already Completed (400):*
```json
{
  "success": false,
  "httpStatus": "BAD_REQUEST",
  "message": "Payment is already completed",
  "action_time": "2025-10-18T10:30:45",
  "data": "Payment is already completed"
}
```

*Bad Request - Payment Not Due (400):*
```json
{
  "success": false,
  "httpStatus": "BAD_REQUEST",
  "message": "Payment is not due yet. Due date: 2025-12-18T00:00:00",
  "action_time": "2025-10-18T10:30:45",
  "data": "Payment is not due yet. Due date: 2025-12-18T00:00:00"
}
```

---


## 6. Get Upcoming Payments
**Purpose**: Retrieve all upcoming payments across all active agreements for the authenticated customer

**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}/upcoming-payments`

**Access Level**: 🔒 Protected (Requires Authentication)

**Authentication**: Bearer Token

**Request Headers**:
| Header | Type | Required | Description |
|--------|------|----------|-------------|
| Authorization | string | Yes | Bearer token for authentication |

**Success Response JSON Sample**:
```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Upcoming payments retrieved successfully",
  "action_time": "2025-10-18T10:30:45",
  "data": [
    {
      "paymentId": "6d7e8f9a-0b1c-4d2e-9f3a-4b5c6d7e8f9a",
      "paymentNumber": 2,
      "scheduledAmount": 166666.67,
      "paidAmount": null,
      "principalPortion": 148533.33,
      "interestPortion": 18133.34,
      "remainingBalance": 1304800.00,
      "lateFee": null,
      "currency": "TZS",
      "paymentStatus": "SCHEDULED",
      "paymentStatusDisplay": "SCHEDULED",
      "dueDate": "2025-12-18T00:00:00",
      "paidAt": null,
      "attemptedAt": null,
      "paymentMethod": null,
      "transactionId": null,
      "failureReason": null,
      "retryCount": 0,
      "daysUntilDue": 61,
      "daysOverdue": null,
      "canPay": false,
      "canRetry": false
    }
  ]
}
```

**Success Response Fields**:
Same as Get Agreement Payments endpoint

**Error Response Examples**:
Standard authentication errors only

---

## 7. Make Manual Payment
**Purpose**: Process a manual payment for a specific scheduled installment

**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}/agreements/{agreementId}/payments/{paymentId}/pay`

**Access Level**: 🔒 Protected (Requires Authentication, Owner Only)

**Authentication**: Bearer Token

**Request Headers**:
| Header | Type | Required | Description |
|--------|------|----------|-------------|
| Authorization | string | Yes | Bearer token for authentication |

**Path Parameters**:
| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| agreementId | UUID | Yes | Unique identifier of the agreement | Valid UUID format |
| paymentId | UUID | Yes | Unique identifier of the payment | Valid UUID format |

**Success Response JSON Sample**:
```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Payment processed successfully",
  "action_time": "2025-10-18T10:30:45",
  "data": {
    "paymentId": "6d7e8f9a-0b1c-4d2e-9f3a-4b5c6d7e8f9a",
    "agreementId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
    "agreementNumber": "INST-2025-12345",
    "amount": 166666.67,
    "currency": "TZS",
    "paymentMethod": "WALLET",
    "transactionId": "TXN-2025-67891",
    "status": "COMPLETED",
    "processedAt": "2025-10-18T10:30:45",
    "message": "Payment processed successfully",
    "agreementUpdate": {
      "paymentsCompleted": 2,
      "paymentsRemaining": 10,
      "amountPaid": 733333.34,
      "amountRemaining": 1666666.66,
      "nextPaymentDate": "2026-01-18T00:00:00",
      "nextPaymentAmount": 166666.67,
      "agreementStatus": "ACTIVE",
      "isCompleted": false
    }
  }
}
```

**Success Response Fields**:
| Field | Description |
|-------|-------------|
| paymentId | ID of the processed payment |
| agreementId | ID of the agreement |
| agreementNumber | Agreement number |
| amount | Amount paid |
| currency | Currency code |
| paymentMethod | Payment method used |
| transaction

## 8. Retry Failed Payment
**Purpose**: Retry a failed payment that has not exceeded maximum retry attempts

**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}/payments/{paymentId}/retry`

**Access Level**: 🔒 Protected (Requires Authentication, Owner Only)

**Authentication**: Bearer Token

**Request Headers**:
| Header | Type | Required | Description |
|--------|------|----------|-------------|
| Authorization | string | Yes | Bearer token for authentication |

**Path Parameters**:
| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| paymentId | UUID | Yes | Unique identifier of the payment to retry | Valid UUID format |

**Success Response JSON Sample**:
```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Payment retry processed successfully",
  "action_time": "2025-10-18T10:30:45",
  "data": {
    "paymentId": "6d7e8f9a-0b1c-4d2e-9f3a-4b5c6d7e8f9a",
    "agreementId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
    "agreementNumber": "INST-2025-12345",
    "amount": 166666.67,
    "currency": "TZS",
    "paymentMethod": "WALLET",
    "transactionId": "TXN-2025-67892",
    "status": "COMPLETED",
    "processedAt": "2025-10-18T10:30:45",
    "message": "Payment retry successful",
    "agreementUpdate": {
      "paymentsCompleted": 2,
      "paymentsRemaining": 10,
      "amountPaid": 733333.34,
      "amountRemaining": 1666666.66,
      "nextPaymentDate": "2026-01-18T00:00:00",
      "nextPaymentAmount": 166666.67,
      "agreementStatus": "ACTIVE",
      "isCompleted": false
    }
  }
}
```

**Success Response Fields**:
Same as Make Manual Payment endpoint

**Error Response Examples**:

*Bad Request - Cannot Retry (400):*
```json
{
  "success": false,
  "httpStatus": "BAD_REQUEST",
  "message": "Payment cannot be retried",
  "action_time": "2025-10-18T10:30:45",
  "data": "Payment cannot be retried"
}
```

*Bad Request - Max Retries Exceeded (400):*
```json
{
  "success": false,
  "httpStatus": "BAD_REQUEST",
  "message": "Maximum retry attempts (5) exceeded",
  "action_time": "2025-10-18T10:30:45",
  "data": "Maximum retry attempts (5) exceeded"
}
```

---
## 9. Preview Flexible Payment
**Purpose**: Calculate and preview how a flexible payment amount will be distributed across installment payments before processing

**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}/installments/agreements/{agreementId}/early-flexible-payment/preview`

**Access Level**: 🔒 Protected (Requires Authentication)

**Authentication**: Bearer Token (JWT)

**Request Headers**:
| Header | Type | Required | Description |
|--------|------|----------|-------------|
| Authorization | string | Yes | Bearer token for authentication |
| Content-Type | string | Yes | Must be `application/json` |

**Path Parameters**:
| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| agreementId | UUID | Yes | The ID of the installment agreement | Must be a valid UUID, agreement must belong to authenticated user |

**Request JSON Sample**:
```json
{
  "amount": 500000.00
}
```

**Request Body Parameters**:
| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| amount | decimal | Yes | Amount the customer wants to pay | Must be > 0, cannot exceed remaining balance |

**Success Response JSON Sample**:
```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Flexible payment preview calculated successfully",
  "action_time": "2025-11-06T10:30:45",
  "data": {
    "requestedAmount": 500000.00,
    "minimumRequired": 150000.00,
    "maximumAllowed": 1200000.00,
    "isValid": true,
    "validationMessage": null,
    "impactedPayments": [
      {
        "paymentNumber": 3,
        "dueDate": "2025-12-01T00:00:00",
        "scheduledAmount": 200000.00,
        "currentPaid": 50000.00,
        "willApply": 150000.00,
        "willRemain": 0.00,
        "resultStatus": "Will be COMPLETED"
      },
      {
        "paymentNumber": 4,
        "dueDate": "2026-01-01T00:00:00",
        "scheduledAmount": 200000.00,
        "currentPaid": 0.00,
        "willApply": 200000.00,
        "willRemain": 0.00,
        "resultStatus": "Will be COMPLETED"
      },
      {
        "paymentNumber": 5,
        "dueDate": "2026-02-01T00:00:00",
        "scheduledAmount": 200000.00,
        "currentPaid": 0.00,
        "willApply": 150000.00,
        "willRemain": 50000.00,
        "resultStatus": "Will be PARTIALLY_PAID"
      }
    ],
    "paymentsWillComplete": 2,
    "paymentsWillBePartial": 1,
    "remainingAfter": 700000.00
  }
}
```

**Success Response Fields**:
| Field | Description |
|-------|-------------|
| requestedAmount | The amount customer wants to pay |
| minimumRequired | Minimum payment required (next incomplete payment amount) |
| maximumAllowed | Maximum payment allowed (total remaining balance) |
| isValid | Whether the requested amount is valid |
| validationMessage | Error message if amount is invalid, null if valid |
| impactedPayments | Array of payments that will be affected by this payment |
| impactedPayments[].paymentNumber | Sequential payment number |
| impactedPayments[].dueDate | When this payment is due |
| impactedPayments[].scheduledAmount | Original scheduled amount for this payment |
| impactedPayments[].currentPaid | Amount already paid towards this payment |
| impactedPayments[].willApply | How much of the flexible payment will apply here |
| impactedPayments[].willRemain | How much will remain unpaid after applying |
| impactedPayments[].resultStatus | Final status: "Will be COMPLETED" or "Will be PARTIALLY_PAID" |
| paymentsWillComplete | Number of payments that will be fully completed |
| paymentsWillBePartial | Number of payments that will be partially paid |
| remainingAfter | Total remaining balance after this payment |

**Error Response Examples**:

*Bad Request - Amount Too Low (400):*
```json
{
  "success": false,
  "httpStatus": "BAD_REQUEST",
  "message": "Minimum payment required: 150000.00 TZS",
  "action_time": "2025-11-06T10:30:45",
  "data": "Minimum payment required: 150000.00 TZS"
}
```

*Bad Request - Amount Too High (400):*
```json
{
  "success": false,
  "httpStatus": "BAD_REQUEST",
  "message": "Payment amount exceeds remaining balance. Use early payoff endpoint if paying off completely.",
  "action_time": "2025-11-06T10:30:45",
  "data": "Payment amount exceeds remaining balance. Use early payoff endpoint if paying off completely."
}
```

*Forbidden - Not Agreement Owner (403):*
```json
{
  "success": false,
  "httpStatus": "FORBIDDEN",
  "message": "You do not have access to this agreement",
  "action_time": "2025-11-06T10:30:45",
  "data": "You do not have access to this agreement"
}
```

*Not Found - Agreement Not Found (404):*
```json
{
  "success": false,
  "httpStatus": "NOT_FOUND",
  "message": "Agreement not found",
  "action_time": "2025-11-06T10:30:45",
  "data": "Agreement not found"
}
```

*Validation Error - Invalid Amount (422):*
```json
{
  "success": false,
  "httpStatus": "UNPROCESSABLE_ENTITY",
  "message": "Validation failed",
  "action_time": "2025-11-06T10:30:45",
  "data": {
    "amount": "must be greater than 0"
  }
}
```

---

## 10. Process Flexible Payment
**Purpose**: Process a flexible payment that can pay multiple installments or partially pay upcoming installments

**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}/installments/agreements/{agreementId}/early-flexible-payment`

**Access Level**: 🔒 Protected (Requires Authentication, Sufficient Wallet Balance)

**Authentication**: Bearer Token (JWT)

**Request Headers**:
| Header | Type | Required | Description |
|--------|------|----------|-------------|
| Authorization | string | Yes | Bearer token for authentication |
| Content-Type | string | Yes | Must be `application/json` |

**Path Parameters**:
| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| agreementId | UUID | Yes | The ID of the installment agreement | Must be a valid UUID, agreement must belong to authenticated user |

**Request JSON Sample**:
```json
{
  "amount": 500000.00,
  "note": "Paying 3 months ahead"
}
```

**Request Body Parameters**:
| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| amount | decimal | Yes | Amount to pay | Must be > 0, between minimumRequired and maximumAllowed |
| note | string | No | Optional note about the payment | Max 500 characters |

**Success Response JSON Sample**:
```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Successfully paid 2 installments and partially paid 1 more",
  "action_time": "2025-11-06T10:30:45",
  "data": {
    "agreementId": "a8b3c4d5-e6f7-8901-2345-6789abcdef01",
    "agreementNumber": "INST-2025-12345",
    "totalAmountPaid": 500000.00,
    "currency": "TZS",
    "transactionId": "TXN-2025-67890",
    "processedAt": "2025-11-06T10:30:45",
    "paymentsAffected": [
      {
        "paymentId": "p1a2b3c4-d5e6-f789-0123-456789abcdef",
        "paymentNumber": 3,
        "dueDate": "2025-12-01T00:00:00",
        "scheduledAmount": 200000.00,
        "amountApplied": 150000.00,
        "previouslyPaid": 50000.00,
        "newPaidAmount": 200000.00,
        "remaining": 0.00,
        "status": "COMPLETED",
        "wasCompleted": true
      },
      {
        "paymentId": "p2a3b4c5-d6e7-f890-1234-56789abcdef0",
        "paymentNumber": 4,
        "dueDate": "2026-01-01T00:00:00",
        "scheduledAmount": 200000.00,
        "amountApplied": 200000.00,
        "previouslyPaid": 0.00,
        "newPaidAmount": 200000.00,
        "remaining": 0.00,
        "status": "COMPLETED",
        "wasCompleted": true
      },
      {
        "paymentId": "p3a4b5c6-d7e8-f901-2345-6789abcdef01",
        "paymentNumber": 5,
        "dueDate": "2026-02-01T00:00:00",
        "scheduledAmount": 200000.00,
        "amountApplied": 150000.00,
        "previouslyPaid": 0.00,
        "newPaidAmount": 150000.00,
        "remaining": 50000.00,
        "status": "PARTIALLY_PAID",
        "wasCompleted": false
      }
    ],
    "agreementUpdate": {
      "paymentsCompleted": 4,
      "paymentsPartial": 1,
      "paymentsRemaining": 8,
      "amountPaid": 1300000.00,
      "amountRemaining": 700000.00,
      "nextPaymentDate": "2026-02-01T00:00:00",
      "nextPaymentAmount": 50000.00,
      "agreementStatus": "ACTIVE",
      "isCompleted": false
    },
    "message": "Successfully paid 2 installments and partially paid 1 more"
  }
}
```

**Success Response Fields**:
| Field | Description |
|-------|-------------|
| agreementId | ID of the installment agreement |
| agreementNumber | Human-readable agreement number |
| totalAmountPaid | Total amount paid in this transaction |
| currency | Currency code (TZS) |
| transactionId | Unique transaction identifier from ledger system |
| processedAt | Timestamp when payment was processed |
| paymentsAffected | Array of payments that were affected |
| paymentsAffected[].paymentId | ID of the affected payment |
| paymentsAffected[].paymentNumber | Sequential payment number |
| paymentsAffected[].dueDate | When this payment is due |
| paymentsAffected[].scheduledAmount | Original scheduled amount |
| paymentsAffected[].amountApplied | How much of flexible payment was applied here |
| paymentsAffected[].previouslyPaid | Amount that was previously paid |
| paymentsAffected[].newPaidAmount | Total amount now paid for this payment |
| paymentsAffected[].remaining | Amount still remaining for this payment |
| paymentsAffected[].status | New payment status (COMPLETED or PARTIALLY_PAID) |
| paymentsAffected[].wasCompleted | Whether this payment became fully completed |
| agreementUpdate | Updated agreement summary |
| agreementUpdate.paymentsCompleted | Total number of completed payments |
| agreementUpdate.paymentsPartial | Number of partially paid payments |
| agreementUpdate.paymentsRemaining | Number of payments still remaining |
| agreementUpdate.amountPaid | Total amount paid on agreement |
| agreementUpdate.amountRemaining | Total amount still remaining |
| agreementUpdate.nextPaymentDate | When next payment is due |
| agreementUpdate.nextPaymentAmount | Amount of next payment (remaining amount) |
| agreementUpdate.agreementStatus | Current agreement status |
| agreementUpdate.isCompleted | Whether agreement is fully completed |
| message | Human-readable success message |

**Error Response Examples**:

*Bad Request - Insufficient Balance (400):*
```json
{
  "success": false,
  "httpStatus": "BAD_REQUEST",
  "message": "Insufficient wallet balance. Required: 500000.00 TZS, Available: 300000.00 TZS",
  "action_time": "2025-11-06T10:30:45",
  "data": "Insufficient wallet balance. Required: 500000.00 TZS, Available: 300000.00 TZS"
}
```

*Bad Request - Agreement Not Active (400):*
```json
{
  "success": false,
  "httpStatus": "BAD_REQUEST",
  "message": "Cannot make payment on inactive agreement. Status: COMPLETED",
  "action_time": "2025-11-06T10:30:45",
  "data": "Cannot make payment on inactive agreement. Status: COMPLETED"
}
```

*Bad Request - Wallet Not Active (400):*
```json
{
  "success": false,
  "httpStatus": "BAD_REQUEST",
  "message": "Wallet is not active",
  "action_time": "2025-11-06T10:30:45",
  "data": "Wallet is not active"
}
```

*Validation Error (422):*
```json
{
  "success": false,
  "httpStatus": "UNPROCESSABLE_ENTITY",
  "message": "Validation failed",
  "action_time": "2025-11-06T10:30:45",
  "data": {
    "amount": "must be greater than 0",
    "note": "size must be between 0 and 500"
  }
}
```

---

## 11. Calculate Early Payoff
**Purpose**: Calculate the amount required to pay off the entire agreement early with interest discount

**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}/agreements/{agreementId}/early-payoff`

**Access Level**: 🔒 Protected (Requires Authentication, Owner Only)

**Authentication**: Bearer Token

**Request Headers**:
| Header | Type | Required | Description |
|--------|------|----------|-------------|
| Authorization | string | Yes | Bearer token for authentication |

**Path Parameters**:
| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| agreementId | UUID | Yes | Unique identifier of the agreement | Valid UUID format |

**Success Response JSON Sample**:
```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Early payoff calculation completed",
  "action_time": "2025-10-18T10:30:45",
  "data": {
    "agreementId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
    "paymentsCompleted": 2,
    "paymentsRemaining": 10,
    "amountPaid": 733333.34,
    "remainingPrincipal": 1320000.00,
    "unaccruedInterest": 346666.66,
    "interestRebate": 260000.00,
    "payoffWithRebate": 1406666.66,
    "payoffWithoutRebate": 1666666.66,
    "savingsVsScheduled": 260000.00,
    "rebatePolicy": "75% discount on remaining interest for early payoff",
    "calculatedAt": "2025-10-18T10:30:45"
  }
}
```

**Success Response Fields**:
| Field | Description |
|-------|-------------|
| agreementId | ID of the agreement |
| paymentsCompleted | Number of payments already made |
| paymentsRemaining | Number of payments left |
| amountPaid | Total amount paid so far |
| remainingPrincipal | Principal amount remaining |
| unaccruedInterest | Interest not yet charged |
| interestRebate | Interest discount amount (75% of remaining interest) |
| payoffWithRebate | Early payoff amount with discount applied |
| payoffWithoutRebate | Full remaining amount if paid on schedule |
| savingsVsScheduled | Amount saved by paying off early |
| rebatePolicy | Description of rebate policy |
| calculatedAt | When calculation was performed |

**Error Response Examples**:

*Bad Request - Not Eligible (400):*
```json
{
  "success": false,
  "httpStatus": "BAD_REQUEST",
  "message": "Early payoff not available for this agreement",
  "action_time": "2025-10-18T10:30:45",
  "data": "Early payoff not available for this agreement"
}
```

*Bad Request - Agreement Not Active (400):*
```json
{
  "success": false,
  "httpStatus": "BAD_REQUEST",
  "message": "Agreement is not active. Status: COMPLETED",
  "action_time": "2025-10-18T10:30:45",
  "data": "Agreement is not active. Status: COMPLETED"
}
```

---

## 12. Process Early Payoff
**Purpose**: Process full early payment of the agreement with interest discount applied

**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}/agreements/{agreementId}/early-payoff`

**Access Level**: 🔒 Protected (Requires Authentication, Owner Only)

**Authentication**: Bearer Token

**Request Headers**:
| Header | Type | Required | Description |
|--------|------|----------|-------------|
| Authorization | string | Yes | Bearer token for authentication |

**Path Parameters**:
| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| agreementId | UUID | Yes | Unique identifier of the agreement | Valid UUID format |

**Success Response JSON Sample**:
```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Early payoff processed successfully",
  "action_time": "2025-10-18T10:30:45",
  "data": {
    "paymentId": null,
    "agreementId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
    "agreementNumber": "INST-2025-12345",
    "amount": 1406666.66,
    "currency": "TZS",
    "paymentMethod": "WALLET",
    "transactionId": null,
    "status": "COMPLETED",
    "processedAt": "2025-10-18T10:30:45",
    "message": "Early payoff processed successfully",
    "agreementUpdate": {
      "paymentsCompleted": 12,
      "paymentsRemaining": 0,
      "amountPaid": 2400000.00,
      "amountRemaining": 0.00,
      "nextPaymentDate": null,
      "nextPaymentAmount": null,
      "agreementStatus": "COMPLETED",
      "isCompleted": true
    }
  }
}
```

**Success Response Fields**:
| Field | Description |
|-------|-------------|
| paymentId | Payment ID (null for early payoff) |
| agreementId | ID of the agreement |
| agreementNumber | Agreement number |
| amount | Total early payoff amount paid |
| currency | Currency code |
| paymentMethod | Payment method used |
| transactionId | Transaction reference (null for early payoff) |
| status | Payment status (COMPLETED) |
| processedAt | When payoff was processed |
| message | Success message |
| agreementUpdate | Updated agreement information |
| agreementUpdate.paymentsCompleted | Updated to total number of payments |
| agreementUpdate.paymentsRemaining | Set to 0 |
| agreementUpdate.amountPaid | Updated to total amount |
| agreementUpdate.amountRemaining | Set to 0 |
| agreementUpdate.nextPaymentDate | Set to null |
| agreementUpdate.nextPaymentAmount | Set to null |
| agreementUpdate.agreementStatus | Set to COMPLETED |
| agreementUpdate.isCompleted | Set to true |

**Error Response Examples**:

*Bad Request - Insufficient Balance (400):*
```json
{
  "success": false,
  "httpStatus": "BAD_REQUEST",
  "message": "Insufficient wallet balance for early payoff. Required: 1406666.66 TZS, Available: 500000.00 TZS",
  "action_time": "2025-10-18T10:30:45",
  "data": "Insufficient wallet balance for early payoff. Required: 1406666.66 TZS, Available: 500000.00 TZS"
}
```

*Bad Request - Not Eligible (400):*
```json
{
  "success": false,
  "httpStatus": "BAD_REQUEST",
  "message": "Agreement is not active. Status: DEFAULTED",
  "action_time": "2025-10-18T10:30:45",
  "data": "Agreement is not active. Status: DEFAULTED"
}
```

---

## 13. Cancel Agreement
**Purpose**: Cancel an installment agreement before any payments have been completed

**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}/agreements/{agreementId}/cancel`

**Access Level**: 🔒 Protected (Requires Authentication, Owner Only)

**Authentication**: Bearer Token

**Request Headers**:
| Header | Type | Required | Description |
|--------|------|----------|-------------|
| Authorization | string | Yes | Bearer token for authentication |
| Content-Type | string | Yes | application/json |

**Path Parameters**:
| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| agreementId | UUID | Yes | Unique identifier of the agreement | Valid UUID format |

**Request JSON Sample**:
```json
{
  "reason": "Changed my mind about the purchase. Found a better deal elsewhere."
}
```

**Request Body Parameters**:
| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| reason | string | Yes | Reason for cancellation | Min: 1, Max: 500 characters |

**Success Response JSON Sample**:
```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Agreement cancelled successfully",
  "action_time": "2025-10-18T10:30:45",
  "data": null
}
```

**Success Response Fields**:
No data returned on successful cancellation

**Error Response Examples**:

*Bad Request - Cannot Cancel (400):*
```json
{
  "success": false,
  "httpStatus": "BAD_REQUEST",
  "message": "Agreement cannot be cancelled. Only agreements with no completed payments can be cancelled.",
  "action_time": "2025-10-18T10:30:45",
  "data": "Agreement cannot be cancelled. Only agreements with no completed payments can be cancelled."
}
```

*Validation Error (422):*
```json
{
  "success": false,
  "httpStatus": "UNPROCESSABLE_ENTITY",
  "message": "Validation failed",
  "action_time": "2025-10-18T10:30:45",
  "data": {
    "reason": "Cancellation reason is required"
  }
}
```

---

## Quick Reference Guide

### HTTP Method Badge Code Templates

**GET Badge:**
```html
<span style="background-color: #28a745; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">GET</span>
```

**POST Badge:**
```html
<span style="background-color: #007bff; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">POST</span>
```

### Common HTTP Status Codes
- `200 OK`: Successful GET/POST request
- `400 Bad Request`: Invalid request data or business logic violation
- `401 Unauthorized`: Authentication required/failed
- `403 Forbidden`: Insufficient permissions
- `404 Not Found`: Resource not found
- `422 Unprocessable Entity`: Validation errors
- `500 Internal Server Error`: Server error

### Authentication
- **Bearer Token**: Include `Authorization: Bearer your_token` in headers
- All customer endpoints require valid authentication
- Token must belong to the customer who owns the agreement

### Agreement Status Flow
1. **PENDING_FIRST_PAYMENT**: Down payment made, waiting for grace period
2. **ACTIVE**: Currently paying installments
3. **COMPLETED**: Fully paid off
4. **DEFAULTED**: Missed 2+ payments, in collections
5. **CANCELLED**: User cancelled before first payment

### Payment Status Types
- **SCHEDULED**: Not due yet
- **PENDING**: Due today, awaiting payment
- **PROCESSING**: Payment in progress
- **COMPLETED**: Successfully paid
- **FAILED**: Payment attempt failed
- **LATE**: Past due date
- **SKIPPED**: Missed completely
- **WAIVED**: Forgiven (special cases)

### Business Rules
- **Early Payoff Discount**: 75% off remaining interest
- **Maximum Retries**: 5 attempts per payment
- **Default Threshold**: 2 missed payments triggers DEFAULTED status
- **Cancellation Window**: Only before first payment completed
- **Payment Method**: Wallet payments only
- **Currency**: All amounts in TZS (Tanzanian Shillings)

### Common Error Scenarios
1. **Insufficient Wallet Balance**: Top up wallet before payment
2. **Payment Not Due**: Wait until due date or make early payoff
3. **Agreement Defaulted**: Contact support for resolution
4. **Max Retries Exceeded**: Contact support
5. **Ownership Validation Failed**: Can only access own agreements

---

## Notes for Developers

### Idempotency
- Payment processing endpoints are idempotent
- Duplicate payment requests will return existing payment status
- Use transaction IDs to track payment processing

### Rate Limiting
- Standard rate limits apply: 100 requests per hour per user
- Payment processing has additional throttling for security

### Webhooks
- Payment success/failure events trigger notifications
- Agreement completion triggers order creation (for AFTER_PAYMENT fulfillment)
- Integration with notification service for email/SMS alerts

### Testing
- Use sandbox environment for testing: `https://sandbox-api.nextgate.com`
- Test user accounts available with pre-loaded wallets
- Mock payment processing for integration testing

### Support
- For API issues: api-support@nextgate.com
- For business logic questions: product@nextgate.com
- Emergency contact: +255-XXX-XXX-XXXId | Transaction reference ID |
| failureReason | Reason if payment failed |
| retryCount | Number of retry attempts |
| daysUntilDue | Days until payment is due (negative if overdue) |
| daysOverdue | Days payment is overdue (null if not overdue) |
| canPay | Whether payment can be made now |
| canRetry | Whether payment can be retried |
---

# Installment Purchase  - Public & Plan Endpoints

**Author**: Josh S. Sakweli, Backend Lead Team  
**Last Updated**: 2025-10-18  
**Version**: v1.0

**Base URL**: `https://api.nextgate.com/api/v1/installments`

**Short Description**: This API provides public endpoints for browsing installment plans and calculating installment previews before checkout. These endpoints do not require authentication and are designed to help customers explore installment options, understand payment breakdowns, and make informed purchase decisions. The endpoints return detailed financial calculations including amortization schedules, interest breakdowns, and payment timelines.

**Hints**: 
- Public endpoints do not require authentication
- Plans are filtered to show only active plans to customers
- Preview calculations use amortization formula for accurate payment schedules
- Early payoff discount (75% off remaining interest) is standard across all plans
- Down payment range: minimum set by plan (typically 10-20%), maximum 50% (platform limit)
- All financial calculations rounded to 2 decimal places
- Grace periods determine when first payment is due
- Fulfillment timing (IMMEDIATE vs AFTER_PAYMENT) affects when product ships
- APR (Annual Percentage Rate) converted to period rate based on payment frequency
- All amounts are in TZS (Tanzanian Shillings)

---

## Standard Response Format

All API responses follow a consistent structure using our Globe Response Builder pattern:

### Success Response Structure
```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Operation completed successfully",
  "action_time": "2025-10-18T10:30:45",
  "data": {
    // Actual response data goes here
  }
}
```

### Error Response Structure
```json
{
  "success": false,
  "httpStatus": "BAD_REQUEST",
  "message": "Error description",
  "action_time": "2025-10-18T10:30:45",
  "data": "Error description"
}
```

### Standard Response Fields
| Field | Type | Description |
|-------|------|-------------|
| `success` | boolean | Always `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 operation result |
| `action_time` | string | ISO 8601 timestamp of when the response was generated |
| `data` | object/string | Response payload for success, error details for failures |

---

## HTTP Method Badge Standards

For better visual clarity, all endpoints use colored badges for HTTP methods with the following standard colors:

- **GET** - <span style="background-color: #28a745; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">GET</span> - Green (Safe, read-only operations)
- **POST** - <span style="background-color: #007bff; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">POST</span> - Blue (Create new resources)

---

## Endpoints

## 1. Get Available Plans for Product
**Purpose**: Retrieve all active installment plans available for a specific product

**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}/products/{productId}/plans`

**Access Level**: 🌐 Public (No Authentication Required)

**Authentication**: None

**Path Parameters**:
| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| productId | UUID | Yes | Unique identifier of the product | Valid UUID format |

**Success Response JSON Sample**:
```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Available installment plans retrieved successfully",
  "action_time": "2025-10-18T10:30:45",
  "data": [
    {
      "planId": "4b5c6d7e-8f9a-4b1c-9d2e-3f4a5b6c7d8e",
      "planName": "Quick Payment Plan",
      "paymentFrequency": "WEEKLY",
      "paymentFrequencyDisplay": "Weekly",
      "customFrequencyDays": null,
      "numberOfPayments": 8,
      "duration": "8 weeks",
      "apr": 10.00,
      "minDownPaymentPercent": 20,
      "gracePeriodDays": 7,
      "fulfillmentTiming": "IMMEDIATE",
      "isActive": true,
      "isFeatured": false,
      "displayOrder": 1,
      "preview": {
        "productPrice": 2000000.00,
        "minDownPaymentAmount": 400000.00,
        "maxDownPaymentAmount": 1000000.00,
        "financedAmountExample": 1600000.00,
        "paymentAmountExample": 210000.00,
        "totalInterestExample": 80000.00,
        "totalCostExample": 2080000.00,
        "firstPaymentDateExample": "2025-10-25T00:00:00",
        "lastPaymentDateExample": "2025-12-13T00:00:00"
      }
    },
    {
      "planId": "5c6d7e8f-9a0b-4c1d-8e2f-3a4b5c6d7e8f",
      "planName": "Standard Monthly Plan",
      "paymentFrequency": "MONTHLY",
      "paymentFrequencyDisplay": "Monthly",
      "customFrequencyDays": null,
      "numberOfPayments": 12,
      "duration": "12 months",
      "apr": 15.00,
      "minDownPaymentPercent": 15,
      "gracePeriodDays": 30,
      "fulfillmentTiming": "IMMEDIATE",
      "isActive": true,
      "isFeatured": true,
      "displayOrder": 2,
      "preview": {
        "productPrice": 2000000.00,
        "minDownPaymentAmount": 300000.00,
        "maxDownPaymentAmount": 1000000.00,
        "financedAmountExample": 1700000.00,
        "paymentAmountExample": 156250.00,
        "totalInterestExample": 175000.00,
        "totalCostExample": 2175000.00,
        "firstPaymentDateExample": "2025-11-17T00:00:00",
        "lastPaymentDateExample": "2026-10-17T00:00:00"
      }
    },
    {
      "planId": "6d7e8f9a-0b1c-4d2e-9f3a-4b5c6d7e8f9a",
      "planName": "Budget Friendly Plan",
      "paymentFrequency": "MONTHLY",
      "paymentFrequencyDisplay": "Monthly",
      "customFrequencyDays": null,
      "numberOfPayments": 24,
      "duration": "24 months",
      "apr": 18.00,
      "minDownPaymentPercent": 10,
      "gracePeriodDays": 30,
      "fulfillmentTiming": "AFTER_PAYMENT",
      "isActive": true,
      "isFeatured": false,
      "displayOrder": 3,
      "preview": {
        "productPrice": 2000000.00,
        "minDownPaymentAmount": 200000.00,
        "maxDownPaymentAmount": 1000000.00,
        "financedAmountExample": 1800000.00,
        "paymentAmountExample": 87500.00,
        "totalInterestExample": 300000.00,
        "totalCostExample": 2300000.00,
        "firstPaymentDateExample": "2025-11-17T00:00:00",
        "lastPaymentDateExample": "2027-10-17T00:00:00"
      }
    }
  ]
}
```

**Success Response Fields**:
| Field | Description |
|-------|-------------|
| planId | Unique identifier for the plan |
| planName | Display name of the plan |
| paymentFrequency | Payment frequency enum (DAILY, WEEKLY, BI_WEEKLY, SEMI_MONTHLY, MONTHLY, QUARTERLY, CUSTOM_DAYS) |
| paymentFrequencyDisplay | Human-readable payment frequency |
| customFrequencyDays | Custom frequency in days (only for CUSTOM_DAYS type) |
| numberOfPayments | Total number of payments in the plan |
| duration | Human-readable duration (e.g., "12 months", "8 weeks") |
| apr | Annual Percentage Rate |
| minDownPaymentPercent | Minimum down payment percentage required by this plan |
| gracePeriodDays | Days before first payment is due |
| fulfillmentTiming | When product ships (IMMEDIATE = after down payment, AFTER_PAYMENT = after final payment) |
| isActive | Whether plan is currently active |
| isFeatured | Whether plan is featured/recommended (shows "Most Popular" badge) |
| displayOrder | Order for display on product page |
| preview | Preview calculations object |
| preview.productPrice | Product price used for calculations |
| preview.minDownPaymentAmount | Minimum down payment amount |
| preview.maxDownPaymentAmount | Maximum down payment amount (platform limit: 50%) |
| preview.financedAmountExample | Amount being financed (at minimum down payment) |
| preview.paymentAmountExample | Each installment amount (at minimum down payment) |
| preview.totalInterestExample | Total interest to be paid (at minimum down payment) |
| preview.totalCostExample | Grand total cost including interest (at minimum down payment) |
| preview.firstPaymentDateExample | When first payment would be due |
| preview.lastPaymentDateExample | When last payment would be due |

**Error Response JSON Sample**:
```json
{
  "success": false,
  "httpStatus": "NOT_FOUND",
  "message": "Product not found with ID: 7c9e6679-7425-40de-944b-e07fc1f90ae7",
  "action_time": "2025-10-18T10:30:45",
  "data": "Product not found with ID: 7c9e6679-7425-40de-944b-e07fc1f90ae7"
}
```

**Standard Error Types**:

### Application-Level Exceptions (400-499)
- `404 NOT_FOUND`: Product not found or product does not have installment enabled

**Error Response Examples**:

*Not Found - Product (404):*
```json
{
  "success": false,
  "httpStatus": "NOT_FOUND",
  "message": "Product not found with ID: 7c9e6679-7425-40de-944b-e07fc1f90ae7",
  "action_time": "2025-10-18T10:30:45",
  "data": "Product not found with ID: 7c9e6679-7425-40de-944b-e07fc1f90ae7"
}
```

**Special Cases**:
- If product has installment disabled, returns empty array
- If product has no active plans, returns empty array
- Plans are automatically sorted by displayOrder

---

## 2. Calculate Installment Preview
**Purpose**: Calculate detailed payment breakdown and schedule for a specific plan with customer's chosen down payment percentage

**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}/calculate-preview`

**Access Level**: 🌐 Public (No Authentication Required)

**Authentication**: None

**Request Headers**:
| Header | Type | Required | Description |
|--------|------|----------|-------------|
| Content-Type | string | Yes | application/json |

**Request JSON Sample**:
```json
{
  "planId": "5c6d7e8f-9a0b-4c1d-8e2f-3a4b5c6d7e8f",
  "productPrice": 2000000.00,
  "quantity": 1,
  "downPaymentPercent": 20
}
```

**Request Body Parameters**:
| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| planId | UUID | Yes | ID of the installment plan to calculate | Valid UUID format, plan must exist and be active |
| productPrice | decimal | Yes | Price of the product | Min: 0.01, Max: 999999999.99 |
| quantity | integer | Yes | Quantity of product | Min: 1, Max: 1 (installment limited to 1 item) |
| downPaymentPercent | integer | Yes | Down payment percentage customer chooses | Min: 10, Max: 50, must be >= plan's minDownPaymentPercent |

**Success Response JSON Sample**:
```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Installment preview calculated successfully",
  "action_time": "2025-10-18T10:30:45",
  "data": {
    "planId": "5c6d7e8f-9a0b-4c1d-8e2f-3a4b5c6d7e8f",
    "planName": "Standard Monthly Plan",
    "planDescription": "Pay in 12 monthly installments at 15.0% APR",
    "paymentFrequency": "Monthly",
    "numberOfPayments": 12,
    "durationDisplay": "12 months",
    "apr": 15.00,
    "gracePeriodDays": 30,
    "productPrice": 2000000.00,
    "quantity": 1,
    "totalProductCost": 2000000.00,
    "downPaymentPercent": 20,
    "downPaymentAmount": 400000.00,
    "minDownPaymentPercent": 15,
    "maxDownPaymentPercent": 50,
    "minDownPaymentAmount": 300000.00,
    "maxDownPaymentAmount": 1000000.00,
    "financedAmount": 1600000.00,
    "monthlyPaymentAmount": 146666.67,
    "totalInterestAmount": 160000.00,
    "totalAmount": 2160000.00,
    "currency": "TZS",
    "firstPaymentDate": "2025-11-17T00:00:00",
    "lastPaymentDate": "2026-10-17T00:00:00",
    "schedule": [
      {
        "paymentNumber": 1,
        "dueDate": "2025-11-17T00:00:00",
        "amount": 146666.67,
        "principalPortion": 126666.67,
        "interestPortion": 20000.00,
        "remainingBalance": 1473333.33,
        "description": "Month 1 payment"
      },
      {
        "paymentNumber": 2,
        "dueDate": "2025-12-17T00:00:00",
        "amount": 146666.67,
        "principalPortion": 128250.00,
        "interestPortion": 18416.67,
        "remainingBalance": 1345083.33,
        "description": "Month 2 payment"
      },
      {
        "paymentNumber": 3,
        "dueDate": "2026-01-17T00:00:00",
        "amount": 146666.67,
        "principalPortion": 129853.13,
        "interestPortion": 16813.54,
        "remainingBalance": 1215230.20,
        "description": "Month 3 payment"
      },
      {
        "paymentNumber": 12,
        "dueDate": "2026-10-17T00:00:00",
        "amount": 146666.67,
        "principalPortion": 144833.33,
        "interestPortion": 1833.34,
        "remainingBalance": 0.00,
        "description": "Final payment"
      }
    ],
    "comparison": {
      "payingUpfront": 2000000.00,
      "payingWithInstallment": 2160000.00,
      "additionalCost": 160000.00,
      "additionalCostPercent": 8.00
    },
    "fulfillmentTiming": "IMMEDIATE",
    "fulfillmentDescription": "Product ships immediately after down payment"
  }
}
```

**Success Response Fields**:
| Field | Description |
|-------|-------------|
| planId | ID of the plan used |
| planName | Name of the plan |
| planDescription | Description of the plan |
| paymentFrequency | Human-readable payment frequency |
| numberOfPayments | Total number of payments |
| durationDisplay | Human-readable duration |
| apr | Annual Percentage Rate |
| gracePeriodDays | Grace period before first payment |
| productPrice | Per-unit product price |
| quantity | Quantity purchased |
| totalProductCost | Total product cost (price × quantity) |
| downPaymentPercent | Chosen down payment percentage |
| downPaymentAmount | Calculated down payment amount |
| minDownPaymentPercent | Minimum allowed down payment % |
| maxDownPaymentPercent | Maximum allowed down payment % (50%) |
| minDownPaymentAmount | Minimum down payment amount |
| maxDownPaymentAmount | Maximum down payment amount |
| financedAmount | Amount being financed (after down payment) |
| monthlyPaymentAmount | Each installment payment amount |
| totalInterestAmount | Total interest to be paid |
| totalAmount | Grand total (product + interest) |
| currency | Currency code (TZS) |
| firstPaymentDate | When first payment is due |
| lastPaymentDate | When last payment is due |
| schedule | Array of payment schedule items |
| schedule[].paymentNumber | Payment number (1, 2, 3...) |
| schedule[].dueDate | When payment is due |
| schedule[].amount | Payment amount |
| schedule[].principalPortion | Amount going to principal |
| schedule[].interestPortion | Amount going to interest |
| schedule[].remainingBalance | Balance after this payment |
| schedule[].description | Payment description |
| comparison | Comparison information object |
| comparison.payingUpfront | Cost if paying full price now |
| comparison.payingWithInstallment | Total cost with installment |
| comparison.additionalCost | Extra cost (interest) |
| comparison.additionalCostPercent | Interest as % of product price |
| fulfillmentTiming | When product ships |
| fulfillmentDescription | Description of fulfillment timing |

**Error Response JSON Sample**:
```json
{
  "success": false,
  "httpStatus": "BAD_REQUEST",
  "message": "Down payment must be at least 15% for this plan",
  "action_time": "2025-10-18T10:30:45",
  "data": "Down payment must be at least 15% for this plan"
}
```

**Standard Error Types**:

### Application-Level Exceptions (400-499)
- `400 BAD_REQUEST`: Invalid down payment percentage, inactive plan, or business rule violation
- `404 NOT_FOUND`: Plan not found
- `422 UNPROCESSABLE_ENTITY`: Validation errors on request parameters

**Error Response Examples**:

*Bad Request - Down Payment Too Low (400):*
```json
{
  "success": false,
  "httpStatus": "BAD_REQUEST",
  "message": "Down payment must be at least 15% for this plan",
  "action_time": "2025-10-18T10:30:45",
  "data": "Down payment must be at least 15% for this plan"
}
```

*Bad Request - Down Payment Too High (400):*
```json
{
  "success": false,
  "httpStatus": "BAD_REQUEST",
  "message": "Down payment cannot exceed 50%",
  "action_time": "2025-10-18T10:30:45",
  "data": "Down payment cannot exceed 50%"
}
```

*Bad Request - Plan Not Active (400):*
```json
{
  "success": false,
  "httpStatus": "BAD_REQUEST",
  "message": "This installment plan is not currently available",
  "action_time": "2025-10-18T10:30:45",
  "data": "This installment plan is not currently available"
}
```

*Not Found - Plan (404):*
```json
{
  "success": false,
  "httpStatus": "NOT_FOUND",
  "message": "Installment plan not found with ID: 5c6d7e8f-9a0b-4c1d-8e2f-3a4b5c6d7e8f",
  "action_time": "2025-10-18T10:30:45",
  "data": "Installment plan not found with ID: 5c6d7e8f-9a0b-4c1d-8e2f-3a4b5c6d7e8f"
}
```

*Validation Error (422):*
```json
{
  "success": false,
  "httpStatus": "UNPROCESSABLE_ENTITY",
  "message": "Validation failed",
  "action_time": "2025-10-18T10:30:45",
  "data": {
    "planId": "Plan ID is required",
    "productPrice": "Product price must be greater than 0",
    "downPaymentPercent": "must be between 10 and 50"
  }
}
```

---

## Quick Reference Guide

### HTTP Method Badge Code Templates

**GET Badge:**
```html
<span style="background-color: #28a745; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">GET</span>
```

**POST Badge:**
```html
<span style="background-color: #007bff; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">POST</span>
```

### Common HTTP Status Codes
- `200 OK`: Successful GET/POST request
- `400 Bad Request`: Invalid request data or business logic violation
- `404 Not Found`: Resource not found
- `422 Unprocessable Entity`: Validation errors
- `500 Internal Server Error`: Server error

### Payment Frequency Types
- **DAILY**: Payment every day
- **WEEKLY**: Payment every week (7 days)
- **BI_WEEKLY**: Payment every 2 weeks (14 days)
- **SEMI_MONTHLY**: Payment twice per month (1st and 15th)
- **MONTHLY**: Payment every month (30 days)
- **QUARTERLY**: Payment every 3 months (90 days)
- **CUSTOM_DAYS**: Payment at custom interval (specified in customFrequencyDays)

### Fulfillment Timing Options
- **IMMEDIATE**: Product ships immediately after down payment is made. Customer gets product while paying installments.
- **AFTER_PAYMENT**: Product ships only after final payment is completed (layaway model). Inventory is held during payment period.

### Down Payment Rules
- **Minimum**: Set by individual plan (typically 10-20%)
- **Maximum**: 50% (platform-wide limit)
- **Validation**: Customer's choice must be within min-max range
- **Purpose**: Reduces financed amount and total interest paid

### APR (Annual Percentage Rate)
- Expressed as percentage (e.g., 15.00 = 15%)
- Range: 0% to 36% (platform limits)
- Converted to period rate based on payment frequency
- Used in amortization calculation for each payment

### Financial Calculations

#### Amortization Formula
Monthly Payment = P × [r(1+r)^n] / [(1+r)^n - 1]

Where:
- P = Principal (financed amount)
- r = Period rate (APR / periods per year)
- n = Number of payments

#### Period Rate Calculation
- **Daily**: APR / 365
- **Weekly**: APR / 52
- **Bi-weekly**: APR / 26
- **Semi-monthly**: APR / 24
- **Monthly**: APR / 12
- **Quarterly**: APR / 4
- **Custom**: APR / (365 / customDays)

#### Payment Breakdown
Each payment consists of:
1. **Interest Portion**: Remaining balance × period rate
2. **Principal Portion**: Payment amount - interest portion
3. **Remaining Balance**: Previous balance - principal portion

### Schedule Generation
- First payment due after grace period
- Subsequent payments calculated based on frequency
- Last payment adjusted for rounding differences
- All dates in ISO 8601 format

### Comparison Information
Shows cost difference between:
- **Paying upfront**: Original product price
- **Paying with installment**: Product price + total interest
- **Additional cost**: Total interest amount
- **Additional cost %**: Interest as percentage of product price

### Common Use Cases

#### 1. Display Plans on Product Page
```
GET /api/v1/installments/products/{productId}/plans
```
- Shows all active plans for product
- Displays preview calculations at minimum down payment
- Highlights featured plans
- Sorted by displayOrder

#### 2. Calculate Custom Preview
```
POST /api/v1/installments/calculate-preview
{
  "planId": "...",
  "productPrice": 2000000.00,
  "quantity": 1,
  "downPaymentPercent": 25
}
```
- Customer adjusts down payment slider
- Real-time calculation of payments
- Shows full payment schedule
- Displays savings comparison

#### 3. Pre-Checkout Validation
```
POST /api/v1/installments/calculate-preview
```
- Validates customer's choices before checkout
- Ensures plan is still active
- Confirms down payment within range
- Generates schedule for agreement creation

### Integration Examples

#### Frontend Flow
1. Product page loads → Call GET /products/{id}/plans
2. Display plan options with previews
3. User selects plan and adjusts down payment
4. Call POST /calculate-preview on down payment change
5. Show detailed breakdown and schedule
6. User proceeds to checkout with selected configuration

#### Mobile App Flow
1. Fetch plans when user taps "Installment Options"
2. Show plan cards with key metrics
3. Tapped plan shows full preview
4. Slider for down payment percentage
5. Real-time preview updates
6. "Continue to Checkout" with configuration

### Error Handling Best Practices

#### Client-Side Validation
- Validate down payment range before API call
- Check quantity = 1 for installment
- Ensure product price > 0
- Validate UUID formats

#### Server-Side Errors
- 400: Show user-friendly message, allow retry
- 404: Product/plan not found, redirect or show alternatives
- 422: Display field-specific validation errors
- 500: Generic error message, log for investigation

### Performance Considerations
- Cache plan data (TTL: 1 hour)
- Debounce preview calculations (300ms)
- Lazy load full schedules
- Optimize for mobile networks

### Testing Scenarios

#### Happy Path
1. Get plans for valid product with installment enabled
2. Calculate preview with valid parameters
3. Verify calculations match expected values

#### Edge Cases
1. Product with no active plans → empty array
2. Product with installment disabled → empty array
3. Down payment at minimum boundary
4. Down payment at maximum boundary (50%)
5. Inactive plan in preview request → error

#### Error Cases
1. Invalid product ID → 404
2. Invalid plan ID → 404
3. Down payment too low → 400
4. Down payment too high → 400
5. Missing required fields → 422
6. Invalid data types → 422

### Data Format Standards
- **Dates**: ISO 8601 format (2025-10-18T14:30:00Z)
- **Currency**: TZS (Tanzanian Shillings), no currency symbol in API
- **Decimals**: 2 decimal places for all monetary values
- **Percentages**: Whole numbers (15 = 15%, not 0.15)
- **UUIDs**: Standard UUID v4 format with hyphens

### Business Rules Summary
1. **One Item Per Agreement**: Installment limited to single product
2. **Active Plans Only**: Only active plans returned in public endpoints
3. **Down Payment Range**: 10-50% (plan minimum to platform maximum)
4. **APR Limits**: 0-36% (platform enforced)
5. **Grace Period**: 0-60 days (plan-specific)
6. **Payment Frequency**: All standard frequencies supported
7. **Fulfillment Options**: IMMEDIATE or AFTER_PAYMENT
8. **Currency**: TZS only (Tanzania market)
9. **Rounding**: All calculations rounded to 2 decimal places
10. **Early Payoff**: 75% discount on remaining interest (not shown in preview)

### Notes for Developers

#### Calculation Accuracy
- Use decimal/numeric types for currency calculations
- Avoid floating-point arithmetic
- Round to 2 decimals at final step only
- Last payment absorbs rounding differences

#### Caching Strategy
- Cache plan data per product (1 hour TTL)
- Invalidate cache when plans updated
- Preview calculations should not be cached
- Consider CDN for plan endpoints

#### Mobile Optimization
- Minimize payload size (exclude schedule if not needed)
- Compress responses (gzip/brotli)
- Use pagination for long schedules
- Implement request debouncing

#### Security Considerations
- No authentication required for public endpoints
- Rate limiting: 1000 requests/hour per IP
- Input validation on all parameters
- SQL injection prevention (parameterized queries)
- XSS prevention (sanitize all inputs)

### Support & Resources
- **API Documentation**: https://docs.nextgate.com/api/installments
- **Developer Portal**: https://developers.nextgate.com
- **Support Email**: api-support@nextgate.com
- **Status Page**: https://status.nextgate.com
- **Changelog**: https://docs.nextgate.com/changelog

## Documentation Checklist

Before using this API, ensure you understand:

- [x] **Public Access**: No authentication required
- [x] **Plan Filtering**: Only active plans returned
- [x] **Down Payment Rules**: Min (plan-specific) to Max (50%)
- [x] **APR Calculation**: Converted to period rate automatically
- [x] **Amortization**: Principal + interest breakdown per payment
- [x] **Schedule Generation**: All payment dates calculated
- [x] **Fulfillment Options**: IMMEDIATE vs AFTER_PAYMENT
- [x] **Currency**: TZS only
- [x] **Rounding**: 2 decimal places
- [x] **Error Handling**: Proper validation and error messages
- [x] **Rate Limiting**: 1000 requests/hour per IP
- [x] **Caching**: Recommended for plan data
- [x] **Testing**: Sandbox environment available

---

# Installment Plan Management - Admin/Shop Endpoints

**Author**: Josh S. Sakweli, Backend Lead Team  
**Last Updated**: 2025-10-18  
**Version**: v1.0

**Base URL**: `https://api.nextgate.com/api/v1/products/{shopId}/{productId}/installment-plans`

**Short Description**: This API provides comprehensive endpoints for shop owners and administrators to create, manage, and configure installment plans for their products. Shop owners can define flexible payment terms including payment frequencies, interest rates, down payment requirements, grace periods, and fulfillment options. The API supports full CRUD operations, plan activation/deactivation, featured plan selection, and product-level installment enablement. All endpoints require authentication and validate shop ownership.

**Hints**: 
- All endpoints require authentication and shop ownership validation
- Shop owners can only manage plans for their own products
- Multiple plans can be created per product for different customer segments
- Only one plan per product can be featured at a time
- Plans can be deactivated without deletion to preserve historical data
- Active plans are automatically available to customers on product pages
- Inactive plans are hidden from customers but preserved for existing agreements
- Payment frequencies support daily, weekly, bi-weekly, semi-monthly, monthly, quarterly, and custom intervals
- APR (Annual Percentage Rate) must be between 0% and 36%
- Down payment requirements: 10-50% range
- Grace periods: 0-60 days before first payment
- Fulfillment options: IMMEDIATE (ship after down payment) or AFTER_PAYMENT (ship after completion)
- Display order determines plan sorting on product pages
- Featured plans get "Most Popular" or "Recommended" badge
- Enabling/disabling installments at product level affects all plans

---

## Standard Response Format

All API responses follow a consistent structure using our Globe Response Builder pattern:

### Success Response Structure
```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Operation completed successfully",
  "action_time": "2025-10-18T10:30:45",
  "data": {
    // Actual response data goes here
  }
}
```

### Error Response Structure
```json
{
  "success": false,
  "httpStatus": "BAD_REQUEST",
  "message": "Error description",
  "action_time": "2025-10-18T10:30:45",
  "data": "Error description"
}
```

### Standard Response Fields
| Field | Type | Description |
|-------|------|-------------|
| `success` | boolean | Always `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 operation result |
| `action_time` | string | ISO 8601 timestamp of when the response was generated |
| `data` | object/string | Response payload for success, error details for failures |

---

## HTTP Method Badge Standards

For better visual clarity, all endpoints use colored badges for HTTP methods with the following standard colors:

- **GET** - <span style="background-color: #28a745; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">GET</span> - Green (Safe, read-only operations)
- **POST** - <span style="background-color: #007bff; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">POST</span> - Blue (Create new resources)
- **PUT** - <span style="background-color: #ffc107; color: black; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">PUT</span> - Yellow (Update/replace entire resource)
- **PATCH** - <span style="background-color: #fd7e14; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">PATCH</span> - Orange (Partial updates)
- **DELETE** - <span style="background-color: #dc3545; color: white; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 12px; font-weight: bold;">DELETE</span> - Red (Remove resources)

---

## Endpoints

## 1. Create Installment Plan
**Purpose**: Create a new installment plan for a specific product

**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}`

**Access Level**: 🔒 Protected (Requires Authentication, Shop Owner Only)

**Authentication**: Bearer Token

**Request Headers**:
| Header | Type | Required | Description |
|--------|------|----------|-------------|
| Authorization | string | Yes | Bearer token for authentication |

**Path Parameters**:
| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| shopId | UUID | Yes | Unique identifier of the shop | Valid UUID format, must be owned by authenticated user |
| productId | UUID | Yes | Unique identifier of the product | Valid UUID format, must belong to specified shop |
| planId | UUID | Yes | Unique identifier of the plan to deactivate | Valid UUID format, must belong to specified product |

**Success Response JSON Sample**:
```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Installment plan deactivated successfully",
  "action_time": "2025-10-18T10:30:45",
  "data": {
    "planId": "6d7e8f9a-0b1c-4d2e-9f3a-4b5c6d7e8f9a",
    "planName": "Budget Friendly Plan",
    "isActive": false,
    "updatedAt": "2025-10-18T10:30:45"
  }
}
```

**Success Response Fields**:
| Field | Description |
|-------|-------------|
| planId | ID of the deactivated plan |
| planName | Name of the plan |
| isActive | Active status (false) |
| updatedAt | Update timestamp |

**Error Response Examples**:
Same as Activate Installment Plan endpoint

**Important Notes**:
- Deactivated plans are hidden from customers on product pages
- Existing agreements continue to use deactivated plans
- Plan can be reactivated at any time
- Deactivation is preferred over deletion for plans with existing agreements

---

## 2. Get Product Installment Plans
**Purpose**: Retrieve all installment plans for a specific product (including inactive plans for admin view)

**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}`

**Access Level**: 🔒 Protected (Requires Authentication, Shop Owner Only)

**Authentication**: Bearer Token

**Request Headers**:
| Header | Type | Required | Description |
|--------|------|----------|-------------|
| Authorization | string | Yes | Bearer token for authentication |

**Path Parameters**:
| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| shopId | UUID | Yes | Unique identifier of the shop | Valid UUID format, must be owned by authenticated user |
| productId | UUID | Yes | Unique identifier of the product | Valid UUID format, must belong to specified shop |

**Success Response JSON Sample**:
```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Installment plans retrieved successfully",
  "action_time": "2025-10-18T10:30:45",
  "data": [
    {
      "planId": "4b5c6d7e-8f9a-4b1c-9d2e-3f4a5b6c7d8e",
      "planName": "Quick Payment Plan",
      "paymentFrequency": "WEEKLY",
      "paymentFrequencyDisplay": "Weekly",
      "customFrequencyDays": null,
      "numberOfPayments": 8,
      "calculatedDurationDays": 56,
      "calculatedDurationDisplay": "8 weeks",
      "apr": 10.00,
      "minDownPaymentPercent": 20,
      "gracePeriodDays": 7,
      "fulfillmentTiming": "IMMEDIATE",
      "isActive": true,
      "isFeatured": false,
      "displayOrder": 1,
      "productId": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
      "productName": "Samsung Galaxy S24 Ultra",
      "shopId": "8d3a7b12-9c4e-4f8a-b5d2-3e6f7a8b9c0d",
      "shopName": "Tech World Store",
      "createdAt": "2025-10-15T09:00:00",
      "updatedAt": "2025-10-15T09:00:00"
    },
    {
      "planId": "5c6d7e8f-9a0b-4c1d-8e2f-3a4b5c6d7e8f",
      "planName": "12 Month Standard Plan",
      "paymentFrequency": "MONTHLY",
      "paymentFrequencyDisplay": "Monthly",
      "customFrequencyDays": null,
      "numberOfPayments": 12,
      "calculatedDurationDays": 360,
      "calculatedDurationDisplay": "12 months",
      "apr": 15.00,
      "minDownPaymentPercent": 20,
      "gracePeriodDays": 30,
      "fulfillmentTiming": "IMMEDIATE",
      "isActive": true,
      "isFeatured": true,
      "displayOrder": 2,
      "productId": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
      "productName": "Samsung Galaxy S24 Ultra",
      "shopId": "8d3a7b12-9c4e-4f8a-b5d2-3e6f7a8b9c0d",
      "shopName": "Tech World Store",
      "createdAt": "2025-10-16T10:30:00",
      "updatedAt": "2025-10-16T10:30:00"
    },
    {
      "planId": "6d7e8f9a-0b1c-4d2e-9f3a-4b5c6d7e8f9a",
      "planName": "Budget Friendly Plan",
      "paymentFrequency": "MONTHLY",
      "paymentFrequencyDisplay": "Monthly",
      "customFrequencyDays": null,
      "numberOfPayments": 24,
      "calculatedDurationDays": 720,
      "calculatedDurationDisplay": "24 months",
      "apr": 18.00,
      "minDownPaymentPercent": 10,
      "gracePeriodDays": 30,
      "fulfillmentTiming": "AFTER_PAYMENT",
      "isActive": false,
      "isFeatured": false,
      "displayOrder": 3,
      "productId": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
      "productName": "Samsung Galaxy S24 Ultra",
      "shopId": "8d3a7b12-9c4e-4f8a-b5d2-3e6f7a8b9c0d",
      "shopName": "Tech World Store",
      "createdAt": "2025-10-17T14:00:00",
      "updatedAt": "2025-10-18T08:00:00"
    }
  ]
}
```

**Success Response Fields**:
Same as Create Installment Plan endpoint, returned as array

**Error Response Examples**:

*Forbidden (403):*
```json
{
  "success": false,
  "httpStatus": "FORBIDDEN",
  "message": "You do not have permission to view this shop's products",
  "action_time": "2025-10-18T10:30:45",
  "data": "You do not have permission to view this shop's products"
}
```

*Not Found (404):*
```json
{
  "success": false,
  "httpStatus": "NOT_FOUND",
  "message": "Product not found",
  "action_time": "2025-10-18T10:30:45",
  "data": "Product not found"
}
```

---

## 3. Get Installment Plan By ID
**Purpose**: Retrieve detailed information about a specific 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;">GET</span> `{base_url}/{planId}`

**Access Level**: 🔒 Protected (Requires Authentication, Shop Owner Only)

**Authentication**: Bearer Token

**Request Headers**:
| Header | Type | Required | Description |
|--------|------|----------|-------------|
| Authorization | string | Yes | Bearer token for authentication |

**Path Parameters**:
| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| shopId | UUID | Yes | Unique identifier of the shop | Valid UUID format, must be owned by authenticated user |
| productId | UUID | Yes | Unique identifier of the product | Valid UUID format, must belong to specified shop |
| planId | UUID | Yes | Unique identifier of the plan | Valid UUID format, must belong to specified product |

**Success Response JSON Sample**:
```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Installment plan retrieved successfully",
  "action_time": "2025-10-18T10:30:45",
  "data": {
    "planId": "5c6d7e8f-9a0b-4c1d-8e2f-3a4b5c6d7e8f",
    "planName": "12 Month Standard Plan",
    "paymentFrequency": "MONTHLY",
    "paymentFrequencyDisplay": "Monthly",
    "customFrequencyDays": null,
    "numberOfPayments": 12,
    "calculatedDurationDays": 360,
    "calculatedDurationDisplay": "12 months",
    "apr": 15.00,
    "minDownPaymentPercent": 20,
    "gracePeriodDays": 30,
    "fulfillmentTiming": "IMMEDIATE",
    "isActive": true,
    "isFeatured": true,
    "displayOrder": 2,
    "productId": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
    "productName": "Samsung Galaxy S24 Ultra",
    "shopId": "8d3a7b12-9c4e-4f8a-b5d2-3e6f7a8b9c0d",
    "shopName": "Tech World Store",
    "createdAt": "2025-10-16T10:30:00",
    "updatedAt": "2025-10-16T10:30:00",
    "metadata": {}
  }
}
```

**Success Response Fields**:
Same as Create Installment Plan endpoint

**Error Response Examples**:
Same as Get Product Installment Plans endpoint, plus:

*Not Found - Plan (404):*
```json
{
  "success": false,
  "httpStatus": "NOT_FOUND",
  "message": "Installment plan not found",
  "action_time": "2025-10-18T10:30:45",
  "data": "Installment plan not found"
}
```

---

## 4. Update Installment Plan
**Purpose**: Update an existing installment plan's configuration

**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> `{base_url}/{planId}`

**Access Level**: 🔒 Protected (Requires Authentication, Shop Owner Only)

**Authentication**: Bearer Token

**Request Headers**:
| Header | Type | Required | Description |
|--------|------|----------|-------------|
| Authorization | string | Yes | Bearer token for authentication |
| Content-Type | string | Yes | application/json |

**Path Parameters**:
| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| shopId | UUID | Yes | Unique identifier of the shop | Valid UUID format, must be owned by authenticated user |
| productId | UUID | Yes | Unique identifier of the product | Valid UUID format, must belong to specified shop |
| planId | UUID | Yes | Unique identifier of the plan to update | Valid UUID format, must belong to specified product |

**Request JSON Sample**:
```json
{
  "planName": "12 Month Premium Plan",
  "paymentFrequency": "MONTHLY",
  "customFrequencyDays": null,
  "numberOfPayments": 12,
  "apr": 12.00,
  "minDownPaymentPercent": 25,
  "gracePeriodDays": 45,
  "fulfillmentTiming": "IMMEDIATE",
  "displayOrder": 1
}
```

**Request Body Parameters**:
| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| planName | string | No | Updated display name | Min: 3, Max: 100 characters |
| paymentFrequency | string | No | Updated payment frequency | enum: DAILY, WEEKLY, BI_WEEKLY, SEMI_MONTHLY, MONTHLY, QUARTERLY, CUSTOM_DAYS |
| customFrequencyDays | integer | Conditional | Updated custom frequency | Required if paymentFrequency is CUSTOM_DAYS, Min: 1, Max: 365 |
| numberOfPayments | integer | No | Updated number of payments | Min: 2, Max: 120 |
| apr | decimal | No | Updated APR | Min: 0.00, Max: 36.00 |
| minDownPaymentPercent | integer | No | Updated minimum down payment | Min: 10, Max: 50 |
| gracePeriodDays | integer | No | Updated grace period | Min: 0, Max: 60 |
| fulfillmentTiming | string | No | Updated fulfillment option | enum: IMMEDIATE, AFTER_PAYMENT |
| displayOrder | integer | No | Updated display order | Min: 0 |

**Success Response JSON Sample**:
```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Installment plan updated successfully",
  "action_time": "2025-10-18T10:30:45",
  "data": {
    "planId": "5c6d7e8f-9a0b-4c1d-8e2f-3a4b5c6d7e8f",
    "planName": "12 Month Premium Plan",
    "paymentFrequency": "MONTHLY",
    "paymentFrequencyDisplay": "Monthly",
    "customFrequencyDays": null,
    "numberOfPayments": 12,
    "calculatedDurationDays": 360,
    "calculatedDurationDisplay": "12 months",
    "apr": 12.00,
    "minDownPaymentPercent": 25,
    "gracePeriodDays": 45,
    "fulfillmentTiming": "IMMEDIATE",
    "isActive": true,
    "isFeatured": true,
    "displayOrder": 1,
    "productId": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
    "productName": "Samsung Galaxy S24 Ultra",
    "shopId": "8d3a7b12-9c4e-4f8a-b5d2-3e6f7a8b9c0d",
    "shopName": "Tech World Store",
    "createdAt": "2025-10-16T10:30:00",
    "updatedAt": "2025-10-18T10:30:45"
  }
}
```

**Success Response Fields**:
Same as Create Installment Plan endpoint

**Error Response Examples**:
Same as Create Installment Plan endpoint

**Important Notes**:
- Updating a plan only affects new agreements created after the update
- Existing agreements continue using the original terms
- Cannot update isActive or isFeatured via this endpoint (use dedicated endpoints)

---

## 5. Delete Installment Plan
**Purpose**: Permanently delete an installment plan (only if no active agreements exist)

**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}/{planId}`

**Access Level**: 🔒 Protected (Requires Authentication, Shop Owner Only)

**Authentication**: Bearer Token

**Request Headers**:
| Header | Type | Required | Description |
|--------|------|----------|-------------|
| Authorization | string | Yes | Bearer token for authentication |

**Path Parameters**:
| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| shopId | UUID | Yes | Unique identifier of the shop | Valid UUID format, must be owned by authenticated user |
| productId | UUID | Yes | Unique identifier of the product | Valid UUID format, must belong to specified shop |
| planId | UUID | Yes | Unique identifier of the plan to delete | Valid UUID format, must belong to specified product |

**Success Response JSON Sample**:
```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Installment plan deleted successfully",
  "action_time": "2025-10-18T10:30:45",
  "data": null
}
```

**Success Response Fields**:
No data returned on successful deletion

**Error Response Examples**:

*Bad Request - Has Active Agreements (400):*
```json
{
  "success": false,
  "httpStatus": "BAD_REQUEST",
  "message": "Cannot delete plan with active agreements. Deactivate the plan instead.",
  "action_time": "2025-10-18T10:30:45",
  "data": "Cannot delete plan with active agreements. Deactivate the plan instead."
}
```

*Forbidden (403):*
```json
{
  "success": false,
  "httpStatus": "FORBIDDEN",
  "message": "You do not have permission to delete this plan",
  "action_time": "2025-10-18T10:30:45",
  "data": "You do not have permission to delete this plan"
}
```

*Not Found (404):*
```json
{
  "success": false,
  "httpStatus": "NOT_FOUND",
  "message": "Installment plan not found",
  "action_time": "2025-10-18T10:30:45",
  "data": "Installment plan not found"
}
```

---

## 6. Activate Installment Plan
**Purpose**: Activate an installment plan to make it available to customers

**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}/{planId}/activate`

**Access Level**: 🔒 Protected (Requires Authentication, Shop Owner Only)

**Authentication**: Bearer Token

**Request Headers**:
| Header | Type | Required | Description |
|--------|------|----------|-------------|
| Authorization | string | Yes | Bearer token for authentication |

**Path Parameters**:
| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| shopId | UUID | Yes | Unique identifier of the shop | Valid UUID format, must be owned by authenticated user |
| productId | UUID | Yes | Unique identifier of the product | Valid UUID format, must belong to specified shop |
| planId | UUID | Yes | Unique identifier of the plan to activate | Valid UUID format, must belong to specified product |

**Success Response JSON Sample**:
```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Installment plan activated successfully",
  "action_time": "2025-10-18T10:30:45",
  "data": {
    "planId": "6d7e8f9a-0b1c-4d2e-9f3a-4b5c6d7e8f9a",
    "planName": "Budget Friendly Plan",
    "isActive": true,
    "updatedAt": "2025-10-18T10:30:45"
  }
}
```

**Success Response Fields**:
| Field | Description |
|-------|-------------|
| planId | ID of the activated plan |
| planName | Name of the plan |
| isActive | Active status (true) |
| updatedAt | Update timestamp |

**Error Response Examples**:
Same as Delete Installment Plan endpoint

---

## 7. Deactivate Installment Plan
**Purpose**: Deactivate an installment plan to hide it from customers (preserves data for existing agreements)

**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}/{planId}/deactivate`

**Access Level**: 🔒 Protected (Requires Authentication, Shop Owner Only)

**Authentication**: Bearer Token

**Request Headers**:
| Header | Type | Required | Description |
|--------|------|----------|-------------|
| Authorization | string | Yes | Bearer token

## 8. Set Featured Plan
**Purpose**: Mark a plan as featured (recommended/most popular) - only one plan per product can be featured

**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}/{planId}/set-featured`

**Access Level**: 🔒 Protected (Requires Authentication, Shop Owner Only)

**Authentication**: Bearer Token

**Request Headers**:
| Header | Type | Required | Description |
|--------|------|----------|-------------|
| Authorization | string | Yes | Bearer token for authentication |

**Path Parameters**:
| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| shopId | UUID | Yes | Unique identifier of the shop | Valid UUID format, must be owned by authenticated user |
| productId | UUID | Yes | Unique identifier of the product | Valid UUID format, must belong to specified shop |
| planId | UUID | Yes | Unique identifier of the plan to feature | Valid UUID format, must belong to specified product |

**Success Response JSON Sample**:
```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Featured plan set successfully",
  "action_time": "2025-10-18T10:30:45",
  "data": {
    "planId": "5c6d7e8f-9a0b-4c1d-8e2f-3a4b5c6d7e8f",
    "planName": "12 Month Premium Plan",
    "isFeatured": true,
    "previousFeaturedPlanId": "4b5c6d7e-8f9a-4b1c-9d2e-3f4a5b6c7d8e",
    "updatedAt": "2025-10-18T10:30:45"
  }
}
```

**Success Response Fields**:
| Field | Description |
|-------|-------------|
| planId | ID of the newly featured plan |
| planName | Name of the plan |
| isFeatured | Featured status (true) |
| previousFeaturedPlanId | ID of previously featured plan (null if none) |
| updatedAt | Update timestamp |

**Error Response Examples**:
Same as Activate Installment Plan endpoint

**Important Notes**:
- Setting a plan as featured automatically un-features any previously featured plan
- Only active plans should be featured
- Featured plans typically display "Most Popular" or "Recommended" badge
- Featured plans often appear first in plan listings

---

## 9. Enable Installments for Product
**Purpose**: Enable installment payment option for a product (makes all active plans available)

**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}/enable-installments`

**Access Level**: 🔒 Protected (Requires Authentication, Shop Owner Only)

**Authentication**: Bearer Token

**Request Headers**:
| Header | Type | Required | Description |
|--------|------|----------|-------------|
| Authorization | string | Yes | Bearer token for authentication |

**Path Parameters**:
| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| shopId | UUID | Yes | Unique identifier of the shop | Valid UUID format, must be owned by authenticated user |
| productId | UUID | Yes | Unique identifier of the product | Valid UUID format, must belong to specified shop |

**Success Response JSON Sample**:
```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Installments enabled successfully for product",
  "action_time": "2025-10-18T10:30:45",
  "data": {
    "productId": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
    "productName": "Samsung Galaxy S24 Ultra",
    "installmentAvailable": true,
    "activePlansCount": 2,
    "updatedAt": "2025-10-18T10:30:45"
  }
}
```

**Success Response Fields**:
| Field | Description |
|-------|-------------|
| productId | ID of the product |
| productName | Name of the product |
| installmentAvailable | Installment availability status (true) |
| activePlansCount | Number of active plans for this product |
| updatedAt | Update timestamp |

**Error Response Examples**:

*Bad Request - No Plans (400):*
```json
{
  "success": false,
  "httpStatus": "BAD_REQUEST",
  "message": "Cannot enable installments: No installment plans created for this product",
  "action_time": "2025-10-18T10:30:45",
  "data": "Cannot enable installments: No installment plans created for this product"
}
```

*Forbidden (403):*
```json
{
  "success": false,
  "httpStatus": "FORBIDDEN",
  "message": "You do not have permission to modify this product",
  "action_time": "2025-10-18T10:30:45",
  "data": "You do not have permission to modify this product"
}
```

*Not Found (404):*
```json
{
  "success": false,
  "httpStatus": "NOT_FOUND",
  "message": "Product not found",
  "action_time": "2025-10-18T10:30:45",
  "data": "Product not found"
}
```

---

## 10. Disable Installments for Product
**Purpose**: Disable installment payment option for a product (hides all plans from customers)

**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}/disable-installments`

**Access Level**: 🔒 Protected (Requires Authentication, Shop Owner Only)

**Authentication**: Bearer Token

**Request Headers**:
| Header | Type | Required | Description |
|--------|------|----------|-------------|
| Authorization | string | Yes | Bearer token for authentication |

**Path Parameters**:
| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| shopId | UUID | Yes | Unique identifier of the shop | Valid UUID format, must be owned by authenticated user |
| productId | UUID | Yes | Unique identifier of the product | Valid UUID format, must belong to specified shop |

**Success Response JSON Sample**:
```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Installments disabled successfully for product",
  "action_time": "2025-10-18T10:30:45",
  "data": {
    "productId": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
    "productName": "Samsung Galaxy S24 Ultra",
    "installmentAvailable": false,
    "activePlansCount": 2,
    "updatedAt": "2025-10-18T10:30:45"
  }
}
```

**Success Response Fields**:
Same as Enable Installments endpoint

**Error Response Examples**:
Same as Enable Installments endpoint

**Important Notes**:
- Disabling installments hides the option from product page
- Existing agreements continue to function normally
- Plans are preserved and can be reactivated
- Customers with active agreements can still make payments

---

### Common HTTP Status Codes
- `200 OK`: Successful request
- `400 Bad Request`: Invalid request data or business logic violation
- `401 Unauthorized`: Authentication required/failed
- `403 Forbidden`: Insufficient permissions or not shop owner
- `404 Not Found`: Resource not found
- `422 Unprocessable Entity`: Validation errors
- `500 Internal Server Error`: Server error

### Payment Frequency Options
- **DAILY**: Payment every day (1 day)
- **WEEKLY**: Payment every week (7 days)
- **BI_WEEKLY**: Payment every 2 weeks (14 days)
- **SEMI_MONTHLY**: Payment twice per month (~15 days)
- **MONTHLY**: Payment every month (~30 days)
- **QUARTERLY**: Payment every 3 months (~90 days)
- **CUSTOM_DAYS**: Custom interval (1-365 days)

### Fulfillment Timing Options
- **IMMEDIATE**: Product ships after down payment, customer receives product while paying installments
- **AFTER_PAYMENT**: Product ships after final payment (layaway model), inventory held during payment period

### Plan Configuration Constraints
| Parameter | Minimum | Maximum | Notes |
|-----------|---------|---------|-------|
| APR | 0% | 36% | Platform-enforced limit |
| Down Payment | 10% | 50% | Minimum by plan, maximum platform-wide |
| Number of Payments | 2 | 120 | Typical range: 4-24 payments |
| Grace Period | 0 days | 60 days | Days before first payment |
| Custom Frequency | 1 day | 365 days | Only for CUSTOM_DAYS type |
| Plan Name | 3 chars | 100 chars | Must be unique per product |

### Plan States and Lifecycle

#### Active Plan
- Visible to customers on product page
- Can be selected during checkout
- Creates new agreements
- Appears in public API responses

#### Inactive Plan
- Hidden from customers
- Cannot be selected for new purchases
- Existing agreements continue
- Visible only to shop owner

#### Featured Plan
- Displays "Most Popular" or "Recommended" badge
- Only one plan per product can be featured
- Typically listed first in plan options
- Used for promoting preferred payment terms

#### Deleted Plan
- Permanently removed from system
- Only possible if no agreements exist
- Cannot be recovered
- Use deactivation instead for preservation

### Business Rules Summary

1. **One Featured Plan**: Only one plan per product can be featured at a time
2. **Shop Ownership**: Only shop owners can manage their product plans
3. **Active Plans Only**: Customers only see active plans for products with installments enabled
4. **No Deletion with Agreements**: Plans with existing agreements cannot be deleted, only deactivated
5. **Update Impact**: Plan updates only affect new agreements, not existing ones
6. **Product-Level Control**: Disabling installments at product level hides all plans
7. **Duration Calculation**: System automatically calculates duration based on frequency and payment count
8. **Validation**: All parameters validated against platform constraints
9. **Plan Minimum**: At least one active plan required to enable installments on product
10. **Historical Preservation**: Deactivated plans preserved for agreement history

### Common Workflows

#### Creating a New Installment Plan
1. Ensure product exists and you own the shop
2. POST /installment-plans with plan configuration
3. Verify plan appears in GET /installment-plans
4. Optionally set as featured via PATCH /set-featured
5. Enable installments on product if not already enabled

#### Managing Multiple Plans
1. Create multiple plans for different customer segments
   - Quick payment plan (8 weeks, 10% APR)
   - Standard plan (12 months, 15% APR)
   - Budget plan (24 months, 18% APR)
2. Set most balanced plan as featured
3. Order by display priority (displayOrder field)
4. Monitor customer preferences and adjust

#### Temporarily Disabling Plans
1. PATCH /deactivate for specific plan
2. OR PATCH /disable-installments for all plans
3. Existing agreements continue normally
4. Reactivate when ready with PATCH /activate or /enable-installments

#### Updating Plan Terms
1. Analyze current agreement performance
2. PUT /installment-plans/{planId} with new terms
3. New terms apply only to future agreements
4. Consider creating new plan for major changes

#### Deleting Unused Plans
1. Check if plan has any agreements (attempt delete will fail if so)
2. DELETE /installment-plans/{planId}
3. Plan permanently removed
4. If has agreements, use deactivate instead

#### Client-Side Validation
- Validate shop/product ownership before API call
- Check APR within 0-36% range
- Verify down payment within 10-50% range
- Validate payment count between 2-120
- Ensure customFrequencyDays present for CUSTOM_DAYS
- Check plan name length (3-100 characters)

#### Server-Side Error Handling
- 400: Display user-friendly message, allow retry
- 403: Show "Insufficient permissions" message
- 404: Redirect to product list or show error
- 422: Display field-specific validation errors
- 500: Generic error message, log for investigation

### Testing Scenarios

#### Happy Path
1. Create plan with valid parameters
2. Retrieve plan and verify all fields
3. Update plan configuration
4. Set as featured plan
5. Activate/deactivate plan
6. Enable/disable product installments

#### Edge Cases
1. Create multiple plans with same parameters
2. Set featured when no featured plan exists
3. Set featured when another plan is featured
4. Update plan to use custom frequency
5. Deactivate only active plan
6. Enable installments with no active plans

#### Error Cases
1. Invalid shop/product IDs → 404
2. Non-owner attempting management → 403
3. APR outside 0-36% range → 422
4. Down payment outside 10-50% → 422
5. Delete plan with agreements → 400
6. Missing required fields → 422
7. Invalid payment frequency → 422
8. Custom days without CUSTOM_DAYS → 422

### Performance Considerations
- Cache plan data at product level (TTL: 30 minutes)
- Invalidate cache on plan create/update/delete
- Batch operations when managing multiple plans
- Use pagination for products with many plans
- Index on productId for fast retrieval

### Security Considerations
- Always validate shop ownership
- Verify product belongs to shop
- Rate limiting: 100 requests/hour per user
- Input validation on all parameters
- SQL injection prevention (parameterized queries)
- XSS prevention (sanitize all inputs)
- Audit log all plan changes

### Monitoring and Analytics
Track the following metrics:
- Plans created per shop
- Plan activation/deactivation frequency
- Featured plan changes
- Plans by payment frequency
- Average APR across plans
- Popular plan configurations
- Plans with most agreements

### Best Practices for Shop Owners

#### Plan Design Strategy
1. **Offer 2-3 plans** for different budgets
   - Quick plan: Short term, low interest
   - Standard plan: Medium term, moderate interest
   - Budget plan: Long term, higher interest
2. **Use clear naming**: "8-Week Quick Pay", "12-Month Standard"
3. **Set appropriate featured plan**: Balance of affordability and profit
4. **Adjust based on data**: Monitor which plans customers choose

#### APR Configuration
- **Premium products**: Lower APR (8-12%)
- **Standard products**: Moderate APR (12-18%)
- **High-risk categories**: Higher APR (18-24%)
- **Promotional periods**: Reduced APR (0-10%)

#### Down Payment Strategy
- **High-value items**: Higher down payment (25-40%)
- **Impulse purchases**: Lower down payment (10-20%)
- **Luxury items**: Flexible range (15-50%)
- **Clearance items**: Fixed minimum (10-15%)

#### Grace Period Guidelines
- **Weekly payments**: 7-14 days grace
- **Monthly payments**: 30-45 days grace
- **Seasonal products**: Align with customer cash flow
- **New customers**: Shorter grace (7-15 days)

### Common Mistakes to Avoid
1. Too many plans (confuses customers) - stick to 2-4
2. Too high APR (customers avoid) - keep competitive
3. Too low down payment (higher default risk) - minimum 15-20%
4. Deleting active plans - deactivate instead
5. Not setting featured plan - customers want guidance
6. Inconsistent plan naming - use clear conventions
7. Not updating based on performance - review quarterly
8. Enabling without active plans - create plans first

### Support & Resources
- **API Documentation**: https://docs.nextgate.com/api/installment-plans
- **Shop Owner Guide**: https://help.nextgate.com/shop/installments
- **Developer Portal**: https://developers.nextgate.com
- **Support Email**: shop-support@nextgate.com
- **Status Page**: https://status.nextgate.com
- **Video Tutorials**: https://academy.nextgate.com/installments

### Version 1.0 (2025-10-18)
- Initial release
- Full CRUD operations for installment plans
- Plan activation/deactivation
- Featured plan management
- Product-level installment control
- Shop ownership validation
- Comprehensive error handling
- Audit logging support for authentication |
| Content-Type | string | Yes | application/json |

**Path Parameters**:
| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| shopId | UUID | Yes | Unique identifier of the shop | Valid UUID format, must be owned by authenticated user |
| productId | UUID | Yes | Unique identifier of the product | Valid UUID format, must belong to specified shop |

**Request JSON Sample**:
```json
{
  "planName": "12 Month Standard Plan",
  "paymentFrequency": "MONTHLY",
  "customFrequencyDays": null,
  "numberOfPayments": 12,
  "apr": 15.00,
  "minDownPaymentPercent": 20,
  "gracePeriodDays": 30,
  "fulfillmentTiming": "IMMEDIATE",
  "isActive": true,
  "isFeatured": false,
  "displayOrder": 1
}
```

**Request Body Parameters**:
| Parameter | Type | Required | Description | Validation |
|-----------|------|----------|-------------|------------|
| planName | string | Yes | Display name for the plan | Min: 3, Max: 100 characters |
| paymentFrequency | string | Yes | Payment frequency type | enum: DAILY, WEEKLY, BI_WEEKLY, SEMI_MONTHLY, MONTHLY, QUARTERLY, CUSTOM_DAYS |
| customFrequencyDays | integer | Conditional | Custom frequency in days | Required if paymentFrequency is CUSTOM_DAYS, Min: 1, Max: 365 |
| numberOfPayments | integer | Yes | Total number of payments | Min: 2, Max: 120 |
| apr | decimal | Yes | Annual Percentage Rate | Min: 0.00, Max: 36.00 |
| minDownPaymentPercent | integer | Yes | Minimum down payment percentage | Min: 10, Max: 50 |
| gracePeriodDays | integer | Yes | Days before first payment due | Min: 0, Max: 60 |
| fulfillmentTiming | string | Yes | When to ship product | enum: IMMEDIATE, AFTER_PAYMENT |
| isActive | boolean | No | Whether plan is active | Default: true |
| isFeatured | boolean | No | Whether plan is featured | Default: false |
| displayOrder | integer | No | Display order on product page | Min: 0, Default: 0 |

**Success Response JSON Sample**:
```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Installment plan created successfully",
  "action_time": "2025-10-18T10:30:45",
  "data": {
    "planId": "5c6d7e8f-9a0b-4c1d-8e2f-3a4b5c6d7e8f",
    "planName": "12 Month Standard Plan",
    "paymentFrequency": "MONTHLY",
    "paymentFrequencyDisplay": "Monthly",
    "customFrequencyDays": null,
    "numberOfPayments": 12,
    "calculatedDurationDays": 360,
    "calculatedDurationDisplay": "12 months",
    "apr": 15.00,
    "minDownPaymentPercent": 20,
    "gracePeriodDays": 30,
    "fulfillmentTiming": "IMMEDIATE",
    "isActive": true,
    "isFeatured": false,
    "displayOrder": 1,
    "productId": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
    "productName": "Samsung Galaxy S24 Ultra",
    "shopId": "8d3a7b12-9c4e-4f8a-b5d2-3e6f7a8b9c0d",
    "shopName": "Tech World Store",
    "createdAt": "2025-10-18T10:30:45",
    "updatedAt": "2025-10-18T10:30:45"
  }
}
```

**Success Response Fields**:
| Field | Description |
|-------|-------------|
| planId | Unique identifier for the created plan |
| planName | Name of the plan |
| paymentFrequency | Payment frequency enum value |
| paymentFrequencyDisplay | Human-readable frequency |
| customFrequencyDays | Custom days (if CUSTOM_DAYS type) |
| numberOfPayments | Total number of payments |
| calculatedDurationDays | Auto-calculated total duration in days |
| calculatedDurationDisplay | Human-readable duration |
| apr | Annual Percentage Rate |
| minDownPaymentPercent | Minimum down payment percentage |
| gracePeriodDays | Grace period in days |
| fulfillmentTiming | Fulfillment option |
| isActive | Active status |
| isFeatured | Featured status |
| displayOrder | Display order |
| productId | Associated product ID |
| productName | Associated product name |
| shopId | Associated shop ID |
| shopName | Associated shop name |
| createdAt | Creation timestamp |
| updatedAt | Last update timestamp |

**Error Response Examples**:

*Bad Request - Invalid APR (400):*
```json
{
  "success": false,
  "httpStatus": "BAD_REQUEST",
  "message": "APR must be between 0% and 36%",
  "action_time": "2025-10-18T10:30:45",
  "data": "APR must be between 0% and 36%"
}
```

*Bad Request - Invalid Payment Count (400):*
```json
{
  "success": false,
  "httpStatus": "BAD_REQUEST",
  "message": "Number of payments must be between 2 and 120",
  "action_time": "2025-10-18T10:30:45",
  "data": "Number of payments must be between 2 and 120"
}
```

*Validation Error (422):*
```json
{
  "success": false,
  "httpStatus": "UNPROCESSABLE_ENTITY",
  "message": "Validation failed",
  "action_time": "2025-10-18T10:30:45",
  "data": {
    "planName": "Plan name is required",
    "apr": "must be between 0 and 36",
    "minDownPaymentPercent": "must be between 10 and 50"
  }
}
```

*Forbidden - Not Shop Owner (403):*
```json
{
  "success": false,
  "httpStatus": "FORBIDDEN",
  "message": "You do not have permission to manage this shop's products",
  "action_time": "2025-10-18T10:30:45",
  "data": "You do not have permission to manage this shop's products"
}
```

*Not Found - Product (404):*
```json
{
  "success": false,
  "httpStatus": "NOT_FOUND",
  "message": "Product not found",
  "action_time": "2025-10-18T10:30:45",
  "data": "Product not found"
}
```

---

# Order Management

**Author**: Josh S. Sakweli, Backend Lead Team **Last Updated:** 2026-06-10 **Version:** v1.0

**Base URL:** `api/v1/e-commerce/orders`

**Short Description**: The Order Management API handles the complete order lifecycle for the NextGate e-commerce platform. It supports multiple purchase types, order tracking, shipping management, delivery confirmation with 6-digit codes, escrow integration, and digital product downloads.

**Hints**:

- All endpoints require Bearer token authentication
- Order sources: DIRECT\_PURCHASE, CART\_PURCHASE, DIGITAL\_PURCHASE, INSTALLMENT, GROUP\_PURCHASE
- Delivery confirmation uses a 6-digit code (SHA-256 hashed with salt, expires in 30 days, max 5 attempts)
- Digital orders have `deliveryStatus: NOT_APPLICABLE`
- Confirm-delivery response is returned directly (not wrapped in the standard response envelope)
- Every order detail response includes a `timeline` array — ordered list of status steps with timestamps. Steps not yet reached have `timestamp: null` and `isCompleted: false`

---

## Endpoints

### 1. Get Order by ID

**Purpose:** Retrieve detailed information about a specific order.

**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}/{orderId}`

**Access Level:** 🔒 Protected (Buyer or Seller only)

**Authentication:** Bearer Token

**Request Headers:**

<table id="bkmrk-header-type-required"><thead><tr><th>Header</th><th>Type</th><th>Required</th><th>Description</th></tr></thead><tbody><tr><td>Authorization</td><td>string</td><td>Yes</td><td>Bearer token for authentication</td></tr></tbody></table>

**Path Parameters:**

<table id="bkmrk-parameter-type-requi"><thead><tr><th>Parameter</th><th>Type</th><th>Required</th><th>Description</th></tr></thead><tbody><tr><td>orderId</td><td>UUID</td><td>Yes</td><td>Unique identifier of the order</td></tr></tbody></table>

**Response JSON Sample:**

```json
{
  "success": true,
  "message": "Order retrieved successfully",
  "data": {
    "orderId": "550e8400-e29b-41d4-a716-446655440000",
    "orderNumber": "ORD-2025-12345",
    "buyer": {
      "accountId": "123e4567-e89b-12d3-a456-426614174000",
      "userName": "johndoe",
      "email": "john@example.com",
      "firstName": "John",
      "lastName": "Doe"
    },
    "seller": {
      "shopId": "789e0123-e45b-67d8-a901-234567890abc",
      "shopName": "TechStore",
      "shopLogo": "https://cdn.example.com/shops/techstore.png",
      "shopSlug": "techstore"
    },
    "productOrderStatus": "SHIPPED",
    "deliveryStatus": "IN_TRANSIT",
    "productOrderSource": "DIRECT_PURCHASE",
    "items": [
      {
        "orderItemId": "111e2222-e33b-44d5-a666-777788889999",
        "productId": "abc12345-def6-7890-ghij-klmnopqrstuv",
        "productName": "Wireless Headphones",
        "productSlug": "wireless-headphones",
        "productImage": "https://cdn.example.com/products/headphones.jpg",
        "productType": "PHYSICAL",
        "fileIds": null,
        "quantity": 2,
        "unitPrice": 85000.00,
        "subtotal": 170000.00,
        "tax": 0.00,
        "total": 170000.00
      }
    ],
    "subtotal": 170000.00,
    "shippingFee": 5000.00,
    "tax": 0.00,
    "totalAmount": 175000.00,
    "platformFee": 8750.00,
    "sellerAmount": 166250.00,
    "currency": "TZS",
    "paymentMethod": "MPESA",
    "amountPaid": 175000.00,
    "amountRemaining": 0.00,
    "deliveryAddress": "123 Main St, Dar es Salaam, Tanzania",
    "trackingNumber": "TRACK-550E8400",
    "carrier": "NextGate Shipping",
    "isDeliveryConfirmed": false,
    "deliveryConfirmedAt": null,
    "orderedAt": "2025-10-20T14:30:00",
    "shippedAt": "2025-10-21T09:15:00",
    "deliveredAt": null,
    "cancelledAt": null,
    "cancellationReason": null,
    "timeline": [
      {
        "status": "ORDER_PLACED",
        "label": "Order Placed",
        "timestamp": "2025-10-20T14:30:00",
        "isCompleted": true,
        "note": null
      },
      {
        "status": "SHIPPED",
        "label": "Shipped",
        "timestamp": "2025-10-21T09:15:00",
        "isCompleted": true,
        "note": "NextGate Shipping · TRACK-550E8400"
      },
      {
        "status": "DELIVERED",
        "label": "Delivered",
        "timestamp": null,
        "isCompleted": false,
        "note": null
      },
      {
        "status": "COMPLETED",
        "label": "Order Completed",
        "timestamp": null,
        "isCompleted": false,
        "note": null
      }
    ]
  }
}

```

**Response Fields:**

<table id="bkmrk-field-description-or"><thead><tr><th>Field</th><th>Description</th></tr></thead><tbody><tr><td>orderId</td><td>Unique identifier of the order</td></tr><tr><td>orderNumber</td><td>Human-readable order number</td></tr><tr><td>buyer</td><td>Buyer account info (accountId, userName, email, firstName, lastName)</td></tr><tr><td>seller</td><td>Shop info (shopId, shopName, shopLogo, shopSlug)</td></tr><tr><td>productOrderStatus</td><td>PENDING\_PAYMENT, PENDING\_SHIPMENT, SHIPPED, DELIVERED, COMPLETED, CANCELLED, REFUNDED</td></tr><tr><td>deliveryStatus</td><td>PENDING, SHIPPED, IN\_TRANSIT, DELIVERED, CONFIRMED, NOT\_APPLICABLE</td></tr><tr><td>productOrderSource</td><td>DIRECT\_PURCHASE, CART\_PURCHASE, DIGITAL\_PURCHASE, INSTALLMENT, GROUP\_PURCHASE</td></tr><tr><td>items</td><td>Array of order items — see item fields below</td></tr><tr><td>items\[\].productType</td><td>`PHYSICAL` or `DIGITAL` — frontend uses this to show tracking UI vs download UI</td></tr><tr><td>items\[\].fileIds</td><td>List of file UUIDs for the item — populated only when `productType` is `DIGITAL`, `null` for physical. Use these with endpoint 14/15 to download files</td></tr><tr><td>subtotal</td><td>Sum of all item totals before shipping and tax</td></tr><tr><td>shippingFee</td><td>Shipping cost</td></tr><tr><td>tax</td><td>Tax amount</td></tr><tr><td>totalAmount</td><td>Final amount (subtotal + shipping + tax)</td></tr><tr><td>platformFee</td><td>Platform commission</td></tr><tr><td>sellerAmount</td><td>Amount seller receives after platform fee</td></tr><tr><td>currency</td><td>Currency code (TZS)</td></tr><tr><td>paymentMethod</td><td>Payment method used</td></tr><tr><td>amountPaid</td><td>Amount already paid</td></tr><tr><td>amountRemaining</td><td>Remaining balance (installment orders)</td></tr><tr><td>deliveryAddress</td><td>Shipping address</td></tr><tr><td>trackingNumber</td><td>Shipping tracking number (null until shipped)</td></tr><tr><td>carrier</td><td>Shipping carrier (null until shipped)</td></tr><tr><td>isDeliveryConfirmed</td><td>Whether buyer confirmed delivery</td></tr><tr><td>deliveryConfirmedAt</td><td>Timestamp of delivery confirmation (null if not confirmed)</td></tr><tr><td>orderedAt</td><td>Order creation timestamp</td></tr><tr><td>shippedAt</td><td>Shipping timestamp (null until shipped)</td></tr><tr><td>deliveredAt</td><td>Delivery timestamp (null until delivered)</td></tr><tr><td>cancelledAt</td><td>Cancellation timestamp (null if not cancelled)</td></tr><tr><td>cancellationReason</td><td>Reason for cancellation (null if not cancelled)</td></tr><tr><td>timeline</td><td>Ordered list of status steps — see Timeline Fields below</td></tr><tr><td>timeline\[\].status</td><td>Step identifier: `ORDER_PLACED`, `SHIPPED`, `DELIVERED`, `COMPLETED`, `FILES_AVAILABLE` (digital), `CANCELLED`, `DISPUTED`, `REFUNDED`</td></tr><tr><td>timeline\[\].label</td><td>Human-readable step label</td></tr><tr><td>timeline\[\].timestamp</td><td>When this step occurred (`null` if not yet reached)</td></tr><tr><td>timeline\[\].isCompleted</td><td>`true` if this step has been reached</td></tr><tr><td>timeline\[\].note</td><td>Optional context — shipping carrier + tracking on SHIPPED, cancellation reason on CANCELLED, "Confirmed by buyer" or "Auto-confirmed" on COMPLETED, `null` otherwise</td></tr></tbody></table>

**Error Responses:**

- `400 Bad Request`: Access denied — user is not buyer or seller of this order
- `401 Unauthorized`: Authentication required
- `404 Not Found`: Order not found

---

### 2. Get Order by Order Number

**Purpose:** Retrieve order details using the human-readable order number.

**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}/number/{orderNumber}`

**Access Level:** 🔒 Protected (Buyer or Seller only)

**Authentication:** Bearer Token

**Request Headers:**

<table id="bkmrk-header-type-required-1"><thead><tr><th>Header</th><th>Type</th><th>Required</th><th>Description</th></tr></thead><tbody><tr><td>Authorization</td><td>string</td><td>Yes</td><td>Bearer token for authentication</td></tr></tbody></table>

**Path Parameters:**

<table id="bkmrk-parameter-type-requi-1"><thead><tr><th>Parameter</th><th>Type</th><th>Required</th><th>Description</th></tr></thead><tbody><tr><td>orderNumber</td><td>string</td><td>Yes</td><td>Human-readable order number (e.g. ORD-2025-12345)</td></tr></tbody></table>

**Response:** Same structure as Get Order by ID.

**Error Responses:**

- `400 Bad Request`: Access denied — user is not buyer or seller of this order
- `401 Unauthorized`: Authentication required
- `404 Not Found`: Order not found

---

### 3. Get My Orders

**Purpose:** Retrieve all orders for the authenticated customer.

**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}/my-orders`

**Access Level:** 🔒 Protected (Customer only)

**Authentication:** Bearer Token

**Request Headers:**

<table id="bkmrk-header-type-required-2"><thead><tr><th>Header</th><th>Type</th><th>Required</th><th>Description</th></tr></thead><tbody><tr><td>Authorization</td><td>string</td><td>Yes</td><td>Bearer token for authentication</td></tr></tbody></table>

**Response JSON Sample:**

```
{
  "success": true,
  "message": "Orders retrieved successfully",
  "data": [ ...array of order objects (same structure as endpoint 1)... ]
}

```

**Error Responses:**

- `401 Unauthorized`: Authentication required
- `404 Not Found`: User account not found

---

### 4. Get My Orders by Status

**Purpose:** Retrieve customer orders filtered by order status.

**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}/my-orders/status/{status}`

**Access Level:** 🔒 Protected (Customer only)

**Authentication:** Bearer Token

**Request Headers:**

<table id="bkmrk-header-type-required-3"><thead><tr><th>Header</th><th>Type</th><th>Required</th><th>Description</th></tr></thead><tbody><tr><td>Authorization</td><td>string</td><td>Yes</td><td>Bearer token for authentication</td></tr></tbody></table>

**Path Parameters:**

<table id="bkmrk-parameter-type-requi-2"><thead><tr><th>Parameter</th><th>Type</th><th>Required</th><th>Description</th></tr></thead><tbody><tr><td>status</td><td>enum</td><td>Yes</td><td>PENDING\_PAYMENT, PENDING\_SHIPMENT, SHIPPED, DELIVERED, COMPLETED, CANCELLED, REFUNDED</td></tr></tbody></table>

**Response:** Array of order objects (same structure as endpoint 1).

**Error Responses:**

- `400 Bad Request`: Invalid status value
- `401 Unauthorized`: Authentication required
- `404 Not Found`: User account not found

---

### 5. Get My Orders (Paginated)

**Purpose:** Retrieve customer orders with pagination.

**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}/my-orders/paged`

**Access Level:** 🔒 Protected (Customer only)

**Authentication:** Bearer Token

**Request Headers:**

<table id="bkmrk-header-type-required-4"><thead><tr><th>Header</th><th>Type</th><th>Required</th><th>Description</th></tr></thead><tbody><tr><td>Authorization</td><td>string</td><td>Yes</td><td>Bearer token for authentication</td></tr></tbody></table>

**Query Parameters:**

<table id="bkmrk-parameter-type-requi-3"><thead><tr><th>Parameter</th><th>Type</th><th>Required</th><th>Default</th><th>Description</th></tr></thead><tbody><tr><td>page</td><td>integer</td><td>No</td><td>1</td><td>Page number (1-based)</td></tr><tr><td>size</td><td>integer</td><td>No</td><td>10</td><td>Number of items per page</td></tr></tbody></table>

**Response JSON Sample:**

```json
{
  "success": true,
  "message": "Orders retrieved successfully",
  "data": {
    "orders": [ "...order objects (same structure as endpoint 1)..." ],
    "currentPage": 1,
    "pageSize": 10,
    "totalElements": 25,
    "totalPages": 3,
    "hasNext": true,
    "hasPrevious": false,
    "isFirst": true,
    "isLast": false
  }
}

```

**Error Responses:**

- `401 Unauthorized`: Authentication required
- `404 Not Found`: User account not found

---

### 6. Get My Orders by Status (Paginated)

**Purpose:** Retrieve customer orders filtered by status with pagination.

**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}/my-orders/status/{status}/paged`

**Access Level:** 🔒 Protected (Customer only)

**Authentication:** Bearer Token

**Request Headers:**

<table id="bkmrk-header-type-required-5"><thead><tr><th>Header</th><th>Type</th><th>Required</th><th>Description</th></tr></thead><tbody><tr><td>Authorization</td><td>string</td><td>Yes</td><td>Bearer token for authentication</td></tr></tbody></table>

**Path Parameters:**

<table id="bkmrk-parameter-type-requi-4"><thead><tr><th>Parameter</th><th>Type</th><th>Required</th><th>Description</th></tr></thead><tbody><tr><td>status</td><td>enum</td><td>Yes</td><td>PENDING\_PAYMENT, PENDING\_SHIPMENT, SHIPPED, DELIVERED, COMPLETED, CANCELLED, REFUNDED</td></tr></tbody></table>

**Query Parameters:**

<table id="bkmrk-parameter-type-requi-5"><thead><tr><th>Parameter</th><th>Type</th><th>Required</th><th>Default</th><th>Description</th></tr></thead><tbody><tr><td>page</td><td>integer</td><td>No</td><td>1</td><td>Page number (1-based)</td></tr><tr><td>size</td><td>integer</td><td>No</td><td>10</td><td>Number of items per page</td></tr></tbody></table>

**Response:** Same paginated structure as endpoint 5.

**Error Responses:**

- `400 Bad Request`: Invalid status value
- `401 Unauthorized`: Authentication required
- `404 Not Found`: User account not found

---

### 7. Get Shop Orders

**Purpose:** Retrieve all orders for a specific shop.

**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}/shop/{shopId}/orders`

**Access Level:** 🔒 Protected (Shop Owner only)

**Authentication:** Bearer Token

**Request Headers:**

<table id="bkmrk-header-type-required-6"><thead><tr><th>Header</th><th>Type</th><th>Required</th><th>Description</th></tr></thead><tbody><tr><td>Authorization</td><td>string</td><td>Yes</td><td>Bearer token for authentication</td></tr></tbody></table>

**Path Parameters:**

<table id="bkmrk-parameter-type-requi-6"><thead><tr><th>Parameter</th><th>Type</th><th>Required</th><th>Description</th></tr></thead><tbody><tr><td>shopId</td><td>UUID</td><td>Yes</td><td>Unique identifier of the shop</td></tr></tbody></table>

**Response:** Array of order objects (same structure as endpoint 1).

**Error Responses:**

- `400 Bad Request`: User is not the owner of this shop
- `401 Unauthorized`: Authentication required
- `404 Not Found`: Shop not found

---

### 8. Get Shop Orders by Status

**Purpose:** Retrieve shop orders filtered by order status.

**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}/shop/{shopId}/orders/status/{status}`

**Access Level:** 🔒 Protected (Shop Owner only)

**Authentication:** Bearer Token

**Request Headers:**

<table id="bkmrk-header-type-required-7"><thead><tr><th>Header</th><th>Type</th><th>Required</th><th>Description</th></tr></thead><tbody><tr><td>Authorization</td><td>string</td><td>Yes</td><td>Bearer token for authentication</td></tr></tbody></table>

**Path Parameters:**

<table id="bkmrk-parameter-type-requi-7"><thead><tr><th>Parameter</th><th>Type</th><th>Required</th><th>Description</th></tr></thead><tbody><tr><td>shopId</td><td>UUID</td><td>Yes</td><td>Unique identifier of the shop</td></tr><tr><td>status</td><td>enum</td><td>Yes</td><td>PENDING\_PAYMENT, PENDING\_SHIPMENT, SHIPPED, DELIVERED, COMPLETED, CANCELLED, REFUNDED</td></tr></tbody></table>

**Response:** Array of order objects (same structure as endpoint 1).

**Error Responses:**

- `400 Bad Request`: Invalid status value or user is not shop owner
- `401 Unauthorized`: Authentication required
- `404 Not Found`: Shop not found

---

### 9. Get Shop Orders (Paginated)

**Purpose:** Retrieve shop orders with pagination.

**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}/shop/{shopId}/orders/paged`

**Access Level:** 🔒 Protected (Shop Owner only)

**Authentication:** Bearer Token

**Request Headers:**

<table id="bkmrk-header-type-required-8"><thead><tr><th>Header</th><th>Type</th><th>Required</th><th>Description</th></tr></thead><tbody><tr><td>Authorization</td><td>string</td><td>Yes</td><td>Bearer token for authentication</td></tr></tbody></table>

**Path Parameters:**

<table id="bkmrk-parameter-type-requi-8"><thead><tr><th>Parameter</th><th>Type</th><th>Required</th><th>Description</th></tr></thead><tbody><tr><td>shopId</td><td>UUID</td><td>Yes</td><td>Unique identifier of the shop</td></tr></tbody></table>

**Query Parameters:**

<table id="bkmrk-parameter-type-requi-9"><thead><tr><th>Parameter</th><th>Type</th><th>Required</th><th>Default</th><th>Description</th></tr></thead><tbody><tr><td>page</td><td>integer</td><td>No</td><td>1</td><td>Page number (1-based)</td></tr><tr><td>size</td><td>integer</td><td>No</td><td>10</td><td>Number of items per page</td></tr></tbody></table>

**Response:** Same paginated structure as endpoint 5.

**Error Responses:**

- `400 Bad Request`: User is not the owner of this shop
- `401 Unauthorized`: Authentication required
- `404 Not Found`: Shop not found

---

### 10. Get Shop Orders by Status (Paginated)

**Purpose:** Retrieve shop orders filtered by status with pagination.

**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}/shop/{shopId}/orders/status/{status}/paged`

**Access Level:** 🔒 Protected (Shop Owner only)

**Authentication:** Bearer Token

**Request Headers:**

<table id="bkmrk-header-type-required-9"><thead><tr><th>Header</th><th>Type</th><th>Required</th><th>Description</th></tr></thead><tbody><tr><td>Authorization</td><td>string</td><td>Yes</td><td>Bearer token for authentication</td></tr></tbody></table>

**Path Parameters:**

<table id="bkmrk-parameter-type-requi-10"><thead><tr><th>Parameter</th><th>Type</th><th>Required</th><th>Description</th></tr></thead><tbody><tr><td>shopId</td><td>UUID</td><td>Yes</td><td>Unique identifier of the shop</td></tr><tr><td>status</td><td>enum</td><td>Yes</td><td>PENDING\_PAYMENT, PENDING\_SHIPMENT, SHIPPED, DELIVERED, COMPLETED, CANCELLED, REFUNDED</td></tr></tbody></table>

**Query Parameters:**

<table id="bkmrk-parameter-type-requi-11"><thead><tr><th>Parameter</th><th>Type</th><th>Required</th><th>Default</th><th>Description</th></tr></thead><tbody><tr><td>page</td><td>integer</td><td>No</td><td>1</td><td>Page number (1-based)</td></tr><tr><td>size</td><td>integer</td><td>No</td><td>10</td><td>Number of items per page</td></tr></tbody></table>

**Response:** Same paginated structure as endpoint 5.

**Error Responses:**

- `400 Bad Request`: Invalid status value or user is not shop owner
- `401 Unauthorized`: Authentication required
- `404 Not Found`: Shop not found

---

### 11. Mark Order as Shipped

**Purpose:** Seller marks an order as shipped. Generates a delivery confirmation code and sends it to the buyer.

**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}/{orderId}/ship`

**Access Level:** 🔒 Protected (Shop Owner/Seller only)

**Authentication:** Bearer Token

**Request Headers:**

<table id="bkmrk-header-type-required-10"><thead><tr><th>Header</th><th>Type</th><th>Required</th><th>Description</th></tr></thead><tbody><tr><td>Authorization</td><td>string</td><td>Yes</td><td>Bearer token for authentication</td></tr></tbody></table>

**Path Parameters:**

<table id="bkmrk-parameter-type-requi-12"><thead><tr><th>Parameter</th><th>Type</th><th>Required</th><th>Description</th></tr></thead><tbody><tr><td>orderId</td><td>UUID</td><td>Yes</td><td>Unique identifier of the order</td></tr></tbody></table>

**Response JSON Sample:**

```json
{
  "success": true,
  "message": "Order marked as shipped",
  "data": {
    "orderId": "550e8400-e29b-41d4-a716-446655440000",
    "orderNumber": "ORD-2025-12345",
    "shippedAt": "2025-10-25T10:30:45",
    "message": "Order marked as shipped. Confirmation code sent to customer.",
    "confirmationCodeSent": true,
    "codeExpiresAt": "2025-11-24T10:30:45",
    "maxVerificationAttempts": 5
  }
}

```

**Response Fields:**

<table id="bkmrk-field-description-or-1"><thead><tr><th>Field</th><th>Description</th></tr></thead><tbody><tr><td>orderId</td><td>UUID of the shipped order</td></tr><tr><td>orderNumber</td><td>Human-readable order number</td></tr><tr><td>shippedAt</td><td>Timestamp when order was marked as shipped</td></tr><tr><td>message</td><td>Confirmation message</td></tr><tr><td>confirmationCodeSent</td><td>Whether confirmation code was sent to customer</td></tr><tr><td>codeExpiresAt</td><td>When the confirmation code expires (30 days from generation)</td></tr><tr><td>maxVerificationAttempts</td><td>Maximum number of code verification attempts allowed</td></tr></tbody></table>

**Error Responses:**

- `400 Bad Request`: Order is a digital order (does not require shipping), order status is not PENDING\_SHIPMENT, or user is not the seller
- `401 Unauthorized`: Authentication required
- `404 Not Found`: Order not found

---

### 12. Confirm Delivery

**Purpose:** Customer confirms order delivery using the 6-digit confirmation code. Releases escrow to seller.

**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}/{orderId}/confirm-delivery`

**Access Level:** 🔒 Protected (Buyer/Customer only)

**Authentication:** Bearer Token

**Request Headers:**

<table id="bkmrk-header-type-required-11"><thead><tr><th>Header</th><th>Type</th><th>Required</th><th>Description</th></tr></thead><tbody><tr><td>Authorization</td><td>string</td><td>Yes</td><td>Bearer token for authentication</td></tr><tr><td>User-Agent</td><td>string</td><td>No</td><td>Device info for verification tracking</td></tr><tr><td>X-Forwarded-For</td><td>string</td><td>No</td><td>Client IP address (if behind proxy)</td></tr><tr><td>X-Real-IP</td><td>string</td><td>No</td><td>Real client IP address</td></tr></tbody></table>

**Path Parameters:**

<table id="bkmrk-parameter-type-requi-13"><thead><tr><th>Parameter</th><th>Type</th><th>Required</th><th>Description</th></tr></thead><tbody><tr><td>orderId</td><td>UUID</td><td>Yes</td><td>Unique identifier of the order</td></tr></tbody></table>

**Request JSON Sample:**

```json
{
  "confirmationCode": "123456"
}

```

**Request Body Parameters:**

<table id="bkmrk-parameter-type-requi-14"><thead><tr><th>Parameter</th><th>Type</th><th>Required</th><th>Description</th><th>Validation</th></tr></thead><tbody><tr><td>confirmationCode</td><td>string</td><td>Yes</td><td>6-digit delivery confirmation code</td><td>Exactly 6 digits (0-9)</td></tr></tbody></table>

**Response JSON Sample:**

```json
{
  "orderId": "550e8400-e29b-41d4-a716-446655440000",
  "orderNumber": "ORD-2025-12345",
  "deliveredAt": "2025-10-25T10:30:45",
  "confirmedAt": "2025-10-25T10:30:45",
  "escrowReleased": true,
  "sellerAmount": 166250.00,
  "currency": "TZS",
  "message": "Delivery confirmed successfully. Order completed!"
}

```

**Note:** This response is returned directly without the standard success envelope.

**Response Fields:**

<table id="bkmrk-field-description-or-2"><thead><tr><th>Field</th><th>Description</th></tr></thead><tbody><tr><td>orderId</td><td>UUID of the confirmed order</td></tr><tr><td>orderNumber</td><td>Human-readable order number</td></tr><tr><td>deliveredAt</td><td>Timestamp when order was marked as delivered</td></tr><tr><td>confirmedAt</td><td>Timestamp when delivery was confirmed</td></tr><tr><td>escrowReleased</td><td>Whether escrow funds were released to seller</td></tr><tr><td>sellerAmount</td><td>Amount released to seller after platform fee</td></tr><tr><td>currency</td><td>Currency code</td></tr><tr><td>message</td><td>Confirmation message</td></tr></tbody></table>

**Error Responses:**

- `400 Bad Request`: Order is a digital order (completed automatically, no confirmation needed), invalid confirmation code, order not SHIPPED, user is not buyer, max attempts exceeded, code expired, or escrow already released
- `401 Unauthorized`: Authentication required
- `404 Not Found`: Order not found or no active confirmation code
- `422 Unprocessable Entity`: Confirmation code format invalid

---

### 13. Regenerate Confirmation Code

**Purpose:** Customer requests a new delivery confirmation code if the previous one was lost or expired.

**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}/{orderId}/regenerate-code`

**Access Level:** 🔒 Protected (Buyer/Customer only)

**Authentication:** Bearer Token

**Request Headers:**

<table id="bkmrk-header-type-required-12"><thead><tr><th>Header</th><th>Type</th><th>Required</th><th>Description</th></tr></thead><tbody><tr><td>Authorization</td><td>string</td><td>Yes</td><td>Bearer token for authentication</td></tr></tbody></table>

**Path Parameters:**

<table id="bkmrk-parameter-type-requi-15"><thead><tr><th>Parameter</th><th>Type</th><th>Required</th><th>Description</th></tr></thead><tbody><tr><td>orderId</td><td>UUID</td><td>Yes</td><td>Unique identifier of the order</td></tr></tbody></table>

**Response JSON Sample:**

```json
{
  "success": true,
  "message": "Confirmation code regenerated successfully",
  "data": {
    "orderId": "550e8400-e29b-41d4-a716-446655440000",
    "orderNumber": "ORD-2025-12345",
    "codeSent": true,
    "destination": "email",
    "codeExpiresAt": "2025-11-24T10:30:45",
    "maxAttempts": 5,
    "message": "New confirmation code sent to your email"
  }
}

```

**Response Fields:**

<table id="bkmrk-field-description-or-3"><thead><tr><th>Field</th><th>Description</th></tr></thead><tbody><tr><td>orderId</td><td>UUID of the order</td></tr><tr><td>orderNumber</td><td>Human-readable order number</td></tr><tr><td>codeSent</td><td>Whether new code was successfully sent</td></tr><tr><td>destination</td><td>Where the code was sent (`email`)</td></tr><tr><td>codeExpiresAt</td><td>When the new code expires (30 days from generation)</td></tr><tr><td>maxAttempts</td><td>Maximum number of verification attempts allowed</td></tr><tr><td>message</td><td>Confirmation message</td></tr></tbody></table>

**Error Responses:**

- `400 Bad Request`: Order is a digital order (does not use delivery confirmation codes), order status is not SHIPPED, user is not the buyer, or delivery already confirmed
- `401 Unauthorized`: Authentication required
- `404 Not Found`: Order not found

---

### 14. Get Digital Download URL

**Purpose:** Generates a presigned download URL for a digital file linked to an order. The URL expires in 5 minutes.

**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}/{orderId}/downloads/{fileId}`

**Access Level:** 🔒 Protected (Buyer only)

**Authentication:** Bearer Token

**Request Headers:**

<table id="bkmrk-header-type-required-13"><thead><tr><th>Header</th><th>Type</th><th>Required</th><th>Description</th></tr></thead><tbody><tr><td>Authorization</td><td>string</td><td>Yes</td><td>Bearer token for authentication</td></tr></tbody></table>

**Path Parameters:**

<table id="bkmrk-parameter-type-requi-16"><thead><tr><th>Parameter</th><th>Type</th><th>Required</th><th>Description</th></tr></thead><tbody><tr><td>orderId</td><td>UUID</td><td>Yes</td><td>Unique identifier of the order</td></tr><tr><td>fileId</td><td>UUID</td><td>Yes</td><td>Unique identifier of the digital file</td></tr></tbody></table>

**Response JSON Sample:**

```json
{
  "success": true,
  "message": "Download URL generated — link expires in 5 minutes",
  "data": {
    "fileId": "abc12345-def6-7890-ghij-klmnopqrstuv",
    "fileName": "course-material.pdf",
    "downloadUrl": "https://storage.example.com/files/...",
    "expiresAt": "2025-10-25T10:35:45",
    "downloadsRemaining": 3,
    "downloadCount": 2
  }
}

```

**Response Fields:**

<table id="bkmrk-field-description-fi"><thead><tr><th>Field</th><th>Description</th></tr></thead><tbody><tr><td>fileId</td><td>Unique identifier of the digital file</td></tr><tr><td>fileName</td><td>Name of the file</td></tr><tr><td>downloadUrl</td><td>Presigned URL for downloading the file (expires in 5 minutes)</td></tr><tr><td>expiresAt</td><td>Timestamp when the download URL expires</td></tr><tr><td>downloadsRemaining</td><td>Number of downloads remaining for this buyer</td></tr><tr><td>downloadCount</td><td>Number of times this file has been downloaded</td></tr></tbody></table>

**Error Responses:**

- `401 Unauthorized`: Authentication required
- `404 Not Found`: Order or file not found
- `422 Unprocessable Entity`: Download limit exceeded or access not permitted

---

### 15. List Order Downloads

**Purpose:** Returns all digital files the buyer has access to for a given order, including `fileId` needed to generate download URLs. Call this first before endpoint 14.

**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}/{orderId}/downloads`

**Access Level:** 🔒 Protected (Buyer only)

**Authentication:** Bearer Token

**Request Headers:**

<table id="bkmrk-header-type-required-14"><thead><tr><th>Header</th><th>Type</th><th>Required</th><th>Description</th></tr></thead><tbody><tr><td>Authorization</td><td>string</td><td>Yes</td><td>Bearer token for authentication</td></tr></tbody></table>

**Path Parameters:**

<table id="bkmrk-parameter-type-requi-17"><thead><tr><th>Parameter</th><th>Type</th><th>Required</th><th>Description</th></tr></thead><tbody><tr><td>orderId</td><td>UUID</td><td>Yes</td><td>Unique identifier of the order</td></tr></tbody></table>

**Response JSON Sample:**

```json
{
  "success": true,
  "message": "2 file(s) available for download",
  "data": [
    {
      "fileId": "f1a2b3c4-def5-6789-ghij-klmnopqrstuv",
      "fileName": "spring-boot-course.zip",
      "contentType": "application/zip",
      "fileSize": 524288000,
      "downloadCount": 1,
      "downloadsRemaining": 4,
      "accessExpiresAt": "2026-06-18T10:00:00",
      "canDownload": true
    },
    {
      "fileId": "a9b8c7d6-e5f4-3210-hijk-lmnopqrstuvw",
      "fileName": "bonus-resources.pdf",
      "contentType": "application/pdf",
      "fileSize": 2048000,
      "downloadCount": 0,
      "downloadsRemaining": 4,
      "accessExpiresAt": "2026-06-18T10:00:00",
      "canDownload": true
    }
  ]
}

```

**Response Fields:**

<table id="bkmrk-field-description-fi-1"><thead><tr><th>Field</th><th>Description</th></tr></thead><tbody><tr><td>fileId</td><td>Use this in endpoint 14 to get the actual download URL</td></tr><tr><td>fileName</td><td>Display name of the file</td></tr><tr><td>contentType</td><td>MIME type of the file</td></tr><tr><td>fileSize</td><td>File size in bytes</td></tr><tr><td>downloadCount</td><td>How many times this buyer has downloaded this file</td></tr><tr><td>downloadsRemaining</td><td>Downloads left before cap is hit (null = unlimited)</td></tr><tr><td>accessExpiresAt</td><td>When this buyer's access to this file expires</td></tr><tr><td>canDownload</td><td>false if access is revoked, expired, or download cap reached</td></tr></tbody></table>

**Error Responses:**

- `401 Unauthorized`: Authentication required
- `404 Not Found`: Order not found or does not belong to this buyer
- `422 Unprocessable Entity`: Order has no digital files

---

## Order Creation — How Orders Are Generated

Orders are never created manually. They are generated automatically when a checkout session moves to `PAYMENT_COMPLETED`. The system reads the session, applies grouping rules, and creates one or more orders depending on the cart contents and purchase type.

---

### Order Grouping Rules

The grouping key is **(shop + product type)**. Two items end up in the same order only if they share both the same shop and the same product type.

<table id="bkmrk-scenario-result-same"><thead><tr><th>Scenario</th><th>Result</th></tr></thead><tbody><tr><td>Same shop, same type (both PHYSICAL)</td><td>1 order</td></tr><tr><td>Same shop, same type (both DIGITAL)</td><td>1 order</td></tr><tr><td>Same shop, different types (PHYSICAL + DIGITAL)</td><td>2 separate orders</td></tr><tr><td>Different shops, same type</td><td>1 order per shop</td></tr><tr><td>Different shops, different types</td><td>1 order per shop per type</td></tr></tbody></table>

**Why split by type?**Digital orders complete immediately (no shipping, escrow released on creation). Physical orders wait for seller shipment then buyer confirmation. Mixing them in one order would make status tracking and escrow management impossible.

---

### Scenario 1 — Direct Purchase (Buy Now)

Buyer clicks Buy Now on a single product. Always produces exactly one order.

**Physical product:**

```
Buyer → Buy Now → Payment
  → 1 order created (source: DIRECT_PURCHASE)
  → status: PENDING_SHIPMENT
  → escrow held until buyer confirms delivery
  → seller ships → buyer confirms with 6-digit code → escrow released → COMPLETED

```

**Digital product:**

```
Buyer → Buy Now → Payment
  → 1 order created (source: DIGITAL_PURCHASE)
  → status: COMPLETED immediately
  → escrow released immediately
  → DigitalDownloadAccess records created for all active files
  → buyer can download right away

```

---

### Scenario 2 — Cart Purchase

Buyer checks out a cart with multiple items. The system groups by (shop, product type) and creates one order per group.

**Example cart:**

<table id="bkmrk-item-shop-type-wirel"><thead><tr><th>Item</th><th>Shop</th><th>Type</th></tr></thead><tbody><tr><td>Wireless Headphones</td><td>TechStore</td><td>PHYSICAL</td></tr><tr><td>Spring Boot Course (PDF)</td><td>TechStore</td><td>DIGITAL</td></tr><tr><td>Running Shoes</td><td>SportShop</td><td>PHYSICAL</td></tr></tbody></table>

**Result: 3 orders created**

```
Order #1 → TechStore | PHYSICAL
  source: CART_PURCHASE
  status: PENDING_SHIPMENT
  shipping: split proportionally if multi-shop

Order #2 → TechStore | DIGITAL
  source: DIGITAL_PURCHASE
  status: COMPLETED immediately
  shipping: TZS 0
  → DigitalDownloadAccess created for Spring Boot Course files
  → buyer can download immediately

Order #3 → SportShop | PHYSICAL
  source: CART_PURCHASE
  status: PENDING_SHIPMENT
  shipping: split proportionally

```

**Shipping split rule:** If the cart has items from multiple shops, the total shipping cost is divided equally across the number of distinct shops. Each physical order gets its share.

---

### Scenario 3 — Installment Purchase (IMMEDIATE fulfillment)

Buyer pays in installments but gets the product after the first payment.

**Physical product:**

```
First payment → order created (source: INSTALLMENT)
  → status: PENDING_SHIPMENT
  → seller ships after first payment
  → buyer confirms delivery → escrow released proportionally as payments come in

Remaining payments → collected without creating new orders

```

**Digital product:**

```
First payment → order created (source: INSTALLMENT → detected as DIGITAL_PURCHASE)
  → status: COMPLETED immediately
  → DigitalDownloadAccess created
  → buyer can download after first payment

Remaining payments → collected, no new order needed

```

---

### Scenario 4 — Installment Purchase (AFTER\_PAYMENT fulfillment)

Buyer pays all installments first, gets the product only after full payment.

**Physical product:**

```
First payment → no order created yet, agreement tracked only
...
Final payment → order created (source: INSTALLMENT)
  → status: PENDING_SHIPMENT
  → seller ships → buyer confirms → COMPLETED

```

**Digital product:**

```
First payment → no order created yet
...
Final payment → order created (source: INSTALLMENT → detected as DIGITAL_PURCHASE)
  → status: COMPLETED immediately
  → DigitalDownloadAccess created
  → buyer can download only after all installments are paid

```

---

### Scenario 5 — Group Purchase

Multiple buyers join a group for a discounted price. When the group reaches its participant goal, an order is created for every participant simultaneously.

**Physical product:**

```
Group goal reached →
  For each participant:
    → 1 order created (source: GROUP_PURCHASE)
    → status: PENDING_SHIPMENT
    → seller ships to each buyer individually
    → each buyer confirms delivery independently

```

**Digital product:**

```
Group goal reached →
  For each participant:
    → 1 order created (source: GROUP_PURCHASE → detected as DIGITAL_PURCHASE)
    → status: COMPLETED immediately
    → DigitalDownloadAccess created per participant
    → all buyers can download simultaneously

```

Group metadata stored on each order: `groupInstanceId`, `groupPrice`, `regularPrice`, `savings`.

---

### Digital Download Flow (after any purchase)

Once an order with source `DIGITAL_PURCHASE` is created, the fulfillment service creates a `DigitalDownloadAccess` record per file per buyer. These records enforce:

<table id="bkmrk-rule-configured-by-a"><thead><tr><th>Rule</th><th>Configured by</th></tr></thead><tbody><tr><td>Access expiry</td><td>`product.downloadExpiryDays` (default: 365 days)</td></tr><tr><td>Max downloads</td><td>`product.maxDownloadsPerBuyer` (null = unlimited)</td></tr><tr><td>Per-download URL TTL</td><td>5 minutes (hardcoded)</td></tr></tbody></table>

**Frontend download flow:**

Step 1 — List available files for an order:

```
GET api/v1/e-commerce/orders/{orderId}/downloads

Response:
[
  {
    "fileId": "f1a2b3c4-...",
    "fileName": "spring-boot-course.zip",
    "contentType": "application/zip",
    "fileSize": 524288000,
    "downloadCount": 0,
    "downloadsRemaining": 5,
    "accessExpiresAt": "2026-06-18T10:00:00",
    "canDownload": true
  }
]

```

Step 2 — Get a short-lived download link per file:

```
GET api/v1/e-commerce/orders/{orderId}/downloads/{fileId}

Response:
{
  "fileId": "f1a2b3c4-...",
  "fileName": "spring-boot-course.zip",
  "downloadUrl": "https://storage.../...?X-Amz-Expires=300&...",
  "expiresAt": "2026-05-19T11:05:00",
  "downloadsRemaining": 4,
  "downloadCount": 1
}

```

Step 3 — Buyer hits `downloadUrl` directly. The URL points to MinIO and expires in 5 minutes. Each call to Step 2 increments `downloadCount`.

---

### Order Status Reference

<table id="bkmrk-status-applies-to-me"><thead><tr><th>Status</th><th>Applies to</th><th>Meaning</th></tr></thead><tbody><tr><td>`PENDING_SHIPMENT`</td><td>Physical</td><td>Order paid, waiting for seller to ship</td></tr><tr><td>`SHIPPED`</td><td>Physical</td><td>Seller marked as shipped, waiting for buyer confirmation</td></tr><tr><td>`COMPLETED`</td><td>Both</td><td>Physical: buyer confirmed delivery. Digital: set immediately on creation</td></tr><tr><td>`CANCELLED`</td><td>Both</td><td>Order cancelled</td></tr><tr><td>`REFUNDED`</td><td>Both</td><td>Payment refunded</td></tr></tbody></table>

<table id="bkmrk-delivery-status-appl"><thead><tr><th>Delivery Status</th><th>Applies to</th><th>Meaning</th></tr></thead><tbody><tr><td>`PENDING`</td><td>Physical</td><td>Not yet shipped</td></tr><tr><td>`IN_TRANSIT`</td><td>Physical</td><td>Seller marked as shipped</td></tr><tr><td>`CONFIRMED`</td><td>Physical</td><td>Buyer confirmed receipt</td></tr><tr><td>`NOT_APPLICABLE`</td><td>Digital</td><td>No physical delivery involved</td></tr></tbody></table>

---

### Timeline Reference

The `timeline` field is embedded in every order detail response. It is a sequential list of steps representing the order's lifecycle. Steps not yet reached have `timestamp: null` and `isCompleted: false` — the frontend renders these as pending/greyed-out.

**Physical order steps (DIRECT\_PURCHASE, CART\_PURCHASE, INSTALLMENT, GROUP\_PURCHASE):**

```
ORDER_PLACED → SHIPPED → DELIVERED → COMPLETED

```

**Digital order steps (DIGITAL\_PURCHASE):**

```
ORDER_PLACED → FILES_AVAILABLE → COMPLETED

```

**Terminal branches** (replace remaining steps when reached):

```
CANCELLED  — appears after ORDER_PLACED if cancelled before shipping
DISPUTED   — appears after SHIPPED/FILES_AVAILABLE if buyer raises a dispute
REFUNDED   — appears after DISPUTED if resolved in buyer's favour

```

**Step notes:**

<table id="bkmrk-step-note-value-ship"><thead><tr><th>Step</th><th>Note value</th></tr></thead><tbody><tr><td>`SHIPPED`</td><td>`"<Carrier> · <TrackingNumber>"` if tracking info is set, otherwise `null`</td></tr><tr><td>`CANCELLED`</td><td>Cancellation reason if provided, otherwise `null`</td></tr><tr><td>`COMPLETED`</td><td>`"Confirmed by buyer"` or `"Auto-confirmed"` depending on how it was confirmed</td></tr><tr><td>All others</td><td>`null`</td></tr></tbody></table>

# Marketplace

**Author**: Josh S. Sakweli, Backend Lead Team  
**Last Updated**: 2026-06-04  
**Version**: v1.0

**Base URL**: `api/v1/e-commerce/marketplace`

**Short Description**: The Marketplace API is the primary product discovery layer of the Nexgate e-commerce platform. It exposes personalised product feeds, trending rankings, hot deals, live group purchases, and a powerful advanced filter with keyword search — all driven by a scoring formula built on real purchase, view, and cart signals.

**Hints**:

- All endpoints work for **anonymous users** (no token required). Authenticated users automatically receive personalised results where applicable — no extra parameter needed.
- `TRENDING` and `FOR_YOU` feeds re-rank within each page using the full scoring formula. Other sort modes (`NEWEST`, `PRICE_ASC`, etc.) are sorted at the database level and return exact ordering.
- `hasMultipleColors`, `maxGroupSeatsLeft`, and `minGroupDiscountPercent` filters are applied **after** the database query, so the `totalElements` count in the response reflects the pre-filtered DB count — not the post-filtered count.
- `viewCount` increments each time `GET /shops/{shopId}/products/{productId}` is called. `cartAddCount` increments when a product is added to a cart for the first time (not on quantity updates).
- Pages are **1-based** (`page=1` returns the first page).

---

## Standard Response Format

All API responses follow a consistent structure using the Globe Response Builder pattern:

### Success Response Structure

```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Operation completed successfully",
  "action_time": "2026-06-04T10:30:45",
  "data": {
    "content": [...],
    "currentPage": 1,
    "pageSize": 20,
    "totalElements": 158,
    "totalPages": 8,
    "hasNext": true,
    "hasPrevious": false
  }
}

```

### Error Response Structure

```json
{
  "success": false,
  "httpStatus": "BAD_REQUEST",
  "message": "Error description",
  "action_time": "2026-06-04T10:30:45",
  "data": "Error description"
}

```

### Standard Response Fields

<table id="bkmrk-field-type-descripti"><thead><tr><th>Field</th><th>Type</th><th>Description</th></tr></thead><tbody><tr><td>`success`</td><td>boolean</td><td>`true` for success, `false` for errors</td></tr><tr><td>`httpStatus`</td><td>string</td><td>HTTP status name (OK, BAD\_REQUEST, NOT\_FOUND, etc.)</td></tr><tr><td>`message`</td><td>string</td><td>Human-readable operation result</td></tr><tr><td>`action_time`</td><td>string</td><td>ISO 8601 timestamp of response generation</td></tr><tr><td>`data`</td><td>object</td><td>Paginated payload for success, error detail for failures</td></tr></tbody></table>

---

## Scoring Formulas

These formulas are the engine behind `TRENDING`, `FOR_YOU`, and `BEST_DEAL` sort modes. Understanding them helps predict how products are ranked.

### Log Normalization

All count-based signals (soldQuantity, viewCount, cartAddCount) are log-normalized before applying weights. This prevents one product with 100,000 sales from dominating everything else.

```
normalize(value) = min(1.0,  log(1 + value) / log(1 + 10,000))

Examples:
  0 sales     →  0.000
  100 sales   →  0.501
  1,000 sales →  0.750
  10,000 sales → 1.000   ← reference ceiling
  50,000 sales → 1.000   ← capped at 1.0

```

---

### Formula 1 — Trending Score (global, same for everyone)

```
┌──────────────────────────────────────────────────────────────────────┐
│                      TRENDING SCORE FORMULA                          │
├──────────────────────────────────────────────────────────────────────┤
│                                                                      │
│  trendingScore =                                                     │
│      normalize(soldQuantity)   × 0.30   ← real money paid           │
│    + normalize(viewCount)      × 0.25   ← real browsing intent      │
│    + groupHeat                 × 0.20   ← social urgency            │
│    + normalize(cartAddCount)   × 0.15   ← purchase intent           │
│    + discountStrength          × 0.07   ← deal attractiveness       │
│    + recencyBonus              × 0.03   ← freshness tiebreaker      │
│                                                                      │
│  groupHeat       = seatsOccupied / totalSeats  (hottest live group)  │
│  discountStrength= (comparePrice - price) / comparePrice            │
│  recencyBonus    = 1.0 if ≤ 7 days old                              │
│                    0.5 if ≤ 30 days old                             │
│                    0.0 otherwise                                     │
└──────────────────────────────────────────────────────────────────────┘

```

---

### Formula 2 — Personalized Trending Score (authenticated users)

```
┌──────────────────────────────────────────────────────────────────────┐
│                  PERSONALIZED TRENDING SCORE FORMULA                 │
├──────────────────────────────────────────────────────────────────────┤
│                                                                      │
│  personalizedScore =                                                 │
│      trendingScore                                                   │
│    + 0.25   (if product's shop is in user's subscriptions)          │
│    + 0.00   (otherwise)                                              │
│                                                                      │
│  This is Option A — soft priority. Subscribed-shop products float   │
│  near the top but a genuinely viral product still beats them.       │
└──────────────────────────────────────────────────────────────────────┘

```

---

### Formula 3 — Relevance Score (For You feed)

```
┌──────────────────────────────────────────────────────────────────────┐
│                       RELEVANCE SCORE FORMULA                        │
├──────────────────────────────────────────────────────────────────────┤
│                                                                      │
│  relevanceScore =                                                    │
│      categoryMatch  × 0.40   ← product's category is in cart        │
│    + favShopBoost   × 0.40   ← shop is in user's subscriptions      │
│    + trendingScore  × 0.20   ← global popularity tiebreaker         │
│                                                                      │
│  categoryMatch = 1.0 if product's category matches any category     │
│                  currently in the user's cart, else 0.0             │
│  favShopBoost  = 1.0 if shop is subscribed, else 0.0               │
│                                                                      │
│  Fallback: if user has no cart items and no subscriptions,          │
│  the endpoint falls back to the Personalized Trending feed.         │
└──────────────────────────────────────────────────────────────────────┘

```

---

## Advanced Filter — UI Reference

The advanced filter groups its parameters into logical sections. Below is a reference layout showing how a frontend filter panel would be structured:

```
┌───────────────────────────────────────────────────────────┐
│                    MARKETPLACE FILTERS                    │
├───────────────────────────────────────────────────────────┤
│  🔍  SEARCH                                               │
│  ┌─────────────────────────────────────────────────────┐  │
│  │  ?q=  keyword search across name & description...  │  │
│  └─────────────────────────────────────────────────────┘  │
├───────────────────────────────────────────────────────────┤
│  💰  PRICE RANGE                                          │
│      Min [ minPrice _________ ]                           │
│      Max [ maxPrice _________ ]                           │
├───────────────────────────────────────────────────────────┤
│  🏷️  PRODUCT                                              │
│      Category    [ categoryId ▼ ]                         │
│      Condition   ○ NEW   ○ USED   ○ REFURBISHED           │
│      Type        ○ Physical   ○ Digital                   │
│      Urgency     ○ NONE  ○ LIMITED_TIME  ○ LOW_STOCK      │
│                  ○ FLASH_SALE                             │
│      [ ] hasMultipleColors  — show colour variants only   │
├───────────────────────────────────────────────────────────┤
│  📦  AVAILABILITY                                         │
│      [ ] inStock           — in-stock only               │
│      Min stock [ minStockQuantity ]  — bulk buyers        │
├───────────────────────────────────────────────────────────┤
│  🤝  GROUP DEALS                                          │
│      [ ] hasGroupBuying    — supports group buying        │
│      [ ] hasActiveGroup    — live OPEN group right now   │
│      Max seats left  [ maxGroupSeatsLeft ]  ← urgency    │
│      Min group disc. [ minGroupDiscountPercent ]%         │
├───────────────────────────────────────────────────────────┤
│  💳  PAYMENT                                              │
│      [ ] onSale            — currently discounted         │
│      [ ] hasInstallments   — instalment plans available   │
├───────────────────────────────────────────────────────────┤
│  🏪  SHOP TRUST                                           │
│      [ ] shopVerified      — verified shops only          │
│      Min trust score  [ minTrustScore ] / 5.00            │
├───────────────────────────────────────────────────────────┤
│  📈  POPULARITY                                           │
│      Min sold count  [ minSoldCount ]                     │
├───────────────────────────────────────────────────────────┤
│  ↕️  SORT BY                                              │
│      ○ TRENDING      ← formula-ranked                     │
│      ○ FOR_YOU       ← personalised relevance             │
│      ○ NEWEST        ← createdAt DESC                     │
│      ○ PRICE_ASC     ← cheapest first                     │
│      ○ PRICE_DESC    ← most expensive first               │
│      ○ MOST_SOLD     ← soldQuantity DESC                  │
│      ○ BEST_DEAL     ← highest discount % first           │
│      ○ MOST_VIEWED   ← viewCount DESC                     │
│      ○ MOST_CARTED   ← cartAddCount DESC                  │
└───────────────────────────────────────────────────────────┘

```

---

## Endpoints

---

## 1. Main Discovery Feed

**Purpose**: Returns the primary marketplace product feed with optional filters and all sort strategies. Works for both anonymous and authenticated users — authenticated users receive personalised scoring automatically.

**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> `api/v1/e-commerce/marketplace/feed`

**Access Level**: 🌐 Public (personalised when authenticated)

**Authentication**: Bearer Token (optional — improves results when provided)

**Query Parameters**:

<table id="bkmrk-parameter-type-requi"><thead><tr><th>Parameter</th><th>Type</th><th>Required</th><th>Description</th><th>Default</th></tr></thead><tbody><tr><td>`sortBy`</td><td>enum</td><td>No</td><td>Sort strategy. Values: `TRENDING`, `FOR_YOU`, `NEWEST`, `PRICE_ASC`, `PRICE_DESC`, `MOST_SOLD`, `BEST_DEAL`, `MOST_VIEWED`, `MOST_CARTED`</td><td>`TRENDING`</td></tr><tr><td>`page`</td><td>integer</td><td>No</td><td>Page number (1-based)</td><td>`1`</td></tr><tr><td>`size`</td><td>integer</td><td>No</td><td>Items per page</td><td>`20`</td></tr><tr><td>`minPrice`</td><td>decimal</td><td>No</td><td>Minimum product price</td><td>—</td></tr><tr><td>`maxPrice`</td><td>decimal</td><td>No</td><td>Maximum product price</td><td>—</td></tr><tr><td>`categoryId`</td><td>UUID</td><td>No</td><td>Filter by product category</td><td>—</td></tr><tr><td>`condition`</td><td>enum</td><td>No</td><td>`NEW`, `USED`, `REFURBISHED`</td><td>—</td></tr><tr><td>`productType`</td><td>enum</td><td>No</td><td>`PHYSICAL`, `DIGITAL`</td><td>—</td></tr><tr><td>`inStock`</td><td>boolean</td><td>No</td><td>`true` = in-stock products only</td><td>—</td></tr><tr><td>`onSale`</td><td>boolean</td><td>No</td><td>`true` = discounted products only</td><td>—</td></tr><tr><td>`hasActiveGroup`</td><td>boolean</td><td>No</td><td>`true` = products with a live OPEN group right now</td><td>—</td></tr><tr><td>`shopVerified`</td><td>boolean</td><td>No</td><td>`true` = verified shops only</td><td>—</td></tr></tbody></table>

**Success Response JSON Sample**:

```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Feed retrieved successfully",
  "action_time": "2026-06-04T10:30:45",
  "data": {
    "content": [
      {
        "productId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
        "productName": "Samsung Galaxy S24",
        "productSlug": "samsung-galaxy-s24-techstore",
        "primaryImage": "https://cdn.nexgate.com/products/galaxy-s24.jpg",
        "productType": "PHYSICAL",
        "price": 850000.00,
        "comparePrice": 1050000.00,
        "discountPercentage": 19.05,
        "stockQuantity": 42,
        "soldQuantity": 318,
        "viewCount": 2741,
        "cartAddCount": 195,
        "urgencyTag": "LOW_STOCK",
        "condition": "NEW",
        "inStock": true,
        "onSale": true,
        "hasInstallments": true,
        "shopId": "7cb3a812-1234-4abc-b3fc-9d84f55bce12",
        "shopName": "TechStore Tanzania",
        "shopSlug": "techstore-tanzania",
        "shopLogoUrl": "https://cdn.nexgate.com/shops/techstore-logo.jpg",
        "shopVerified": true,
        "shopTrustScore": 4.80,
        "categoryId": "a1b2c3d4-1234-5678-abcd-ef0123456789",
        "categoryName": "Smartphones",
        "hasActiveGroup": true,
        "activeGroupHeat": 0.78,
        "activeGroupPrice": 720000.00,
        "activeGroupSeatsLeft": 4,
        "activeGroupExpiresAt": "2026-06-04T18:00:00",
        "createdAt": "2026-05-28T09:15:00"
      }
    ],
    "currentPage": 1,
    "pageSize": 20,
    "totalElements": 1284,
    "totalPages": 65,
    "hasNext": true,
    "hasPrevious": false
  }
}

```

**Success Response Fields**:

<table id="bkmrk-field-description-co"><thead><tr><th>Field</th><th>Description</th></tr></thead><tbody><tr><td>`content`</td><td>Array of product cards for this page</td></tr><tr><td>`content[].productId`</td><td>Unique product identifier</td></tr><tr><td>`content[].primaryImage`</td><td>URL of the first product image</td></tr><tr><td>`content[].price`</td><td>Current selling price</td></tr><tr><td>`content[].comparePrice`</td><td>Original price (present only if product is on sale)</td></tr><tr><td>`content[].discountPercentage`</td><td>Regular sale discount % — `null` if no `comparePrice`</td></tr><tr><td>`content[].effectiveDiscountPercentage`</td><td>Best available deal across ALL discount types — `max(salePct, activeGroupPct) × 100`. `null` if product has no discount of any kind. This is what the Hot Deals feed sorts by</td></tr><tr><td>`content[].soldQuantity`</td><td>Total units sold — visible only if shop has enabled this</td></tr><tr><td>`content[].viewCount`</td><td>Cumulative public views since tracking began</td></tr><tr><td>`content[].cartAddCount`</td><td>Cumulative times added to any cart</td></tr><tr><td>`content[].urgencyTag`</td><td>`NONE`, `LIMITED_TIME`, `LOW_STOCK`, `FLASH_SALE`</td></tr><tr><td>`content[].hasActiveGroup`</td><td>`true` if there is a live OPEN group purchase right now</td></tr><tr><td>`content[].activeGroupHeat`</td><td>Group fill ratio — `0.0` (empty) to `1.0` (full). `null` if no active group</td></tr><tr><td>`content[].activeGroupPrice`</td><td>Discounted price available inside the active group</td></tr><tr><td>`content[].activeGroupSeatsLeft`</td><td>Remaining seats in the active group</td></tr><tr><td>`content[].activeGroupExpiresAt`</td><td>When the active group purchase expires</td></tr><tr><td>`content[].shopTrustScore`</td><td>Shop trust rating from `0.00` to `5.00`</td></tr><tr><td>`currentPage`</td><td>Current page number (1-based)</td></tr><tr><td>`totalElements`</td><td>Total matching products across all pages</td></tr><tr><td>`hasNext` / `hasPrevious`</td><td>Pagination navigation flags</td></tr></tbody></table>

**Standard Error Types**:

- `500 INTERNAL_SERVER_ERROR`: Unexpected server failure

---

## 2. Trending Products

**Purpose**: Returns products ranked by the trending score formula. Anonymous users receive the global trending score. Authenticated users receive a personalised ranking — products from subscribed shops receive a +0.25 score boost so they float near the top.

**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> `api/v1/e-commerce/marketplace/trending`

**Access Level**: 🌐 Public (personalised when authenticated)

**Authentication**: Bearer Token (optional)

**Scoring applied**:

```
Anonymous  →  trendingScore
Authenticated →  trendingScore + 0.25 (if product's shop is subscribed)

```

> **Note**: The database pre-sorts by `soldQuantity DESC` as an approximation, then the full formula re-ranks within each page. This means the absolute order across pages may differ slightly from a pure formula sort — which is intentional (pagination stability).

**Query Parameters**:

<table id="bkmrk-parameter-type-requi-1"><thead><tr><th>Parameter</th><th>Type</th><th>Required</th><th>Description</th><th>Default</th></tr></thead><tbody><tr><td>`page`</td><td>integer</td><td>No</td><td>Page number (1-based)</td><td>`1`</td></tr><tr><td>`size`</td><td>integer</td><td>No</td><td>Items per page</td><td>`20`</td></tr><tr><td>`categoryId`</td><td>UUID</td><td>No</td><td>Filter by category</td><td>—</td></tr><tr><td>`minPrice`</td><td>decimal</td><td>No</td><td>Minimum price</td><td>—</td></tr><tr><td>`maxPrice`</td><td>decimal</td><td>No</td><td>Maximum price</td><td>—</td></tr><tr><td>`inStock`</td><td>boolean</td><td>No</td><td>In-stock only</td><td>—</td></tr><tr><td>`onSale`</td><td>boolean</td><td>No</td><td>On sale only</td><td>—</td></tr><tr><td>`shopVerified`</td><td>boolean</td><td>No</td><td>Verified shops only</td><td>—</td></tr></tbody></table>

**Success Response**: Same structure as the [Main Feed](#1-main-discovery-feed) endpoint.

**Standard Error Types**:

- `500 INTERNAL_SERVER_ERROR`: Unexpected server failure

---

## 3. For You — Personalised Recommendations

**Purpose**: Returns products ranked by relevance to the authenticated user based on their cart categories and shop subscriptions. Falls back to the trending feed for unauthenticated users or users with no cart items and no subscriptions.

**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> `api/v1/e-commerce/marketplace/for-you`

**Access Level**: 🌐 Public (best results when authenticated)

**Authentication**: Bearer Token (optional — falls back to trending if absent)

**Scoring applied**:

```
relevanceScore =
    categoryMatch  × 0.40   (product's category is in user's cart)
  + favShopBoost   × 0.40   (shop is in user's subscriptions)
  + trendingScore  × 0.20   (global popularity tiebreaker)

Pool strategy: fetches 3× the requested page size (max 150), scores in Java,
then slices the requested page from the ranked result.

```

**Query Parameters**:

<table id="bkmrk-parameter-type-requi-2"><thead><tr><th>Parameter</th><th>Type</th><th>Required</th><th>Description</th><th>Default</th></tr></thead><tbody><tr><td>`page`</td><td>integer</td><td>No</td><td>Page number (1-based)</td><td>`1`</td></tr><tr><td>`size`</td><td>integer</td><td>No</td><td>Items per page</td><td>`20`</td></tr><tr><td>`categoryId`</td><td>UUID</td><td>No</td><td>Narrow recommendations to a specific category</td><td>—</td></tr><tr><td>`inStock`</td><td>boolean</td><td>No</td><td>In-stock products only</td><td>—</td></tr><tr><td>`shopVerified`</td><td>boolean</td><td>No</td><td>Verified shops only</td><td>—</td></tr></tbody></table>

**Success Response**: Same structure as the [Main Feed](#1-main-discovery-feed) endpoint.

**Standard Error Types**:

- `500 INTERNAL_SERVER_ERROR`: Unexpected server failure

---

## 4. Hot Deals

**Purpose**: Returns products with any form of discount — regular sale price **or** an active group purchase discount — ranked by the best available saving. A product with a 35% group discount ranks higher than one with a 15% regular sale, even if it has no `comparePrice` set.

**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> `api/v1/e-commerce/marketplace/hot-deals`

**Access Level**: 🌐 Public

**Authentication**: None required

**Scoring applied**:

```
┌──────────────────────────────────────────────────────────────────────┐
│                   EFFECTIVE DISCOUNT FORMULA                         │
├──────────────────────────────────────────────────────────────────────┤
│                                                                      │
│  salePct      = (comparePrice - price) / comparePrice                │
│                 0.0 if no comparePrice or comparePrice ≤ price       │
│                                                                      │
│  activeGroupPct = (regularPrice - groupPrice) / regularPrice         │
│                   0.0 if no live OPEN group exists for the product   │
│                                                                      │
│  effectiveDiscountPct = max(salePct, activeGroupPct)                 │
│                                                                      │
│  Products included: onSale = true  OR  hasActiveGroup = true         │
│  Sorted by: effectiveDiscountPct DESC (within each page)             │
└──────────────────────────────────────────────────────────────────────┘

Example:
  Product A — regular sale 15% off, no group     → effectivePct = 15%
  Product B — no sale, group discount 35% off    → effectivePct = 35%  ← ranks first
  Product C — sale 20% off + group 40% off       → effectivePct = 40%  ← ranks first

```

**Query Parameters**:

<table id="bkmrk-parameter-type-requi-3"><thead><tr><th>Parameter</th><th>Type</th><th>Required</th><th>Description</th><th>Default</th></tr></thead><tbody><tr><td>`page`</td><td>integer</td><td>No</td><td>Page number (1-based)</td><td>`1`</td></tr><tr><td>`size`</td><td>integer</td><td>No</td><td>Items per page</td><td>`20`</td></tr><tr><td>`categoryId`</td><td>UUID</td><td>No</td><td>Filter by category</td><td>—</td></tr><tr><td>`minPrice`</td><td>decimal</td><td>No</td><td>Minimum price</td><td>—</td></tr><tr><td>`maxPrice`</td><td>decimal</td><td>No</td><td>Maximum price</td><td>—</td></tr><tr><td>`shopVerified`</td><td>boolean</td><td>No</td><td>Verified shops only</td><td>—</td></tr><tr><td>`inStock`</td><td>boolean</td><td>No</td><td>In-stock only</td><td>—</td></tr></tbody></table>

**Success Response**: Same structure as the [Main Feed](#1-main-discovery-feed) endpoint. `effectiveDiscountPercentage` is always present and non-null in this feed.

**Standard Error Types**:

- `500 INTERNAL_SERVER_ERROR`: Unexpected server failure

---

## 5. New Arrivals

**Purpose**: Returns recently published products, newest first. Ideal for users who want to discover what just dropped.

**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> `api/v1/e-commerce/marketplace/new-arrivals`

**Access Level**: 🌐 Public

**Authentication**: None required

**Sorting**: `createdAt DESC` — exact database-level sort, no formula applied.

**Query Parameters**:

<table id="bkmrk-parameter-type-requi-4"><thead><tr><th>Parameter</th><th>Type</th><th>Required</th><th>Description</th><th>Default</th></tr></thead><tbody><tr><td>`page`</td><td>integer</td><td>No</td><td>Page number (1-based)</td><td>`1`</td></tr><tr><td>`size`</td><td>integer</td><td>No</td><td>Items per page</td><td>`20`</td></tr><tr><td>`categoryId`</td><td>UUID</td><td>No</td><td>Filter by category</td><td>—</td></tr><tr><td>`productType`</td><td>enum</td><td>No</td><td>`PHYSICAL` or `DIGITAL`</td><td>—</td></tr><tr><td>`shopVerified`</td><td>boolean</td><td>No</td><td>Verified shops only</td><td>—</td></tr></tbody></table>

**Success Response**: Same structure as the [Main Feed](#1-main-discovery-feed) endpoint.

**Standard Error Types**:

- `500 INTERNAL_SERVER_ERROR`: Unexpected server failure

---

## 6. Live Group Purchases

**Purpose**: Returns products that currently have an active OPEN group purchase, sorted by group heat (most seats filled = most urgent = shown first). Useful for showing users time-sensitive social buying opportunities.

**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> `api/v1/e-commerce/marketplace/live-groups`

**Access Level**: 🌐 Public

**Authentication**: None required

**Sorting applied**:

```
groupHeat = seatsOccupied / totalSeats

Products sorted by groupHeat DESC — a group at 90% capacity appears
before one at 30%, creating urgency awareness for the user.

```

**Query Parameters**:

<table id="bkmrk-parameter-type-requi-5"><thead><tr><th>Parameter</th><th>Type</th><th>Required</th><th>Description</th><th>Default</th></tr></thead><tbody><tr><td>`page`</td><td>integer</td><td>No</td><td>Page number (1-based)</td><td>`1`</td></tr><tr><td>`size`</td><td>integer</td><td>No</td><td>Items per page</td><td>`20`</td></tr></tbody></table>

**Success Response JSON Sample**:

```json
{
  "success": true,
  "httpStatus": "OK",
  "message": "Live group purchases retrieved successfully",
  "action_time": "2026-06-04T10:30:45",
  "data": {
    "content": [
      {
        "productId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
        "productName": "Samsung Galaxy S24",
        "price": 850000.00,
        "hasActiveGroup": true,
        "activeGroupHeat": 0.92,
        "activeGroupPrice": 720000.00,
        "activeGroupSeatsLeft": 2,
        "activeGroupExpiresAt": "2026-06-04T18:00:00"
      }
    ],
    "currentPage": 1,
    "pageSize": 20,
    "totalElements": 37,
    "totalPages": 2,
    "hasNext": true,
    "hasPrevious": false
  }
}

```

**Standard Error Types**:

- `500 INTERNAL_SERVER_ERROR`: Unexpected server failure

---

## 7. Advanced Filter

**Purpose**: The most powerful endpoint in the marketplace. Combines every available filter with an optional keyword search (`?q=`) so users can express highly specific queries like: *"show me NEW Samsung smartphones under 1M from verified shops with an active group deal saving at least 20%, sorted by trending."*

**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> `api/v1/e-commerce/marketplace/advanced-filter`

**Access Level**: 🌐 Public (personalised scoring when authenticated)

**Authentication**: Bearer Token (optional — enables `FOR_YOU` and personalised `TRENDING`)

**Scoring applied per `sortBy`**:

```
┌────────────────┬────────────────────────────────────────────────────────┐
│ sortBy         │ How it works                                           │
├────────────────┼────────────────────────────────────────────────────────┤
│ TRENDING       │ Full formula re-rank in Java after DB fetch            │
│ FOR_YOU        │ Relevance formula re-rank in Java after DB fetch       │
│ NEWEST         │ createdAt DESC — exact DB sort                         │
│ PRICE_ASC      │ price ASC — exact DB sort                              │
│ PRICE_DESC     │ price DESC — exact DB sort                             │
│ MOST_SOLD      │ soldQuantity DESC — exact DB sort                      │
│ BEST_DEAL      │ discount % DESC — re-ranked in Java after DB fetch     │
│ MOST_VIEWED    │ viewCount DESC — exact DB sort                         │
│ MOST_CARTED    │ cartAddCount DESC — exact DB sort                      │
└────────────────┴────────────────────────────────────────────────────────┘

```

**Post-fetch Java filters** (applied after DB query — `totalElements` reflects pre-filter count):

- `hasMultipleColors` — checks `colors.size() > 1` in memory
- `maxGroupSeatsLeft` — checks active group seats remaining in memory
- `minGroupDiscountPercent` — calculates group discount % in memory

**Query Parameters**:

<table id="bkmrk-group-parameter-type"><thead><tr><th>Group</th><th>Parameter</th><th>Type</th><th>Required</th><th>Description</th><th>Default</th></tr></thead><tbody><tr><td>**Sort &amp; Page**</td><td>`sortBy`</td><td>enum</td><td>No</td><td>Sort strategy (see table above)</td><td>`TRENDING`</td></tr><tr><td></td><td>`page`</td><td>integer</td><td>No</td><td>Page number (1-based)</td><td>`1`</td></tr><tr><td></td><td>`size`</td><td>integer</td><td>No</td><td>Items per page</td><td>`20`</td></tr><tr><td>**Search**</td><td>`q`</td><td>string</td><td>No</td><td>Keyword — searches product name and description (case-insensitive LIKE)</td><td>—</td></tr><tr><td>**Price**</td><td>`minPrice`</td><td>decimal</td><td>No</td><td>Minimum price</td><td>—</td></tr><tr><td></td><td>`maxPrice`</td><td>decimal</td><td>No</td><td>Maximum price</td><td>—</td></tr><tr><td>**Product**</td><td>`categoryId`</td><td>UUID</td><td>No</td><td>Filter by product category</td><td>—</td></tr><tr><td></td><td>`condition`</td><td>enum</td><td>No</td><td>`NEW`, `USED`, `REFURBISHED`</td><td>—</td></tr><tr><td></td><td>`productType`</td><td>enum</td><td>No</td><td>`PHYSICAL`, `DIGITAL`</td><td>—</td></tr><tr><td></td><td>`urgencyTag`</td><td>enum</td><td>No</td><td>`NONE`, `LIMITED_TIME`, `LOW_STOCK`, `FLASH_SALE`</td><td>—</td></tr><tr><td></td><td>`hasMultipleColors`</td><td>boolean</td><td>No</td><td>`true` = colour variant products only *(post-fetch)*</td><td>—</td></tr><tr><td>**Availability**</td><td>`inStock`</td><td>boolean</td><td>No</td><td>`true` = in-stock only</td><td>—</td></tr><tr><td></td><td>`minStockQuantity`</td><td>integer</td><td>No</td><td>Minimum stock units (bulk buyers)</td><td>—</td></tr><tr><td>**Deals**</td><td>`onSale`</td><td>boolean</td><td>No</td><td>`true` = discounted only</td><td>—</td></tr><tr><td></td><td>`hasGroupBuying`</td><td>boolean</td><td>No</td><td>`true` = group buying enabled on product</td><td>—</td></tr><tr><td></td><td>`hasActiveGroup`</td><td>boolean</td><td>No</td><td>`true` = live OPEN group right now</td><td>—</td></tr><tr><td></td><td>`maxGroupSeatsLeft`</td><td>integer</td><td>No</td><td>Max seats remaining in active group *(post-fetch)*</td><td>—</td></tr><tr><td></td><td>`minGroupDiscountPercent`</td><td>integer</td><td>No</td><td>Min group discount % e.g. `20` *(post-fetch)*</td><td>—</td></tr><tr><td></td><td>`hasInstallments`</td><td>boolean</td><td>No</td><td>`true` = instalment plans available</td><td>—</td></tr><tr><td>**Shop Trust**</td><td>`shopVerified`</td><td>boolean</td><td>No</td><td>`true` = verified shops only</td><td>—</td></tr><tr><td></td><td>`minTrustScore`</td><td>decimal</td><td>No</td><td>Min shop trust score e.g. `4.00` (0.00–5.00)</td><td>—</td></tr><tr><td>**Popularity**</td><td>`minSoldCount`</td><td>integer</td><td>No</td><td>Min total units sold</td><td>—</td></tr></tbody></table>

**Example Requests**:

*Find trending Samsung phones on sale from verified shops:*

```
GET /marketplace/advanced-filter
  ?q=samsung
  &categoryId=a1b2c3d4-...
  &onSale=true
  &shopVerified=true
  &sortBy=TRENDING

```

*Find live group deals saving at least 25% with fewer than 5 seats left:*

```
GET /marketplace/advanced-filter
  ?hasActiveGroup=true
  &minGroupDiscountPercent=25
  &maxGroupSeatsLeft=5
  &sortBy=BEST_DEAL

```

*Find digital products under 50,000 newly published, for anonymous browsing:*

```
GET /marketplace/advanced-filter
  ?productType=DIGITAL
  &maxPrice=50000
  &sortBy=NEWEST

```

**Success Response**: Same structure as the [Main Feed](#1-main-discovery-feed) endpoint.

**Standard Error Types**:

- `500 INTERNAL_SERVER_ERROR`: Unexpected server failure

---

## MarketplaceProductResponse — Full Field Reference

<table id="bkmrk-field-type-nullable-"><thead><tr><th>Field</th><th>Type</th><th>Nullable</th><th>Description</th></tr></thead><tbody><tr><td>`productId`</td><td>UUID</td><td>No</td><td>Unique product identifier</td></tr><tr><td>`productName`</td><td>string</td><td>No</td><td>Product display name</td></tr><tr><td>`productSlug`</td><td>string</td><td>No</td><td>URL-safe unique slug</td></tr><tr><td>`primaryImage`</td><td>string</td><td>Yes</td><td>URL of first product image</td></tr><tr><td>`productType`</td><td>enum</td><td>No</td><td>`PHYSICAL` or `DIGITAL`</td></tr><tr><td>`price`</td><td>decimal</td><td>No</td><td>Current selling price</td></tr><tr><td>`comparePrice`</td><td>decimal</td><td>Yes</td><td>Original price — present only when product is on sale</td></tr><tr><td>`discountPercentage`</td><td>decimal</td><td>Yes</td><td>Regular sale discount % — `(comparePrice - price) / comparePrice × 100`. `null` if not on sale</td></tr><tr><td>`effectiveDiscountPercentage`</td><td>decimal</td><td>Yes</td><td>Best available deal: `max(salePct, activeGroupPct) × 100`. Covers both regular sale AND live group discounts. This is the field the Hot Deals feed sorts by. `null` if no discount of any kind</td></tr><tr><td>`stockQuantity`</td><td>integer</td><td>No</td><td>Available stock units</td></tr><tr><td>`soldQuantity`</td><td>integer</td><td>No</td><td>Total units sold</td></tr><tr><td>`viewCount`</td><td>long</td><td>No</td><td>Cumulative public views</td></tr><tr><td>`cartAddCount`</td><td>long</td><td>No</td><td>Cumulative first-time cart adds</td></tr><tr><td>`urgencyTag`</td><td>enum</td><td>No</td><td>`NONE`, `LIMITED_TIME`, `LOW_STOCK`, `FLASH_SALE`</td></tr><tr><td>`condition`</td><td>enum</td><td>No</td><td>`NEW`, `USED`, `REFURBISHED`</td></tr><tr><td>`inStock`</td><td>boolean</td><td>No</td><td>`true` if `stockQuantity > 0`</td></tr><tr><td>`onSale`</td><td>boolean</td><td>No</td><td>`true` if `comparePrice > price`</td></tr><tr><td>`hasInstallments`</td><td>boolean</td><td>No</td><td>`true` if instalment plans exist</td></tr><tr><td>`shopId`</td><td>UUID</td><td>No</td><td>Owning shop identifier</td></tr><tr><td>`shopName`</td><td>string</td><td>No</td><td>Shop display name</td></tr><tr><td>`shopSlug`</td><td>string</td><td>No</td><td>Shop URL slug</td></tr><tr><td>`shopLogoUrl`</td><td>string</td><td>Yes</td><td>Shop logo image URL</td></tr><tr><td>`shopVerified`</td><td>boolean</td><td>No</td><td>Whether shop has passed verification</td></tr><tr><td>`shopTrustScore`</td><td>decimal</td><td>No</td><td>Shop trust rating `0.00–5.00`</td></tr><tr><td>`categoryId`</td><td>UUID</td><td>Yes</td><td>Product category identifier</td></tr><tr><td>`categoryName`</td><td>string</td><td>Yes</td><td>Product category display name</td></tr><tr><td>`hasActiveGroup`</td><td>boolean</td><td>No</td><td>`true` if a live OPEN group exists</td></tr><tr><td>`activeGroupHeat`</td><td>decimal</td><td>Yes</td><td>Fill ratio `0.0–1.0` of hottest live group</td></tr><tr><td>`activeGroupPrice`</td><td>decimal</td><td>Yes</td><td>Discounted group price</td></tr><tr><td>`activeGroupSeatsLeft`</td><td>integer</td><td>Yes</td><td>Remaining seats in active group</td></tr><tr><td>`activeGroupExpiresAt`</td><td>datetime</td><td>Yes</td><td>Expiry of active group purchase</td></tr><tr><td>`createdAt`</td><td>datetime</td><td>No</td><td>When the product was published</td></tr></tbody></table>

---

## Quick Reference — All Marketplace Endpoints

<table id="bkmrk-%23-endpoint-auth-sort"><thead><tr><th>\#</th><th>Endpoint</th><th>Auth</th><th>Sort Mode</th></tr></thead><tbody><tr><td>1</td><td>`GET /marketplace/feed`</td><td>Optional</td><td>All `MarketplaceSortBy` values</td></tr><tr><td>2</td><td>`GET /marketplace/trending`</td><td>Optional</td><td>Formula-ranked</td></tr><tr><td>3</td><td>`GET /marketplace/for-you`</td><td>Optional</td><td>Relevance-ranked</td></tr><tr><td>4</td><td>`GET /marketplace/hot-deals`</td><td>None</td><td>`effectiveDiscountPercentage` DESC (sale + group)</td></tr><tr><td>5</td><td>`GET /marketplace/new-arrivals`</td><td>None</td><td>`createdAt` DESC</td></tr><tr><td>6</td><td>`GET /marketplace/live-groups`</td><td>None</td><td>Group heat DESC</td></tr><tr><td>7</td><td>`GET /marketplace/advanced-filter`</td><td>Optional</td><td>All `MarketplaceSortBy` values</td></tr></tbody></table>