Free tutorials & notes in Hindi & English · Clean code examples 🧰 Free PDF & Image Tools →
🛠️ PHP + MySQL REST API  ·  Lesson 07

Cart API

Cart rules

  1. 1Only logged-in users have a cart (require_auth())
  2. 2One row per product per user — tapping "Add" again increases the quantity
  3. 3Quantity is between 1 and 10, and never more than the stock
  4. 4Prices and totals are always calculated on the server, never sent by the app
  5. 5Free delivery when the subtotal is ₹499 or more, otherwise ₹40 (set in config.php)
  6. 6Hidden or out-of-stock items stay visible but are marked available: false and not counted
What the API guarantees
9:41▮▮ 85%☰Cart⋮🎉 You get FREE delivery on thisorder⌚Smart Watch Pro₹1,899− 1 +📘Learn Kotlin in 30 Days₹349 × 2− 1 +Price (3 items)₹5,997Product discount− ₹3,400Total amount₹2,597Checkout🏠Home🗂️Categories♡Wishlist🛒Cart👤Account
Every number on this screen comes from one cart response

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 – backend/includes/cart.php
<?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:

SQL – the upsert
-- 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 – backend/api/cart/add.php
<?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.');
require_auth()401 if no tokenproduct exists?404 if hidden / wrong idnew qty = in cart + qtye.g. 1 + 1 = 2≤ stock and ≤ 10?409 "Only 12 left in stock."INSERT … ON DUPLICATE KEYone query for new and existingreturn cart_summary()items + totals
add.php step by step

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 – backend/api/cart/update.php
<?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 – backend/api/cart/remove.php
<?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 – backend/api/cart/list.php
<?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

FieldFormulaExample
mrp_totalΣ mrp × qty4999 + 499 × 2 = 5,997
subtotalΣ price × qty1899 + 349 × 2 = 2,597
discountcoupon discount (0 without coupon)0
delivery_fee0 if subtotal ≥ 499, else 400
totalsubtotal − discount + delivery_fee2,597
you_savemrp_total − subtotal + discount3,400
⚠️ Never trust a price from the app

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.

Real responses

Add the first product
POST /karteasy/api/cart/add.php Authorization: Bearer 3f9c…e1a7 {"product_id": 7, "qty": 1} HTTP 200 { "success": true, "message": "Added to cart.", "data": { "items": [ { "product_id": 7, "name": "Smart Watch Pro", "image": "https://yourdomain.com/karteasy/uploads/products/p7.png", "price": 1899, "mrp": 4999, "qty": 1, "stock": 35, "line_total": 1899, "available": true } ], "item_count": 1, "mrp_total": 4999, "subtotal": 1899, "you_save": 3100, "discount": 0, "coupon_code": null, "coupon_message": null, "delivery_fee": 0, "total": 1899 } }
Add a second product (qty 2)
POST /karteasy/api/cart/add.php Authorization: Bearer 3f9c…e1a7 {"product_id": 13, "qty": 2} HTTP 200 { "success": true, "message": "Added to cart.", "data": { "items": [ { "product_id": 13, "name": "Learn Kotlin in 30 Days", "image": "https://yourdomain.com/karteasy/uploads/products/p13.png", "price": 349, "mrp": 499, "qty": 2, "stock": 70, "line_total": 698, "available": true }, { "product_id": 7, "name": "Smart Watch Pro", "image": "https://yourdomain.com/karteasy/uploads/products/p7.png", "price": 1899, "mrp": 4999, "qty": 1, "stock": 35, "line_total": 1899, "available": true } ], "item_count": 3, "mrp_total": 5997, "subtotal": 2597, "you_save": 3400, "discount": 0, "coupon_code": null, "coupon_message": null, "delivery_fee": 0, "total": 2597 } }
Too many
POST /karteasy/api/cart/add.php Authorization: Bearer 3f9c…e1a7 {"product_id": 16, "qty": 13} HTTP 409 { "success": false, "message": "Only 12 left in stock.", "data": null }
Change quantity with − button
POST /karteasy/api/cart/update.php Authorization: Bearer 3f9c…e1a7 {"product_id": 13, "qty": 1} HTTP 200 { "success": true, "message": "Cart updated.", "data": { "items": [ { "product_id": 13, "name": "Learn Kotlin in 30 Days", "image": "https://yourdomain.com/karteasy/uploads/products/p13.png", "price": 349, "mrp": 499, "qty": 1, "stock": 70, "line_total": 349, "available": true }, { "product_id": 7, "name": "Smart Watch Pro", "image": "https://yourdomain.com/karteasy/uploads/products/p7.png", "price": 1899, "mrp": 4999, "qty": 1, "stock": 35, "line_total": 1899, "available": true } ], "item_count": 2, "mrp_total": 5498, "subtotal": 2248, "you_save": 3250, "discount": 0, "coupon_code": null, "coupon_message": null, "delivery_fee": 0, "total": 2248 } }

Frequently Asked Questions

Why does every cart call return the whole cart?
After add, update or remove, the app needs the new totals anyway. Returning the full cart saves a second request and keeps the badge, list and price box always in sync with the server.
What if the price changes while the item is in the cart?
The cart table stores only product_id and qty. Prices are read from products every time, so the cart always shows today’s price, and the order is made with it.
Should guests (not logged in) have a cart?
KartEasy asks users to log in first, which keeps the API simple. A guest cart can be added by keeping items in the phone (DataStore/Room) and sending them to cart/add.php after login.
← E-commerce App Tutorial
🔗

Share this topic with a friend

यह topic किसी दोस्त को भेजें

Found it useful? Send it to a classmate learning the same thing.

अच्छा लगा? जो दोस्त यही सीख रहा है, उसे भेज दीजिए।

Next topicWishlist & Address API Your progress: 7 / 28 (25%) · Lesson 8 / 28 Continue →

How was this lesson?

Your feedback helps us make the content better.

Write a review or suggestion (optional)

After approval, your name and city may appear publicly with your review. Please do not write your email or phone number.

▶ CodingEasily on YouTube

Learn with video lessons in easy Hindi

Featured video: RDBMS Part 3 – Class XI Commerce IT

▶ Subscribe – it’s free