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

Orders & Checkout API

What happens when you tap "Place order"

Check addressmust belong to this userBEGIN transactionnothing is saved yetLock cart productsSELECT … FOR UPDATECheck stock + couponstop with 409 if anything is wrongINSERT order + order_itemssnapshot of name, price, addressReduce stock, empty cartUPDATE products, DELETE cart_itemsCOMMITall of it becomes real at once
orders/place.php in one picture

Transactions in 2 minutes

A transaction groups several queries so that either all of them happen or none. Imagine the power goes off after the stock was reduced but before the order was saved — without a transaction, one watch would simply disappear.

PHP – the pattern
<?php
$pdo = db();
$pdo->beginTransaction();          // from here, nothing is final
try {
    $pdo->prepare('UPDATE products SET stock = stock - 1 WHERE id = 7')->execute();
    $pdo->prepare('INSERT INTO orders (…) VALUES (…)')->execute(array(/* … */));
    $pdo->commit();                // both changes become real together
} catch (Exception $e) {
    $pdo->rollBack();              // undo BOTH, as if nothing happened
}

The last-piece problem

Only one Cricket Bat is left. Riya and Aman tap "Place order" in the same second. Without locking, both see stock = 1 and both orders succeed. SELECT … FOR UPDATE makes the second request wait until the first one commits:

AmanMySQLRiyaSELECT stock … FOR UPDATE → 1 (row locked)SELECT stock … FOR UPDATE → waits ⏳INSERT order, stock = 0, COMMITnow returns stock = 0409 "Only 0 left…"
Row locks stop overselling

Step 1: orders/place.php

Notice that prices are calculated again with cart_summary() inside the transaction — the order uses the server's numbers, and only Cash on Delivery is accepted for now.

PHP – backend/api/orders/place.php
<?php
// POST {address_id, coupon?, payment_method?}
// Turns the cart into an order inside ONE database transaction:
// lock products -> check stock -> create order + items -> reduce stock -> empty cart.
require_once __DIR__ . '/../../includes/helpers.php';
require_once __DIR__ . '/../../includes/cart.php';
require_once __DIR__ . '/../../includes/address.php';
require_method('POST');
$user = require_auth();

$addressId = in_int('address_id');
$coupon = in_str('coupon');
$payment = strtoupper(in_str('payment_method', 'COD'));
if ($payment !== 'COD') {
    fail('Only Cash on Delivery is available right now.', 422);
}

$stmt = db()->prepare('SELECT * FROM addresses WHERE id = ? AND user_id = ?');
$stmt->execute(array($addressId, $user['id']));
$address = $stmt->fetch();
if (!$address) {
    fail('Please select a delivery address.', 422);
}

$pdo = db();
$pdo->beginTransaction();
try {
    // FOR UPDATE locks these product rows until COMMIT, so two buyers
    // cannot both buy the last piece.
    $rows = $pdo->prepare(
        'SELECT c.product_id, c.qty, p.name, p.image, p.price, p.stock, p.is_active
         FROM cart_items c JOIN products p ON p.id = c.product_id
         WHERE c.user_id = ? FOR UPDATE'
    );
    $rows->execute(array($user['id']));
    $items = $rows->fetchAll();
    if (count($items) === 0) {
        throw new RuntimeException('Your cart is empty.');
    }
    foreach ($items as $it) {
        if ((int) $it['is_active'] !== 1) {
            throw new RuntimeException($it['name'] . ' is no longer available. Please remove it from the cart.');
        }
        if ((int) $it['stock'] < (int) $it['qty']) {
            throw new RuntimeException('Only ' . (int) $it['stock'] . ' left of ' . $it['name'] . '.');
        }
    }

    $sum = cart_summary($user['id'], $coupon);
    if ($coupon !== '' && $sum['coupon_code'] === null) {
        throw new RuntimeException($sum['coupon_message']);
    }

    $orderNo = 'KE' . date('ymd') . strtoupper(substr(bin2hex(random_bytes(4)), 0, 6));
    $pdo->prepare(
        'INSERT INTO orders (order_no, user_id, ship_name, ship_phone, ship_address, subtotal, discount,
                             delivery_fee, total, coupon_code, payment_method)
         VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)'
    )->execute(array(
        $orderNo, $user['id'], $address['name'], $address['phone'], address_text($address),
        $sum['subtotal'], $sum['discount'], $sum['delivery_fee'], $sum['total'], $sum['coupon_code'], $payment,
    ));
    $orderId = (int) $pdo->lastInsertId();

    $addItem = $pdo->prepare('INSERT INTO order_items (order_id, product_id, name, image, price, qty) VALUES (?, ?, ?, ?, ?, ?)');
    $reduce = $pdo->prepare('UPDATE products SET stock = stock - ? WHERE id = ?');
    foreach ($items as $it) {
        $addItem->execute(array($orderId, $it['product_id'], $it['name'], $it['image'], $it['price'], $it['qty']));
        $reduce->execute(array($it['qty'], $it['product_id']));
    }
    $pdo->prepare('DELETE FROM cart_items WHERE user_id = ?')->execute(array($user['id']));
    $pdo->commit();
} catch (RuntimeException $e) {
    $pdo->rollBack();
    fail($e->getMessage(), 409);
}

