Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,5 @@ go.work.sum
# env file
.env

bin/
commercify
10 changes: 5 additions & 5 deletions cmd/seed/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -593,17 +593,17 @@ func seedProductVariants(db *sql.DB) error {
productID int
images string
}{
sku: fmt.Sprintf("%s-%s-%s", strings.ReplaceAll(product.name, "'s", ""), color[:1], size),
sku: fmt.Sprintf("%s-%s-%s", strings.ReplaceAll(strings.TrimSpace(product.name), "'s", ""), color[:1], size),
price: basePrice,
stock: 20 - (i * 2) - (j * 1),
attributes: []map[string]string{
{"name": "Color", "value": color},
{"name": "Size", "value": size},
{"name": "Material", "value": materialOptions[i%len(materialOptions)]},
{"name": "Color", "value": strings.TrimSpace(color)},
{"name": "Size", "value": strings.TrimSpace(size)},
{"name": "Material", "value": strings.TrimSpace(materialOptions[i%len(materialOptions)])},
},
isDefault: isDefault,
productID: product.id,
images: fmt.Sprintf(`["/images/%s_%s.jpg"]`, strings.ToLower(strings.ReplaceAll(product.name, " ", "")), strings.ToLower(color)),
images: fmt.Sprintf(`["/images/%s_%s.jpg"]`, strings.ToLower(strings.ReplaceAll(strings.TrimSpace(product.name), " ", "")), strings.ToLower(strings.TrimSpace(color))),
})
}
}
Expand Down
35 changes: 19 additions & 16 deletions docs/checkout_api_examples.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

This document outlines the Checkout API endpoints for the Commercify e-commerce system.

## Important Notes

- **SKUs must be variant SKUs**: When adding or updating items in checkout, the SKU parameter must refer to a product variant SKU, not a product number. All products now have at least one variant, and SKU lookups are performed exclusively against the product_variants table.

## Guest Checkout Endpoints

The following endpoints support guest checkout functionality, allowing users to create and manage checkout sessions without authentication.
Expand Down Expand Up @@ -93,8 +97,7 @@ Adds a product item to the current checkout session.

```json
{
"product_id": 42,
"variant_id": 7,
"sku": "Men-B-M",
"quantity": 1
}
```
Expand Down Expand Up @@ -161,21 +164,20 @@ Adds a product item to the current checkout session.
### Update Checkout Item

```plaintext
PUT /api/checkout/items/{productId}
PUT /api/checkout/items/{sku}
```

Updates the quantity or variant of an item in the current checkout.
Updates the quantity of an item in the current checkout.

**Path Parameters:**

- `productId`: ID of the product to update
- `sku`: SKU of the product variant to update

**Request Body:**

```json
{
"quantity": 2,
"variant_id": 8
"quantity": 2
}
```

Expand Down Expand Up @@ -224,14 +226,14 @@ Updates the quantity or variant of an item in the current checkout.
### Remove Item from Checkout

```plaintext
DELETE /api/checkout/items/{productId}
DELETE /api/checkout/items/{sku}
```

Removes an item from the current checkout session.
Removes an item from the current checkout session using the product variant SKU.

**Path Parameters:**

- `productId`: ID of the product to remove
- `sku`: SKU of the product variant to remove (e.g., "TS-BL-M")

**Response Body:**

Expand Down Expand Up @@ -272,7 +274,8 @@ Removes an item from the current checkout session.
**Status Codes:**

- `200 OK`: Item removed successfully
- `404 Not Found`: Product not found in checkout
- `400 Bad Request`: SKU not provided in URL path
- `404 Not Found`: Product variant not found with provided SKU
- `500 Internal Server Error`: Server error

### Clear Checkout
Expand Down Expand Up @@ -479,7 +482,7 @@ Sets the customer contact information for the current checkout.
"address_line1": "123 Main Street",
"address_line2": "Apt 4B",
"city": "Springfield",
"state": "IL",
"state": "IL",
"postal_code": "62704",
"country": "US"
},
Expand Down Expand Up @@ -629,15 +632,15 @@ Applies a discount code to the current checkout.
"total_weight": 0.6,
"currency": "USD",
"discount_code": "SUMMER25",
"discount_amount": 12.50,
"discount_amount": 12.5,
"final_amount": 43.47,
"applied_discount": {
"id": 5,
"code": "SUMMER25",
"type": "basket",
"method": "percentage",
"value": 25,
"amount": 12.50
"amount": 12.5
},
"updated_at": "2025-05-24T11:10:00Z",
"last_activity_at": "2025-05-24T11:10:00Z",
Expand Down Expand Up @@ -712,12 +715,12 @@ Removes any applied discount code from the current checkout.

