# API Documentation

Base URL: `http://localhost:3000/api/v1`

The API is served by NestJS. Unless stated otherwise, responses are JSON and errors use NestJS HTTP error responses.

## Authentication

Protected endpoints require an access token:

```http
Authorization: Bearer <access-token>
```

Access tokens are accepted only for active users. The login, refresh, and logout flow also uses an HTTP-only `refresh_token` cookie scoped to `/api/v1/auth`.

Role names used by staff endpoints:

- `ADMIN`
- `SUPER_ADMIN`
- `INVENTORY_MANAGER`

Global request validation strips unknown properties and rejects non-whitelisted properties for class-based DTOs. `transform` is enabled.

## Endpoint Reference

### Health

| Method | Path | Auth | Description |
| --- | --- | --- | --- |
| GET | `/database/health` | Public | Check database connectivity. Returns `{ status: "ok", database: "connected" }`. |

### Authentication

| Method | Path | Auth | Request |
| --- | --- | --- | --- |
| POST | `/auth/register` | Public | JSON body: `firstName`, `lastName`, `email`, `password`; optional `phone`. |
| GET | `/auth/verify-email?token=<token>` | Public | Query: `token`. |
| POST | `/auth/login` | Public | JSON body: `email`, `password`. Sets the refresh cookie and returns an access token. Limited to 5 requests per 60 seconds. |
| POST | `/auth/refresh` | Refresh cookie | Reads `refresh_token` from the cookie, rotates it, and returns `{ accessToken, expiresAt }`. |
| POST | `/auth/logout` | Refresh cookie | Revokes the refresh token and clears the cookie. |
| POST | `/auth/forgot-password` | Public | JSON body: `email`. |
| POST | `/auth/reset-password` | Public | JSON body: `token`, `newPassword`. |
| GET | `/auth/me` | Bearer token | Returns the authenticated user. |

### Products and Categories

| Method | Path | Auth | Request |
| --- | --- | --- | --- |
| GET | `/products` | Public | List products. |
| GET | `/products/:id` | Public | Get a product by ID. |
| GET | `/products/slug/:slug` | Public | Get a product by slug. |
| POST | `/products` | Admin staff | JSON body: required `categoryId`, `name`, `basePrice`, `variants`; optional `description`, `images`. Variant fields: `sku`, `size`, `price`, `quantity`; optional `color`. Image fields: `url`; optional `altText`. |
| PATCH | `/products/:id` | Admin staff | Update a product. JSON body: optional `categoryId`, `name`, `description`, `basePrice`. |
| DELETE | `/products/:id` | Admin staff | Delete a product (cascades to variants and images). |
| GET | `/categories` | Public | List categories. |
| GET | `/categories/:id` | Public | Get a category by ID. |
| GET | `/categories/slug/:slug` | Public | Get a category by slug. |
| POST | `/categories` | `ADMIN`, `SUPER_ADMIN` | JSON body: required `name`; optional `description`, `imageUrl`. |

### Product Images

| Method | Path | Auth | Request |
| --- | --- | --- | --- |
| GET | `/products/:productId/images` | Public | List images for a product. |
| POST | `/products/:productId/images` | Admin staff | `multipart/form-data`: required file field `image`; optional fields `altText`, `sortOrder`, `isPrimary`. `sortOrder` must be a non-negative integer. `isPrimary` is true only for the string `true`. |
| PATCH | `/products/images/:imageId` | Admin staff | JSON body: optional `altText`, `sortOrder`, `isPrimary`. |
| PATCH | `/products/:productId/images/:imageId/primary` | Admin staff | Set an image as primary. No body. |
| DELETE | `/products/images/:imageId` | Admin staff | Delete an image from storage and the database. |

“Admin staff” means one of `ADMIN`, `SUPER_ADMIN`, or `INVENTORY_MANAGER`.

### Inventory

All inventory routes require a bearer token and a staff role.

