Cart API
Cart rules
- 1Only logged-in users have a cart (
require_auth()) - 2One row per product per user — tapping "Add" again increases the quantity
- 3Quantity is between 1 and 10, and never more than the stock
- 4Prices and totals are always calculated on the server, never sent by the app
- 5Free delivery when the subtotal is ₹499 or more, otherwise ₹40 (set in config.php)
- 6Hidden or out-of-stock items stay visible but are marked
available: falseand not counted
Step 1: includes/cart.php
Three endpoints (cart, coupons, orders) need the same calculations, so they live in one shared file. cart_rows() joins the cart with live product data; cart_summary() builds the response; coupon_check() is explained in the Coupons lesson.
<?php
// Cart and coupon calculations, shared by cart/list, coupons/apply and orders/place.
// The server always calculates prices itself: never trust a total sent by the app.
/** Cart rows of a user with live product price and stock. */
function cart_rows($userId)
{
$stmt = db()->prepare(
'SELECT c.product_id, c.qty, p.name, p.image, p.price, p.mrp, p.stock, p.is_active
FROM cart_items c JOIN products p ON p.id = c.product_id
WHERE c.user_id = ? ORDER BY c.id DESC'
);
$stmt->execute(array($userId));
return $stmt->fetchAll();
}
/**
* Check a coupon against a subtotal.
* Returns array('ok' => bool, 'message' => string, 'discount' => float).
*/
function coupon_check($code, $subtotal)
{
$code = strtoupper(trim($code));
if ($code === '') {
return array('ok' => false, 'message' => 'Enter a coupon code.', 'discount' => 0);
}
$stmt = db()->prepare('SELECT * FROM coupons WHERE code = ? AND is_active = 1');
$stmt->execute(array($code));
$c = $stmt->fetch();
if (!$c) {
return array('ok' => false, 'message' => 'Invalid coupon code.', 'discount' => 0);
}
if ($c['expires_at'] !== null && strtotime($c['expires_at']) < time()) {
return array('ok' => false, 'message' => 'This coupon has expired.', 'discount' => 0);
}
if ($subtotal < (float) $c['min_order']) {
return array('ok' => false,
'message' => 'Add items worth ' . rs($c['min_order'] - $subtotal) . ' more to use ' . $code . '.',
'discount' => 0);
}
if ($c['type'] === 'percent') {
$discount = $subtotal * (float) $c['value'] / 100;
if ($c['max_discount'] !== null) {
$discount = min($discount, (float) $c['max_discount']);
}
} else {
$discount = (float) $c['value'];
}
$discount = money(min($discount, $subtotal));
return array('ok' => true, 'message' => 'Coupon applied! You saved ' . rs($discount) . '.', 'discount' => $discount, 'code' => $code);
}
/** Full cart with totals. Pass a coupon code to include its discount. */
function cart_summary($userId, $couponCode = '')
{
$items = array();
$subtotal = 0;
$mrpTotal = 0;
$count = 0;
foreach (cart_rows($userId) as $r) {
$available = ((int) $r['is_active'] === 1) && ((int) $r['stock'] >= (int) $r['qty']);
$line = (float) $r['price'] * (int) $r['qty'];
if ($available) {
$subtotal += $line;
$mrpTotal += (float) $r['mrp'] * (int) $r['qty'];
$count += (int) $r['qty'];
}
$items[] = array(
'product_id' => (int) $r['product_id'],
'name' => $r['name'],
'image' => img_url($r['image']),
'price' => (float) $r['price'],
'mrp' => (float) $r['mrp'],
'qty' => (int) $r['qty'],
'stock' => (int) $r['stock'],
'line_total' => money($line),
'available' => $available,
);
}
$discount = 0;
$couponMsg = null;
$appliedCode = null;
if ($couponCode !== '' && $subtotal > 0) {
$res = coupon_check($couponCode, $subtotal);
$couponMsg = $res['message'];
if ($res['ok']) {
$discount = $res['discount'];
$appliedCode = $res['code'];
}
}
$delivery = ($subtotal == 0 || $subtotal >= FREE_DELIVERY_ABOVE) ? 0 : DELIVERY_FEE;
return array(
'items' => $items,
'item_count' => $count,
'mrp_total' => money($mrpTotal),
'subtotal' => money($subtotal),
'you_save' => money($mrpTotal - $subtotal + $discount),
'discount' => money($discount),
'coupon_code' => $appliedCode,
'coupon_message' => $couponMsg,
'delivery_fee' => money($delivery),
'total' => money($subtotal - $discount + $delivery),
);
}Step 2: cart/add.php
Add first reads the stock and the quantity already in the cart, checks the limits, then uses one "upsert" query:
-- First tap on "Add": no row yet → INSERT -- Second tap: row exists (UNIQUE user_id + product_id) → UPDATE qty instead INSERT INTO cart_items (user_id, product_id, qty) VALUES (1, 7, 2) ON DUPLICATE KEY UPDATE qty = VALUES(qty);
<?php
// POST {product_id, qty} -> adds qty to the cart (creates the row if needed)
require_once __DIR__ . '/../../includes/helpers.php';
require_once __DIR__ . '/../../includes/cart.php';
require_method('POST');
$user = require_auth();
$productId = in_int('product_id');
$qty = max(1, in_int('qty', 1));
$stmt = db()->prepare('SELECT stock FROM products WHERE id = ? AND is_active = 1');
$stmt->execute(array($productId));
$stock = $stmt->fetchColumn();
if ($stock === false) {
fail('Product not found.', 404);
}
$cur = db()->prepare('SELECT qty FROM cart_items WHERE user_id = ? AND product_id = ?');
$cur->execute(array($user['id'], $productId));
$newQty = (int) $cur->fetchColumn() + $qty;
if ($newQty > (int) $stock) {
fail('Only ' . (int) $stock . ' left in stock.', 409);
}
if ($newQty > 10) {
fail('You can buy at most 10 of one product.', 409);
}
// One query for both cases thanks to the UNIQUE(user_id, product_id) key.
db()->prepare(
'INSERT INTO cart_items (user_id, product_id, qty) VALUES (?, ?, ?)
ON DUPLICATE KEY UPDATE qty = VALUES(qty)'
)->execute(array($user['id'], $productId, $newQty));
ok(cart_summary($user['id']), 'Added to cart.');Step 3: update.php and remove.php
update.php sets the exact quantity (used by the − / + buttons). Sending qty: 0 removes the item. remove.php deletes the row (swipe to delete).
<?php
// POST {product_id, qty} -> sets the exact qty (qty 0 removes the item)
require_once __DIR__ . '/../../includes/helpers.php';
require_once __DIR__ . '/../../includes/cart.php';
require_method('POST');
$user = require_auth();
$productId = in_int('product_id');
$qty = in_int('qty');
if ($qty <= 0) {
db()->prepare('DELETE FROM cart_items WHERE user_id = ? AND product_id = ?')->execute(array($user['id'], $productId));
ok(cart_summary($user['id']), 'Item removed.');
}
$stmt = db()->prepare('SELECT stock FROM products WHERE id = ? AND is_active = 1');
$stmt->execute(array($productId));
$stock = $stmt->fetchColumn();
if ($stock === false) {
fail('Product not found.', 404);
}
if ($qty > (int) $stock) {
fail('Only ' . (int) $stock . ' left in stock.', 409);
}
if ($qty > 10) {
fail('You can buy at most 10 of one product.', 409);
}
$upd = db()->prepare('UPDATE cart_items SET qty = ? WHERE user_id = ? AND product_id = ?');
$upd->execute(array($qty, $user['id'], $productId));
if ($upd->rowCount() === 0) {
// Not in cart yet (or same qty) -> make sure the row exists
db()->prepare(
'INSERT INTO cart_items (user_id, product_id, qty) VALUES (?, ?, ?)
ON DUPLICATE KEY UPDATE qty = VALUES(qty)'
)->execute(array($user['id'], $productId, $qty));
}
ok(cart_summary($user['id']), 'Cart updated.');<?php
// POST {product_id}
require_once __DIR__ . '/../../includes/helpers.php';
require_once __DIR__ . '/../../includes/cart.php';
require_method('POST');
$user = require_auth();
db()->prepare('DELETE FROM cart_items WHERE user_id = ? AND product_id = ?')
->execute(array($user['id'], in_int('product_id')));
ok(cart_summary($user['id']), 'Item removed.');Step 4: cart/list.php
Only three lines, because cart_summary() does the work. The optional ?coupon= lets checkout reload the cart with a coupon applied.
<?php
// GET api/cart/list.php?coupon=SAVE10 (coupon is optional)
require_once __DIR__ . '/../../includes/helpers.php';
require_once __DIR__ . '/../../includes/cart.php';
require_method('GET');
$user = require_auth();
ok(cart_summary($user['id'], q_str('coupon')));How the totals are calculated
| Field | Formula | Example |
|---|---|---|
| mrp_total | Σ mrp × qty | 4999 + 499 × 2 = 5,997 |
| subtotal | Σ price × qty | 1899 + 349 × 2 = 2,597 |
| discount | coupon discount (0 without coupon) | 0 |
| delivery_fee | 0 if subtotal ≥ 499, else 40 | 0 |
| total | subtotal − discount + delivery_fee | 2,597 |
| you_save | mrp_total − subtotal + discount | 3,400 |
Someone can call the API with a tool and send "price": 1. That is why add.php accepts only product_id and qty, and every rupee is calculated from the database.
