Products Management Service
Product Management
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:
Endpoints
1. Create Product
Purpose: Creates a new product in a shop, supporting group buying, color variations, specifications, and digital product rules.
Endpoint: POST 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):
{
"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):
{
"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:
{
"success": true,
"message": "Product created successfully",
"data": null
}
Business Rules:
- Product name must be unique within the shop
comparePricemust be greater thanpricegroupPricemust be less thanprice- All group buying settings (
groupMaxSize,groupPrice,groupTimeLimitHours) required whengroupBuyingEnabled=true maxOrderQuantitymust be ≥minOrderQuantity- Digital download fields (
downloadExpiryDays,maxDownloadsPerBuyer,maxQuantityForDigital) only apply toDIGITALproducts - After creation, add installment plans via Installment Plan Config and digital files via Digital File Management
Error Responses:
400: Validation errors or business rule violations401: Authentication required403: Insufficient permissions404: Shop or category not found409: Product with same name already exists in shop422: Field-level validation errors
2. Update Product
Purpose: Updates an existing product. Only provided fields are updated.
Endpoint: PUT 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:
{
"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 errors401: Authentication required403: Insufficient permissions404: Shop or product not found409: Updated product name already exists
3. Publish Product
Purpose: Publishes a draft product making it active and publicly available.
Endpoint: PATCH 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:
{
"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 fields401: Authentication required403: Insufficient permissions404: Shop or product not found
4. Delete Product
Purpose: Deletes a product. Draft products are hard-deleted; published products are soft-deleted.
Endpoint: DELETE 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):
{
"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):
{
"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 required403: Insufficient permissions404: Shop or product not found
5. Restore Product
Purpose: Restores a soft-deleted product back to draft status.
Endpoint: PATCH 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:
{
"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 deleted401: Authentication required403: Insufficient permissions404: Shop or product not found
6. Get Product Detailed (Owner/Admin View)
Purpose: Retrieves comprehensive product details including all management information.
Endpoint: GET 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:
{
"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 required403: Insufficient permissions404: Shop or product not found
7. Get Shop Products (Management View)
Purpose: Retrieves all products for a shop with summary statistics.
Endpoint: GET 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:
{
"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 required403: Insufficient permissions404: Shop not found
8. Get Shop Products Paginated (Management View)
Purpose: Retrieves shop products with pagination for management dashboard.
Endpoint: GET 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:
{
"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 required403: Insufficient permissions404: Shop not found
9. Get Public Product by ID
Purpose: Retrieves a single active product for public viewing.
Endpoint: GET 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:
{
"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:
previewTypeandpreviewUrlarenullwhen 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: GET 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:
{
"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: GET 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: GET 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:
{
"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 long404: Shop not found or not accessible
13. Advanced Product Filter
Purpose: Filters products using multiple criteria with combined AND/OR logic.
Endpoint: GET 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 error404: 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).
Endpoint: GET 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: POST 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:
{
"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 errors404: Shop or product not found
15b. Get All Installment Plans
Endpoint: GET 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: GET 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: PUT 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: DELETE api/v1/e-commerce/products/{shopId}/{productId}/installment-plans/{planId}
15f. Activate / Deactivate Plan
Activate: PATCH api/v1/e-commerce/products/{shopId}/{productId}/installment-plans/{planId}/activate
Deactivate: PATCH 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: PATCH 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: POST 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:
{
"fileName": "design-kit-v2.fig",
"contentType": "application/octet-stream",
"fileSize": 52428800,
"displayOrder": 1
}
Response JSON Sample:
{
"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:
- Call
POST /presign-upload→ receiveuploadUrlandobjectKey PUT {uploadUrl}with binary file body (do not call the API for this step)- Call
POST /confirmwithobjectKeyto 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: POST 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:
{
"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: GET 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: DELETE 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: PATCH 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: POST 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:
{
"fileName": "product-trailer.mp4",
"contentType": "video/mp4",
"fileSize": 52428800
}
Response JSON Sample:
{
"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:
- Call
POST /presign-upload→ receiveuploadUrlandobjectKey PUT {uploadUrl}with binary file body (client-to-MinIO directly, not through the API)- Call
POST /confirmwithobjectKeyandpreviewTypeto link the file to the product
Error Responses:
401: Authentication required403: Insufficient permissions404: 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: POST 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:
{
"objectKey": "preview/shop-id/product-id/uuid_product-trailer.mp4",
"previewType": "VIDEO",
"previewDownloadable": false
}
Response JSON Sample:
{
"success": true,
"message": "Preview confirmed and linked to product",
"data": null
}
After confirm, the product's public response will include:
{
"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 previewType401: Authentication required403: Insufficient permissions404: 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: DELETE api/v1/e-commerce/shops/{shopId}/products/{productId}/preview
Response JSON Sample:
{
"success": true,
"message": "Preview removed from product",
"data": null
}
Error Responses:
401: Authentication required403: Insufficient permissions404: 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
{
"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
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) orgroupName(creates a new group) — passing both returns400 - 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=truepermanently removes those items from the wishlist PATCH /{itemId}/groupwithgroupId: nullmoves an item to Ungrouped without deleting itisInWishlist,wishlistItemId,wishlistGroupId, andwishlistGroupNameare populated on the single product detail response (GET /api/v1/e-commerce/products/{slug}) for authenticated users
Standard Response Format
Success Response Structure
{
"success": true,
"httpStatus": "OK",
"message": "Operation completed successfully",
"action_time": "2025-09-23T10:30:45",
"data": {}
}
Error Response Structure
{
"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 token404 NOT_FOUND: Product, wishlist item, or group not found422 UNPROCESSABLE_ENTITY: Validation errors with field-level detail500 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: POST {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):
{
"productId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
}
Request JSON Sample — add to an existing group:
{
"productId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"groupId": "f9e8d7c6-b5a4-3210-fedc-ba9876543210"
}
Request JSON Sample — add and create a new group:
{
"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:
{
"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:
{
"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:
{
"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:
{
"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: GET {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:
{
"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: GET {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:
{
"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: DELETE {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:
{
"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: DELETE {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:
{
"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: POST {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:
{
"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: PATCH {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:
{
"groupId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
}
Request JSON Sample — remove from group (move to Ungrouped):
{
"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:
{
"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: POST {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:
{
"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:
{
"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:
{
"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: GET {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:
{
"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: DELETE {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:
{
"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:
{
"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.