### Complete Checkout

```plaintext
````plaintext
### Complete Checkout

```plaintext
POST /api/checkout/complete
```
````

**Request Body:**

Expand Down
60 changes: 16 additions & 44 deletions docs/commercify_checkout_postman_tests.json
Original file line number Diff line number Diff line change
Expand Up @@ -79,14 +79,9 @@
" pm.expect(response.items.length).to.be.greaterThan(0);",
" ",
" // Check if the item we added is in the cart",
" const addedItem = response.items.find(item => item.product_id === parseInt(pm.collectionVariables.get('product_id')));",
" const addedItem = response.items.find(item => item.sku === pm.collectionVariables.get('first_sku'));",
" pm.expect(addedItem).to.not.be.undefined;",
" pm.expect(addedItem.quantity).to.equal(1);",
" ",
" // Store the first item's product_id for later updates",
" if (response.items.length > 0) {",
" pm.collectionVariables.set('first_item_product_id', response.items[0].product_id);",
" }",
"});",
"",
"pm.test(\"Checkout totals are correct\", function () {",
Expand All @@ -113,7 +108,7 @@
],
"body": {
"mode": "raw",
"raw": "{\n \"product_id\": {{product_id}},\n \"variant_id\": {{variant_id}},\n \"quantity\": 1\n}"
"raw": "{\n \"sku\": \"{{first_sku}}\",\n \"quantity\": 1\n}"
},
"url": {
"raw": "{{baseUrl}}/api/checkout/items",
Expand Down Expand Up @@ -142,10 +137,10 @@
"});",
"",
"const response = pm.response.json();",
"const productId = pm.collectionVariables.get('first_item_product_id');",
"const firstSku = pm.collectionVariables.get('first_sku');",
"",
"pm.test(\"Item quantity was updated\", function () {",
" const updatedItem = response.items.find(item => item.product_id === parseInt(productId));",
" const updatedItem = response.items.find(item => item.sku === firstSku);",
" pm.expect(updatedItem).to.not.be.undefined;",
" pm.expect(updatedItem.quantity).to.equal(2);",
"});",
Expand Down Expand Up @@ -177,15 +172,15 @@
"raw": "{\n \"quantity\": 2\n}"
},
"url": {
"raw": "{{baseUrl}}/api/checkout/items/{{first_item_product_id}}",
"raw": "{{baseUrl}}/api/checkout/items/{{first_sku}}",
"host": [
"{{baseUrl}}"
],
"path": [
"api",
"checkout",
"items",
"{{first_item_product_id}}"
"{{first_sku}}"
]
},
"description": "Updates the quantity of an item in the checkout."
Expand All @@ -210,12 +205,9 @@
" pm.expect(response.items.length).to.be.greaterThan(1);",
" ",
" // Check if the second item we added is in the cart",
" const secondItem = response.items.find(item => item.product_id === parseInt(pm.collectionVariables.get('second_product_id')));",
" const secondItem = response.items.find(item => item.sku === pm.collectionVariables.get('second_sku'));",
" pm.expect(secondItem).to.not.be.undefined;",
" pm.expect(secondItem.quantity).to.equal(1);",
" ",
" // Store the second item id for later use",
" pm.collectionVariables.set('second_item_product_id', secondItem.product_id);",
"});"
],
"type": "text/javascript"
Expand All @@ -232,7 +224,7 @@
],
"body": {
"mode": "raw",
"raw": "{\n \"product_id\": {{second_product_id}},\n \"variant_id\": {{second_variant_id}},\n \"quantity\": 1\n}"
"raw": "{\n \"sku\": \"{{second_sku}}\",\n \"quantity\": 1\n}"
},
"url": {
"raw": "{{baseUrl}}/api/checkout/items",
Expand Down Expand Up @@ -640,10 +632,10 @@
"});",
"",
"const response = pm.response.json();",
"const removedProductId = pm.collectionVariables.get('second_item_product_id');",
"const secondSku = pm.collectionVariables.get('second_sku');",
"",
"pm.test(\"Item was removed from checkout\", function () {",
" const removedItem = response.items.find(item => item.product_id === parseInt(removedProductId));",
" const removedItem = response.items.find(item => item.sku === secondSku);",
" pm.expect(removedItem).to.be.undefined;",
" ",
" // Check we still have one item left",
Expand All @@ -658,15 +650,15 @@
"method": "DELETE",
"header": [],
"url": {
"raw": "{{baseUrl}}/api/checkout/items/{{second_item_product_id}}",
"raw": "{{baseUrl}}/api/checkout/items/{{second_sku}}",
"host": [
"{{baseUrl}}"
],
"path": [
"api",
"checkout",
"items",
"{{second_item_product_id}}"
"{{second_sku}}"
]
},
"description": "Removes a specific item from the checkout."
Expand Down Expand Up @@ -1094,33 +1086,13 @@
"type": "string"
},
{
"key": "product_id",
"value": "1",
"type": "string"
},
{
"key": "variant_id",
"value": "1",
"type": "string"
},
{
"key": "second_product_id",
"value": "2",
"key": "first_sku",
"value": "Men-B-M",
"type": "string"
},
{
"key": "second_variant_id",
"value": "10",
"type": "string"
},
{
"key": "first_item_product_id",
"value": "",
"type": "string"
},
{
"key": "second_item_product_id",
"value": "",
"key": "second_sku",
"value": "Women-R-L",
"type": "string"
},
{
Expand Down
44 changes: 39 additions & 5 deletions docs/product_api_examples.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@

This document provides example request bodies and responses for the product system API endpoints.

## Important Notes

- **All products must have at least one variant**: Every product in the system is required to have at least one product variant. If no variants are specified when creating a product, a default variant will be automatically created using the product's basic information.
- **SKUs are variant-specific**: All SKU-based operations (like adding items to checkout) must use variant SKUs, not product numbers.
- **Product numbers are deprecated**: While products still have product numbers for backward compatibility, all SKU lookups are now performed against variant SKUs.

## Public Product Endpoints

### List Products
Expand Down Expand Up @@ -34,7 +40,19 @@ Example response:
"category_id": 1,
"seller_id": 2,
"images": ["smartphone.jpg"],
"has_variants": false
"has_variants": true,
"variants": [
{
"id": 1,
"product_id": 1,
"sku": "PROD-000001",
"price": 999.99,
"stock_quantity": 50,
"attributes": [],
"images": [],
"is_default": true
}
]
},
{
"id": 2,
Expand Down Expand Up @@ -273,10 +291,12 @@ Request body:
"weight": 1.5,
"category_id": 1,
"images": ["product.jpg"],
"has_variants": false
"variants": []
}
```

**Note:** All products must have at least one variant. If no variants are provided in the request, a default variant will be automatically created using the product's basic information (price, stock) and the product number as the SKU.

Example response:

```json
Expand All @@ -295,7 +315,21 @@ Example response:
"category_id": 1,
"seller_id": 2,
"images": ["product.jpg"],
"has_variants": false
"has_variants": true,
"variants": [
{
"id": 1,
"product_id": 4,
"sku": "PROD-000004",
"price": 199.99,
"stock_quantity": 100,
"attributes": [],
"images": [],
"is_default": true,
"created_at": "2023-04-25T14:00:00Z",
"updated_at": "2023-04-25T14:00:00Z"
}
]
}
}
```
Expand Down Expand Up @@ -345,7 +379,7 @@ Example response:
"category_id": 1,
"seller_id": 2,
"images": ["updated-product.jpg"],
"has_variants": false
"has_variants": true
}
}
```
Expand Down Expand Up @@ -411,7 +445,7 @@ Example response:
"category_id": 1,
"seller_id": 2,
"images": ["updated-product.jpg"],
"has_variants": false
"has_variants": true
}
],
"pagination": {
Expand Down
Loading