| Method | Path | Roles | Request |
| --- | --- | --- | --- |
| GET | `/inventory` | Admin staff | List inventory. |
| GET | `/inventory/low-stock` | Admin staff | List low-stock variants. |
| GET | `/inventory/:variantId` | Admin staff | Get inventory for a variant. |
| POST | `/inventory/:variantId/restock` | Admin staff | JSON body: required positive integer `quantity`; optional `note`. |
| POST | `/inventory/:variantId/reserve` | `ADMIN`, `SUPER_ADMIN` | JSON body: required positive integer `quantity`. |

### Addresses, Cart, and Checkout

These routes require a bearer token and use the authenticated user’s ID.

| Method | Path | Request |
| --- | --- | --- |
| POST | `/addresses` | JSON body: required `firstName`, `lastName`, `phone`, `addressLine`, `city`, `state`; optional `country`, `postalCode`, `isDefault`. |
| GET | `/addresses` | List the user’s addresses. |
| GET | `/addresses/:id` | Get an address. |
| PATCH | `/addresses/:id` | Partially update an address. |
| PATCH | `/addresses/:id/default` | Set an address as default. |
| DELETE | `/addresses/:id` | Delete an address. |
| POST | `/cart/items` | JSON body: required `variantId`, positive integer `quantity`. |
| GET | `/cart` | Get the user’s cart. |
| PATCH | `/cart/items/:variantId` | JSON body: required positive integer `quantity`. |
| DELETE | `/cart/items/:variantId` | Remove the item from the user’s cart. |
| POST | `/checkout` | JSON body: required `shippingAddressId`, `shippingRegion`; optional `couponCode`. Creates an order and calculates the shipping fee from the selected region. |
| GET | `/checkout/:orderId` | Get checkout/order information for the user. |

#### Shipping regions and fees

The frontend must send a `shippingRegion` value before checkout. The backend maps that value to the delivery fee and adds it to the order subtotal.

| Region value | Fee | Coverage |
| --- | ---: | --- |
| `REGION_A` | ₦3,500 | Chevron, Lekki, VGC |
| `REGION_B` | ₦4,000 | Ajah, Ikoyi, Victoria Island |
| `REGION_C` | ₦5,000 | Awoyaya, Lakowe, Sangotedo |
| `REGION_D` | ₦6,000 | Abule Egba, Ago Palace, Anthony, Apapa, Costain, Festac, Ikeja, Isolo, Ketu, Maryland, Ogba, Ojodu, Omole, Orile, Oshodi, Palmgroove, Satellite, Surulere, Yaba |
| `REGION_E` | ₦7,000 | Agege, Alagbado, Alakuko, Ayobo, Egbeda, Ejigbo, Fagba, Iba, Igando, Idimu, Iju, Ikotun, Ikorodu, Iyana Isashi, Iyana Ira, Iyana Paja |
| `OTHER_STATE` | ₦8,000 | Any other state or unmapped location |

Example checkout request:

```json
{
  "shippingAddressId": "addr_123",
  "shippingRegion": "REGION_B",
  "couponCode": "OPTIONAL"
}
```

The server calculates the order total as:

```text
subtotal + shippingFee
```

### Orders

These routes require a bearer token and are scoped to the authenticated user.

| Method | Path | Description |
| --- | --- | --- |
| GET | `/orders` | List the user’s orders. |
| GET | `/orders/:id` | Get one of the user’s orders. |
| DELETE | `/orders/:id` | Cancel the user’s order. |

Staff order access:

| Method | Path | Roles | Description |
| --- | --- | --- | --- |
| GET | `/orders/successful` | `ADMIN`, `SUPER_ADMIN`, `ORDER_MANAGER` | List successful orders for all customers. |

### Payments

| Method | Path | Auth | Request |
| --- | --- | --- | --- |
| POST | `/payments/initialize` | Bearer token | JSON body: `orderId`; optional `callbackUrl` (frontend URL). |
| GET | `/payments/verify/:reference` | Bearer token | Verify a payment reference. |
| POST | `/payments/webhook` | Public | Header `x-paystack-signature`; raw Paystack webhook body. Returns `{ status: true }` on success. |

The webhook is intentionally public because Paystack calls it directly. Its signature is verified by the payment service using the raw request body.

