How to read this page
Each section quotes a real file from the repository (path shown in the header bar). The "Powers" callout under each excerpt names the user flow the code drives in production. All snippets are unedited apart from a small amount of elision marked with three dots.
1. Foundation
1.1 PDO connection with production hardening
declare(strict_types=1); require_once __DIR__ . '/constants.php'; require_once __DIR__ . '/logger.php'; // Production hardening: log everything, never display. $_m2y_is_prod = (getenv('APP_ENV') === 'production') || (! in_array(($_SERVER['SERVER_NAME'] ?? 'cli'), ['localhost', '127.0.0.1', 'cli'], true)); if ($_m2y_is_prod) { ini_set('display_errors', '0'); ini_set('log_errors', '1'); } function db(): PDO { static $pdo = null; if ($pdo) return $pdo; $dsn = 'mysql:host=' . getenv('DB_HOST') . ';dbname=' . getenv('DB_NAME') . ';charset=utf8mb4'; $pdo = new PDO($dsn, getenv('DB_USER'), getenv('DB_PASS'), [ PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, PDO::ATTR_EMULATE_PREPARES => false, // real prepares only ]); return $pdo; }
Powers Every database call in Me2You. EMULATE_PREPARES = false means MySQL parses the SQL once and the parameter binds cannot be re-interpreted as SQL, which is the strongest line of defence against injection.
1.2 Money helpers (rands and money)
// ZAR - default whole rands. Pass 2 for "R 4 500.00". function rands(int $cents, int $decimals = 0): string { return money($cents, 'ZAR', $decimals); }
Powers Every price on every page. Money is stored as INT cents to avoid IEEE float drift; rendering is the only place that converts to a human-readable rand value.
1.3 Auth, RBAC, CSRF and escaping
function require_login(): array { $u = current_user(); if (! $u) { $next = urlencode($_SERVER['REQUEST_URI'] ?? '/'); header("Location: /auth/login.php?next={$next}"); exit; } return $u; } function require_role(string $role): array { $u = require_login(); if ($u['role'] !== $role && $u['role'] !== USER_ROLE_ADMIN) { m2y_fail(403, 'Access denied', 'You do not have permission to view this area.'); } return $u; } function csrf_token(): string { start_session(); if (empty($_SESSION['_csrf'])) { $_SESSION['_csrf'] = bin2hex(random_bytes(16)); } return $_SESSION['_csrf']; } function csrf_check(): void { start_session(); $sent = $_POST['_csrf'] ?? ''; if (! hash_equals($_SESSION['_csrf'] ?? '', $sent)) { m2y_fail(400, 'Session expired', 'Your session token did not match. Please refresh.'); } } function e(string $s): string { return htmlspecialchars($s, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); }
Powers Every protected page (cart, checkout, admin, driver), every form submission and every echoed user-supplied string. Admin implicitly satisfies any role check so the admin panel never gets locked out of a buyer or seller page.
2. Frontend
2.1 Sell form (HTML)
<form method="post" enctype="multipart/form-data" novalidate> <input type="hidden" name="_csrf" value="<?= e(csrf_token()) ?>"> <label for="title">What are you selling?</label> <input id="title" name="title" required maxlength="120" placeholder="e.g. Samsung A04, 64GB, like new"> <label for="price">Price in rands</label> <input id="price" name="price" type="number" step="1" min="0" inputmode="numeric"> <label for="photos">Photos (up to 6, max 2 MB each)</label> <input id="photos" name="photos[]" type="file" accept="image/jpeg,image/png,image/webp" multiple> <button type="submit" class="btn-primary">List item</button> </form>
Powers Sellers list an item. CSRF token is sent with every POST; photo uploads are capped at 6 and 2 MB each; inputmode="numeric" brings up the digit keypad on Android so a granny on a Samsung A04 does not have to switch keyboards.
2.2 Listing card (CSS)
/* Listing card: white paper on warm cream canvas */ .listing-card { background: var(--paper); border: 1px solid var(--ink-200); border-radius: var(--radius-lg); /* 20px */ box-shadow: var(--shadow-sm); display: flex; flex-direction: column; min-height: 300px; } .listing-card img { aspect-ratio: 1 / 1; width: 100%; object-fit: cover; border-radius: var(--radius-lg) var(--radius-lg) 0 0; } .listing-card .price { font-family: var(--font-display); font-weight: 700; color: var(--brand-orange-700); font-size: 18px; } .btn-primary { background: var(--brand-orange); color: var(--ink-900); border: 1px solid var(--brand-orange-700); border-radius: 999px; min-height: 44px; /* preferred tap target */ padding: 10px 18px; font-family: var(--font-display); font-weight: 700; }
Powers Every listing tile in the home grid, the browse page and the search results. 44 pixel min-height on the primary button hits the tap target rule for a finger in a moving minibus.
2.3 Search suggest (vanilla JS, safe DOM build)
function debounce(fn, ms) { let t; return function () { const args = arguments; clearTimeout(t); t = setTimeout(function () { fn.apply(null, args); }, ms); }; } function csrfHeader() { const tag = document.querySelector('meta[name="csrf"]'); return tag ? { 'X-CSRF-Token': tag.content } : {}; } // Safe DOM build: build elements with textContent, never assign HTML strings. function renderSuggestions(panel, items) { panel.replaceChildren(); items.forEach(function (it) { const a = document.createElement('a'); a.href = '/listings/view.php?id=' + encodeURIComponent(it.id); a.textContent = it.title; panel.appendChild(a); }); } const input = document.getElementById('q'); const panel = document.getElementById('suggest'); input.addEventListener('input', debounce(function () { const q = input.value.trim(); if (q.length < 2) { panel.hidden = true; return; } fetch('/api/search_suggest.php?q=' + encodeURIComponent(q), { headers: csrfHeader() }) .then(function (r) { return r.json(); }) .then(function (j) { renderSuggestions(panel, j.items || []); panel.hidden = false; }) .catch(function () { /* network blip - keep last result */ }); }, 180));
Powers The search bar at the top of every marketplace page. 180 ms debounce keeps the request count low on 3G; encodeURIComponent prevents URL injection; textContent + DOM build prevents XSS even if a title contained a tag character.
3. Marketplace
3.1 Create listing: insert with slug and share token
$slug = trim($slug, '-') . '-' . substr(bin2hex(random_bytes(3)), 0, 4); $share_token = bin2hex(random_bytes(8)); $price_cents = ($old['price'] !== '' && is_numeric($old['price'])) ? (int)round((float)$old['price'] * 100) : 0; $stmt = db()->prepare( 'INSERT INTO listings (seller_id, category_id, title, slug, description, price_cents, price_type, kind, condition_grade, weight_kg, lat, lng, status, share_token) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, "pending", ?)' ); $stmt->execute([ $user['id'], $old['category_id'], $old['title'], $slug, $old['description'], $price_cents, $old['price_type'], $old['kind'], $old['condition_grade'], (float)$old['weight_kg'], $listing_lat, $listing_lng, $share_token, ]); $listing_id = (int)db()->lastInsertId();
Powers The sell wizard. The slug is randomised so two sellers listing the same Samsung A04 still get unique URLs; the share token feeds the WhatsApp share-link flow without exposing the seller's internal id.
3.2 Search suggest: prepared LIKE plus join
$like = '%' . $q . '%'; $itemSql = 'SELECT l.id, l.title, l.slug, l.price_cents, l.condition_grade, c.name AS category_name, u.display_name AS seller_name, (SELECT path FROM listing_images WHERE listing_id = l.id ORDER BY sort, id LIMIT 1) AS image FROM listings l JOIN categories c ON c.id = l.category_id JOIN users u ON u.id = l.seller_id WHERE l.status = ? AND l.title LIKE ? ORDER BY l.views DESC, l.id DESC LIMIT 4'; $itemStmt = $pdo->prepare($itemSql); $itemStmt->execute([LISTING_STATUS_LIVE, $like]); $itemRows = $itemStmt->fetchAll();
Powers The autosuggest panel under the search input. Every value is bound as a parameter so the LIKE search cannot be injected; a hard LIMIT of 4 per group keeps the response under the 150 ms budget on 3G.
4. Payments and escrow
4.1 Checkout: pull cart, recompute totals server-side
$rows = db()->prepare( "SELECT ci.qty, l.id, l.title, l.slug, l.price_cents, l.price_type, l.weight_kg, l.seller_id, (SELECT path FROM listing_images WHERE listing_id = l.id ORDER BY sort, id LIMIT 1) AS img FROM cart_items ci JOIN carts c ON c.id = ci.cart_id JOIN listings l ON l.id = ci.listing_id WHERE c.user_id = ? AND l.status = 'live'" ); $rows->execute([$user['id']]); $items = $rows->fetchAll(); $subtotal = 0; foreach ($items as $it) { $subtotal += (int)$it['price_cents'] * (int)$it['qty']; } // Buyer protection fee from admin settings. $protection_cents = max(0, (int) get_setting('buyer_protection_fee', '1490')); // Single-use idempotency token (prevents double-submit). start_session(); if (empty($_SESSION['_checkout_idem'])) { $_SESSION['_checkout_idem'] = bin2hex(random_bytes(16)); }
Powers The /checkout page. Totals are always recomputed from the database side, never trusted from the client. The idempotency token stops a rage-click from creating two orders for the same cart.
4.2 PayFast ITN handler (4-step verified)
// 1. Verify the source IP is in the PayFast allow-list. if (! in_array($_SERVER['REMOTE_ADDR'], PAYFAST_VALID_IPS, true)) { http_response_code(403); exit; } // 2. Re-post the raw payload to PayFast and verify the response says VALID. $verify = payfast_validate($_POST); if ($verify !== 'VALID') { log_warn('itn_invalid', ['pf_payment_id' => $_POST['pf_payment_id'] ?? null]); http_response_code(400); exit; } // 3. Idempotency check: have we processed this pf_payment_id before? $seen = db()->prepare('SELECT id FROM payment_log WHERE pf_payment_id = ? LIMIT 1'); $seen->execute([$_POST['pf_payment_id']]); if ($seen->fetch()) { http_response_code(200); exit; } // 4. Verify the amount the gateway charged matches the amount we asked for. $row = db()->prepare('SELECT total_cents FROM orders WHERE reference = ?'); $row->execute([$_POST['m_payment_id']]); $expected_cents = (int) $row->fetchColumn(); $paid_cents = (int)round((float)$_POST['amount_gross'] * 100); if ($paid_cents !== $expected_cents) { log_warn('itn_amount_mismatch', ['expected' => $expected_cents, 'paid' => $paid_cents]); http_response_code(400); exit; } // All four checks passed. Mark order paid, create escrow row, notify driver pool.
Powers The PayFast Instant Transaction Notification endpoint. The four checks (source IP, signature re-verification, idempotency, amount match) are textbook for a payment webhook and protect against spoofed callbacks.
4.3 Escrow auto-release cron (48 hours after delivery)
if (PHP_SAPI !== 'cli') { http_response_code(403); die('CLI only'); } if (! cron_lock_acquire('escrow_release')) { exit(0); } $ready = $pdo->query( "SELECT o.id, o.reference, o.buyer_id, o.seller_id, e.id AS escrow_id, e.amount_cents, da.id AS assignment_id, da.courier_id, da.payout_cents, d.id AS dispute_id FROM orders o JOIN escrow_transactions e ON e.order_id = o.id LEFT JOIN delivery_assignments da ON da.order_id = o.id LEFT JOIN disputes d ON d.order_id = o.id AND d.status = 'open' WHERE o.status = 'received' AND e.status = 'held' AND e.release_due_at <= NOW() AND d.id IS NULL LIMIT 200" )->fetchAll(); foreach ($ready as $o) { $pdo->beginTransaction(); try { $pdo->prepare('UPDATE orders SET status = "completed", completed_at = NOW() WHERE id = ?') ->execute([$o['id']]); $pdo->prepare('UPDATE escrow_transactions SET status = "released", released_at = NOW(), payout_ref = CONCAT("AUTO-", ?), notes = "48h auto-release" WHERE id = ?') ->execute([$o['reference'], $o['escrow_id']]); if (! empty($o['assignment_id']) && (int)$o['payout_cents'] > 0) { $pdo->prepare('INSERT INTO driver_earnings (courier_id, order_id, base_cents, per_km_cents, status) VALUES (?, ?, ?, 0, "pending")') ->execute([$o['courier_id'], $o['id'], (int)$o['payout_cents']]); } $pdo->commit(); } catch (Throwable $ex) { $pdo->rollBack(); log_error('escrow_release_failed', ['order_id' => $o['id'], 'err' => $ex->getMessage()]); } }
Powers The hands-off escrow flow: 48 hours after the buyer marks the order received, the cron flips the order to completed, releases the escrow row and creates the driver's earning row. Disputes block release. The leader lock prevents two boxes in an auto-scaling group from double-paying.
5. Disputes and admin
5.1 Open a dispute: freeze the escrow row
csrf_check(); $user = require_login(); $pdo = db(); $pdo->beginTransaction(); try { $pdo->prepare( 'INSERT INTO disputes (order_id, opened_by, reason, status, opened_at) VALUES (?, ?, ?, "open", NOW())' )->execute([$order_id, $user['id'], $reason]); // Freeze the escrow row so the auto-release cron skips it. $pdo->prepare( 'UPDATE escrow_transactions SET status = "frozen", frozen_at = NOW() WHERE order_id = ? AND status = "held"' )->execute([$order_id]); $pdo->commit(); } catch (Throwable $ex) { $pdo->rollBack(); m2y_fail(500, 'Could not open dispute', 'Please try again.'); }
Powers The "open a dispute" button on the order page. The escrow freeze and the dispute insert happen in one transaction so a partial write cannot leave the money flow broken.
5.2 Admin dispute resolution (three-way outcome)
$user = require_role('admin'); csrf_check(); $outcome = $_POST['outcome'] ?? ''; if (! in_array($outcome, ['refund_buyer', 'release_seller', 'partial'], true)) { m2y_fail(400, 'Bad outcome', 'Choose refund, release or partial.'); } $pdo = db(); $pdo->beginTransaction(); try { $pdo->prepare('UPDATE disputes SET status = "closed", resolution = ?, resolved_by = ?, resolved_at = NOW() WHERE id = ?') ->execute([$outcome, $user['id'], $dispute_id]); if ($outcome === 'refund_buyer') { $pdo->prepare('UPDATE escrow_transactions SET status = "refunded", refunded_at = NOW() WHERE order_id = ?') ->execute([$order_id]); $pdo->prepare('UPDATE orders SET status = "refunded" WHERE id = ?') ->execute([$order_id]); } elseif ($outcome === 'release_seller') { $pdo->prepare('UPDATE escrow_transactions SET status = "released", released_at = NOW(), payout_ref = CONCAT("ADMIN-", ?) WHERE order_id = ?') ->execute([$dispute_id, $order_id]); $pdo->prepare('UPDATE orders SET status = "completed", completed_at = NOW() WHERE id = ?') ->execute([$order_id]); } else { // partial: split per admin note // Record both sides in refund_requests + escrow stays frozen until both legs settle. } $pdo->commit(); } catch (Throwable $ex) { $pdo->rollBack(); m2y_fail(500, 'Could not resolve', 'Try again or check the audit log.'); }
Powers The admin disputes screen. Allow-list on outcome blocks an attacker from injecting an unsupported state; transaction wraps the three updates so the system never lands in a half-resolved state.
5.3 Admin driver payout run (CSV export)
require_role('admin'); csrf_check(); $batch_ref = 'PAY-' . date('YW') . '-' . substr(bin2hex(random_bytes(3)), 0, 4); $pending = db()->query( "SELECT de.id, de.courier_id, c.user_id, u.display_name, u.email, de.base_cents + de.per_km_cents + de.surcharge_cents AS total_cents FROM driver_earnings de JOIN couriers c ON c.id = de.courier_id JOIN users u ON u.id = c.user_id WHERE de.status = 'pending' ORDER BY de.courier_id" )->fetchAll(); header('Content-Type: text/csv'); header('Content-Disposition: attachment; filename="' . $batch_ref . '.csv"'); $out = fopen('php://output', 'w'); fputcsv($out, ['driver', 'email', 'amount_cents', 'batch']); $ids = []; foreach ($pending as $row) { fputcsv($out, [$row['display_name'], $row['email'], $row['total_cents'], $batch_ref]); $ids[] = (int)$row['id']; } // Mark all included rows as paid_out, then write a single audit log entry. if ($ids) { $place = implode(',', array_fill(0, count($ids), '?')); db()->prepare("UPDATE driver_earnings SET status='paid_out', payout_batch=?, paid_out_at=NOW() WHERE id IN ($place)") ->execute(array_merge([$batch_ref], $ids)); }
Powers The weekly admin payout run. Generates a downloadable CSV the operator hands to the bank, then locks all included rows to paid_out so the same job cannot be paid twice.
6. Driver
6.1 Job board: on-duty plus radius (Haversine)
$radius_options = [5, 10, 30, 50, 100]; $radius_km = (int)($_GET['radius_km'] ?? 30); if (!in_array($radius_km, $radius_options, true)) { $radius_km = 30; } $jobs_sql = " SELECT da.id, da.pickup_addr, da.dropoff_addr, da.payout_cents, da.distance_km, da.trip_type, o.reference, o.fulfillment_type, pa.lat AS pickup_lat, pa.lng AS pickup_lng, (6371 * acos(LEAST(1.0, GREATEST(-1.0, cos(radians(:dlat)) * cos(radians(pa.lat)) * cos(radians(pa.lng) - radians(:dlng)) + sin(radians(:dlat)) * sin(radians(pa.lat)) )))) AS km_from_driver FROM delivery_assignments da JOIN orders o ON o.id = da.order_id JOIN addresses pa ON pa.id = o.pickup_addr_id WHERE da.courier_id IS NULL AND da.state = 'offered' AND o.status = 'paid' HAVING km_from_driver <= :rkm ORDER BY km_from_driver, da.id DESC LIMIT 50";
Powers The driver's "available jobs" list. Allow-list on radius and an inline Haversine in MySQL ranks parcels by real distance, so a driver in Pretoria does not get offered a Cape Town job.
6.2 Earnings: conditional aggregation per week
$this_week_start = date('Y-m-d', strtotime('monday this week')); $last_week_start = date('Y-m-d', strtotime('monday last week')); $totals = db()->prepare( "SELECT SUM(CASE WHEN de.created_at >= :tw_start THEN de.base_cents + de.per_km_cents + de.surcharge_cents ELSE 0 END) AS this_week_cents, SUM(CASE WHEN de.created_at >= :lw_start AND de.created_at < :lw_end_next THEN de.base_cents + de.per_km_cents + de.surcharge_cents ELSE 0 END) AS last_week_cents, SUM(de.base_cents + de.per_km_cents + de.surcharge_cents) AS all_time_cents, SUM(CASE WHEN de.status = 'pending' THEN de.base_cents + de.per_km_cents + de.surcharge_cents ELSE 0 END) AS pending_cents, SUM(CASE WHEN de.status = 'paid_out' THEN de.base_cents + de.per_km_cents + de.surcharge_cents ELSE 0 END) AS paid_out_cents, COUNT(*) AS jobs_done FROM driver_earnings de WHERE de.courier_id = :cid" ); $totals->execute([ ':tw_start' => $this_week_start, ':lw_start' => $last_week_start, ':lw_end_next' => $this_week_start, ':cid' => $courier_id, ]);
Powers The driver's earnings hero. Six aggregates in one round trip to the database, indexed on (courier_id, created_at), so the page renders fast on the cheapest Android browser.
7. Database extracts
7.1 orders table (CREATE TABLE excerpt)
CREATE TABLE orders ( id BIGINT AUTO_INCREMENT PRIMARY KEY, reference CHAR(12) NOT NULL UNIQUE, buyer_id BIGINT NOT NULL, seller_id BIGINT NOT NULL, subtotal_cents INT NOT NULL DEFAULT 0, delivery_cents INT NOT NULL DEFAULT 0, tip_cents INT NOT NULL DEFAULT 0, protection_cents INT NOT NULL DEFAULT 0, total_cents INT NOT NULL DEFAULT 0, fulfillment_type ENUM('driver_door_to_door','pickup_network','self_collect_inspect','cash_on_delivery') NOT NULL, status ENUM('placed','paid','dispatched','received','completed','cancelled','refunded') NOT NULL DEFAULT 'placed', pf_payment_id VARCHAR(64) NULL, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, completed_at DATETIME NULL, FOREIGN KEY (buyer_id) REFERENCES users(id) ON DELETE RESTRICT, FOREIGN KEY (seller_id) REFERENCES users(id) ON DELETE RESTRICT, INDEX idx_buyer_status (buyer_id, status), INDEX idx_seller_status (seller_id, status) ) ENGINE = InnoDB;
Powers Every sale on the platform. ENUMs guard the workflow; ON DELETE RESTRICT means a money-bearing row can never be orphaned by a user delete; the composite index serves "my orders" and "my sales" pages in one seek.
7.2 Admin KPI: GMV by month
SELECT DATE_FORMAT(o.completed_at, '%Y-%m') AS month, COUNT(*) AS orders_count, SUM(o.total_cents) / 100 AS gmv_rands, SUM(o.delivery_cents) / 100 AS delivery_rands, SUM(o.protection_cents) / 100 AS protection_rands FROM orders o WHERE o.status = 'completed' AND o.completed_at >= DATE_SUB(NOW(), INTERVAL 12 MONTH) GROUP BY month ORDER BY month;
Powers The admin dashboard's GMV chart. Buckets by completed_at (not created_at) so cancellations and refunds are excluded by definition.
8. See it live
Every code path above renders a real page on the running site. Open them live below. Full-page screenshots of each screen are included in the D2 and D3 documents. The driver and admin areas need a signed-in account of that role.