ok(array('order_id' => $orderId, 'order_no' => $orderNo, 'total' => $sum['total']), 'Order placed successfully!');

Order status flow

placedcustomer orderedconfirmedshop acceptedshippedwith courierdeliveredreview allowed
Normal path (admin moves it forward)

Can cancel

  • placed
  • confirmed
  • stock goes back

Cannot cancel

  • shipped
  • delivered
  • already cancelled
cancel.php checks this on the server, the app only hides the button

Step 2: list.php and detail.php

The list shows a small card per order: number, date, item count, first image, total and status. Sub-selects give the count and image in the same query. The detail adds all items, the address snapshot, the price breakdown and a timeline the app draws as tracking steps.

PHP – backend/includes/orders.php
<?php
function order_summary_out($o)
{
    return array(
        'id' => (int) $o['id'],
        'order_no' => $o['order_no'],
        'total' => (float) $o['total'],
        'status' => $o['status'],
        'item_count' => isset($o['item_count']) ? (int) $o['item_count'] : 0,
        'first_image' => isset($o['first_image']) ? img_url($o['first_image']) : null,
        'created_at' => $o['created_at'],
        'can_cancel' => in_array($o['status'], array('placed', 'confirmed'), true),
    );
}
PHP – backend/api/orders/list.php
<?php
require_once __DIR__ . '/../../includes/helpers.php';
require_once __DIR__ . '/../../includes/orders.php';
require_method('GET');
$user = require_auth();

$stmt = db()->prepare(
    'SELECT o.*,
       (SELECT SUM(qty) FROM order_items i WHERE i.order_id = o.id) AS item_count,
       (SELECT image FROM order_items i WHERE i.order_id = o.id ORDER BY i.id LIMIT 1) AS first_image
     FROM orders o WHERE o.user_id = ? ORDER BY o.id DESC'
);
$stmt->execute(array($user['id']));
$out = array();
foreach ($stmt->fetchAll() as $o) {
    $out[] = order_summary_out($o);
}
ok($out);
PHP – backend/api/orders/detail.php
<?php
// GET api/orders/detail.php?id=5
require_once __DIR__ . '/../../includes/helpers.php';
require_once __DIR__ . '/../../includes/orders.php';
require_method('GET');
$user = require_auth();

$stmt = db()->prepare('SELECT * FROM orders WHERE id = ? AND user_id = ?');
$stmt->execute(array(q_int('id'), $user['id']));
$o = $stmt->fetch();
if (!$o) {
    fail('Order not found.', 404);
}

$it = db()->prepare('SELECT product_id, name, image, price, qty FROM order_items WHERE order_id = ? ORDER BY id');
$it->execute(array($o['id']));
$items = array();
$count = 0;
foreach ($it->fetchAll() as $r) {
    $count += (int) $r['qty'];
    $items[] = array(
        'product_id' => (int) $r['product_id'], 'name' => $r['name'], 'image' => img_url($r['image']),
        'price' => (float) $r['price'], 'qty' => (int) $r['qty'], 'line_total' => money($r['price'] * $r['qty']),
    );
}

// Timeline for the tracking UI
$steps = array('placed', 'confirmed', 'shipped', 'delivered');
$reached = array_search($o['status'], $steps, true);
$timeline = array();
foreach ($steps as $i => $s) {
    $timeline[] = array('status' => $s, 'done' => $reached !== false && $i <= $reached);
}

$out = order_summary_out($o);
$out['item_count'] = $count;
$out['first_image'] = count($items) ? $items[0]['image'] : null;
$out['items'] = $items;
$out['ship_name'] = $o['ship_name'];
$out['ship_phone'] = $o['ship_phone'];
$out['ship_address'] = $o['ship_address'];
$out['subtotal'] = (float) $o['subtotal'];
$out['discount'] = (float) $o['discount'];
$out['delivery_fee'] = (float) $o['delivery_fee'];
$out['coupon_code'] = $o['coupon_code'];
$out['payment_method'] = $o['payment_method'];
$out['timeline'] = $timeline;
ok($out);

Step 3: cancel.php

Cancel is also a transaction: lock the order row, check the status, mark it cancelled and add every item's quantity back to stock with one UPDATE … JOIN.

PHP – backend/api/orders/cancel.php
<?php
// POST {id} -> cancel an order that has not shipped yet and put the stock back.
require_once __DIR__ . '/../../includes/helpers.php';
require_method('POST');
$user = require_auth();
$id = in_int('id');