When `callbackUrl` is supplied, it is passed to Paystack as `callback_url`. Paystack redirects the customer there after the payment attempt and includes the transaction reference in the query string. The frontend should call `GET /payments/verify/:reference` with the authenticated customer token and use its response status (`SUCCESS`, `failed`, or `abandoned`) to show the result. For a fixed callback URL, set `PAYSTACK_CALLBACK_URL`; a request value takes precedence.

### Shipments

All routes currently require a bearer token. Customer reads are scoped to the authenticated customer.

| Method | Path | Request |
| --- | --- | --- |
| GET | `/shipments` | List the user’s shipments. |
| GET | `/shipments/order/:orderId` | Get the user’s shipment for an order. |
| POST | `/shipments/order/:orderId` | JSON body: optional `courier`, `trackingNumber`. |
| PATCH | `/shipments/:id/status` | JSON body: required `status` enum; optional `courier`, `trackingNumber`. |

The create-shipment and update-status handlers are described as staff-only in source comments, but they currently do not enforce staff roles. Treat this as an implementation caveat until role guards are added.

### Reviews

| Method | Path | Auth | Request |
| --- | --- | --- | --- |
| GET | `/reviews/product/:productId` | Public | List approved reviews for a product. |
| POST | `/reviews/product/:productId` | Bearer token | JSON body: required integer `rating` from 1 to 5; optional `comment`. |
| GET | `/reviews/pending` | `ADMIN`, `SUPER_ADMIN` | List pending reviews. |
| PATCH | `/reviews/:id/moderate` | `ADMIN`, `SUPER_ADMIN` | JSON body: `status`, either `APPROVED` or `REJECTED`. |

### Wishlist

All wishlist routes require a bearer token and are scoped to the authenticated user.

| Method | Path | Request |
| --- | --- | --- |
| GET | `/wishlist` | Retrieve the user's wishlist with all items. |
| POST | `/wishlist/items` | JSON body: required `productId`. Add a product to the wishlist. |
| DELETE | `/wishlist/items/:productId` | Remove a product from the wishlist. |
| DELETE | `/wishlist` | Clear all items from the user's wishlist. |

## Example Requests

Register and log in:

```bash
curl -X POST http://localhost:3000/api/v1/auth/register \
  -H 'Content-Type: application/json' \
  -d '{"firstName":"Ada","lastName":"Lovelace","email":"ada@example.com","password":"strong-password"}'

curl -i -c cookies.txt -X POST http://localhost:3000/api/v1/auth/login \
  -H 'Content-Type: application/json' \
  -d '{"email":"ada@example.com","password":"strong-password"}'
```

Use the returned access token:

```bash
curl http://localhost:3000/api/v1/products \
  -H 'Authorization: Bearer <access-token>'
```

Upload a product image:

```bash
curl -X POST http://localhost:3000/api/v1/products/<product-id>/images \
  -H 'Authorization: Bearer <staff-access-token>' \
  -F 'image=@product.jpg' \
  -F 'altText=Product front view' \
  -F 'sortOrder=0' \
  -F 'isPrimary=true'
```

Create a product review:

```bash
curl -X POST http://localhost:3000/api/v1/reviews/product/<product-id> \
  -H 'Authorization: Bearer <access-token>' \
  -H 'Content-Type: application/json' \
  -d '{"rating":5,"comment":"Great product!"}'
```

Get approved reviews for a product:

```bash
curl http://localhost:3000/api/v1/reviews/product/<product-id>
```

Add a product to wishlist:

```bash
curl -X POST http://localhost:3000/api/v1/wishlist/items \
  -H 'Authorization: Bearer <access-token>' \
  -H 'Content-Type: application/json' \
  -d '{"productId":"<product-id>"}'
```

Get user's wishlist:

```bash
curl http://localhost:3000/api/v1/wishlist \
  -H 'Authorization: Bearer <access-token>'
```

Remove product from wishlist:

```bash
curl -X DELETE http://localhost:3000/api/v1/wishlist/items/<product-id> \
  -H 'Authorization: Bearer <access-token>'
```

## Running Locally

```bash
npm install
npm run start:dev
```

The default server listens on port `3000`. Set `PORT` to use another port. The database and payment/storage environment variables must also be configured; see `.env.example`.