$pdo = db();
$pdo->beginTransaction();
$stmt = $pdo->prepare('SELECT status FROM orders WHERE id = ? AND user_id = ? FOR UPDATE');
$stmt->execute(array($id, $user['id']));
$status = $stmt->fetchColumn();
if ($status === false) {
    $pdo->rollBack();
    fail('Order not found.', 404);
}
if (!in_array($status, array('placed', 'confirmed'), true)) {
    $pdo->rollBack();
    fail('This order is already ' . $status . ' and cannot be cancelled.', 409);
}
$pdo->prepare("UPDATE orders SET status = 'cancelled' WHERE id = ?")->execute(array($id));
$pdo->prepare(
    'UPDATE products p JOIN order_items i ON i.product_id = p.id
     SET p.stock = p.stock + i.qty WHERE i.order_id = ?'
)->execute(array($id));
$pdo->commit();
ok(array('id' => $id, 'status' => 'cancelled'), 'Order cancelled.');

Adding online payment later

KartEasy uses Cash on Delivery so you can run it without a merchant account. To add UPI/cards with a gateway such as Razorpay, the order is created first, then the gateway confirms the payment to your server:

Text – where a payment gateway plugs in
Today (Cash on Delivery)          With an online payment gateway (e.g. Razorpay)
----------------------------      ------------------------------------------------
app → orders/place.php            app → orders/place.php  (status = 'pending_payment')
      status = 'placed'           app → opens the gateway's checkout with order_no + total
                                  gateway → your webhook.php (signature verified on server)
                                  webhook.php → UPDATE orders SET status = 'placed', paid = 1
⚠️ Never mark an order "paid" from the app

The app can be modified. Only your server, after verifying the gateway's signature in the webhook, may set an order as paid.

Real responses

Place order with coupon SAVE10
POST /karteasy/api/orders/place.php Authorization: Bearer 3f9c…e1a7 {"address_id": 1, "coupon": "SAVE10", "payment_method": "COD"} HTTP 200 { "success": true, "message": "Order placed successfully!", "data": { "order_id": 1, "order_no": "KE2610193DFEA8", "total": 2023.2 } }
The cart is now empty
GET /karteasy/api/cart/list.php Authorization: Bearer 3f9c…e1a7 HTTP 200 { "success": true, "message": "OK", "data": { "items": [], "item_count": 0, "mrp_total": 0, "subtotal": 0, "you_save": 0, "discount": 0, "coupon_code": null, "coupon_message": null, "delivery_fee": 0, "total": 0 } }
My orders
GET /karteasy/api/orders/list.php Authorization: Bearer 3f9c…e1a7 HTTP 200 { "success": true, "message": "OK", "data": [ { "id": 1, "order_no": "KE2610193DFEA8", "total": 2023.2, "status": "placed", "item_count": 2, "first_image": "https://yourdomain.com/karteasy/uploads/products/p7.png", "created_at": "2026-10-19 01:28:55", "can_cancel": true } ] }
Order detail with timeline
GET /karteasy/api/orders/detail.php?id=1 Authorization: Bearer 3f9c…e1a7 HTTP 200 { "success": true, "message": "OK", "data": { "id": 1, "order_no": "KE2610193DFEA8", "total": 2023.2, "status": "placed", "item_count": 2, "first_image": "https://yourdomain.com/karteasy/uploads/products/p7.png", "created_at": "2026-10-19 01:28:55", "can_cancel": true, "items": [ { "product_id": 7, "name": "Smart Watch Pro", "image": "https://yourdomain.com/karteasy/uploads/products/p7.png", "price": 1899, "qty": 1, "line_total": 1899 }, { "product_id": 13, "name": "Learn Kotlin in 30 Days", "image": "https://yourdomain.com/karteasy/uploads/products/p13.png", "price": 349, "qty": 1, "line_total": 349 } ], "ship_name": "Riya Sharma", "ship_phone": "9876543210", "ship_address": "12 MG Road, Near City Mall, Indore, Madhya Pradesh - 452001", "subtotal": 2248, "discount": 224.8, "delivery_fee": 0, "coupon_code": "SAVE10", "payment_method": "COD", "timeline": [ { "status": "placed", "done": true }, { "status": "confirmed", "done": false }, "… 2 more" ] } }
Cancel, then try to cancel again
POST /karteasy/api/orders/cancel.php Authorization: Bearer 3f9c…e1a7 {"id": 1} HTTP 200 { "success": true, "message": "Order cancelled.", "data": { "id": 1, "status": "cancelled" } } POST /karteasy/api/orders/cancel.php Authorization: Bearer 3f9c…e1a7 {"id": 1} HTTP 409 { "success": false, "message": "This order is already cancelled and cannot be cancelled.", "data": null }

Frequently Asked Questions

Why RuntimeException inside place.php?
Any problem in the middle (out of stock, hidden product, bad coupon) throws one, the catch block rolls the whole transaction back and replies 409 with the message. Nothing half-saved can remain.
How is the order number made?
KE + yymmdd + 6 random hex characters, for example KE261019C8F96E. It is easy to read on the phone, does not reveal how many orders the shop has, and the UNIQUE key guarantees no duplicates.
Who changes the status to shipped and delivered?
The shop owner, from the admin panel order page. The app only reads the status; customers can cancel while it is placed or confirmed.
← E-commerce App Tutorial
🔗

Share this topic with a friend

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

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

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

Next topicCoupons & Reviews API Your progress: 9 / 28 (32%) · Lesson 10 / 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