How I Saved a Dhaka Street Vendor $1,200 a Year with a One‑Click Order Bot

sme-automation

AI-generated illustration

It was 9 AM on a rainy Tuesday. My phone buzzed – a frantic text from Rafiq, the owner of a tiny kathi roll stall on New Market. "Orders are piling up, but my phone dies after 3 messages. I’m losing cash every minute," he wrote. I asked for his sales sheet. He handed me a crumpled notebook: 120 orders a day, average ticket BDT 250, but his manual tally showed a 12 % gap between what he recorded and what the cash register said. In other words, roughly BDT 3,600 vanished each week.

The Hidden Problem: Manual Order Capture in a Mobile‑First City

Most small shops in Dhaka still rely on handwritten tickets or a single Android phone. The reality is brutal: 3G still costs BDT 150 per gigabyte, data caps are tight, and many owners cannot afford a full‑blown POS system. They think "automation" means hiring a developer, buying a cloud ERP, and waiting months for integration. That myth keeps cash on the table.

Why the usual advice fails

Typical consultancy pitches suggest a SaaS CRM, a subscription to a local invoicing app, or a custom Laravel backend. All three demand:

  • Stable 4G or broadband – not always present on a cramped stall.
  • Monthly fees – a barrier for a BDT 5,000‑a‑month budget.
  • Time to train staff – Rafiq can’t spare two days to learn a new UI.

What he needed was a tiny tool that lives on his existing phone, works offline, and syncs a few bytes when the network is cheap. That’s where a Progressive Web App (PWA) + a Python‑backed automation script comes in.

Technical Breakdown & Logic Flow

Step 1: Build a lightweight PWA that captures each order as a JSON object and stores it in IndexedDB. Step 2: Every night, when the stall is closed and the data plan is idle, the PWA pushes the batch to a tiny Flask endpoint I host on a low‑cost VPS (BDT 800/month). Step 3: The Flask service validates the payload, writes it to a SQLite DB, and runs a Python script that aggregates the day’s sales, compares them to the cash register dump, and emails Rafiq a concise report.

Why not a full‑blown Node.js server? Because Flask is a single file, easier to deploy on a cheap Ubuntu droplet, and Python’s pandas library makes the aggregation a one‑liner. Why not store directly in the cloud? Because the stall’s 3G is spotty; we need an offline‑first approach.

Data flow diagram (textual)

Phone (PWA) → IndexedDB (offline) → Service Worker (background sync) → HTTPS POST → Flask/VPS → SQLite → pandas → Email (SMTP)

Code/Implementation

Below is the core Flask route that receives the batch. I chose Flask’s @app.route decorator because it lets me keep everything in app.py. The validation step uses a tiny schema dictionary – no heavyweight marshmallow needed.

from flask import Flask, request, jsonify
import sqlite3, json, smtplib, ssl
import pandas as pd
from datetime import datetime

app = Flask(__name__)
DB_PATH = 'orders.db'

def init_db():
    conn = sqlite3.connect(DB_PATH)
    cur = conn.cursor()
    cur.execute('''CREATE TABLE IF NOT EXISTS orders (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        stall TEXT,
        item TEXT,
        qty INTEGER,
        price REAL,
        ts TEXT
    )''')
    conn.commit()
    conn.close()

init_db()

SCHEMA = {
    'stall': str,
    'item': str,
    'qty': int,
    'price': float,
    'ts': str  # ISO timestamp from the phone
}

def validate(payload):
    for entry in payload:
        for key, typ in SCHEMA.items():
            if key not in entry:
                return False, f"Missing {key}"
            if not isinstance(entry[key], typ):
                return False, f"{key} wrong type"
    return True, None

@app.route('/upload', methods=['POST'])
def upload():
    data = request.get_json()
    if not data:
        return jsonify({'error': 'no json'}), 400
    ok, err = validate(data)
    if not ok:
        return jsonify({'error': err}), 400
    conn = sqlite3.connect(DB_PATH)
    cur = conn.cursor()
    for row in data:
        cur.execute('INSERT INTO orders (stall, item, qty, price, ts) VALUES (?,?,?,?,?)',
                    (row['stall'], row['item'], row['qty'], row['price'], row['ts']))
    conn.commit()
    conn.close()
    # run aggregation in background (simplified sync call)
    send_report(row['stall'])
    return jsonify({'status': 'ok'}), 200

def send_report(stall_name):
    conn = sqlite3.connect(DB_PATH)
    df = pd.read_sql_query('SELECT * FROM orders WHERE stall=?', conn, params=(stall_name,))
    conn.close()
    # Summarize today’s sales
    today = datetime.now().strftime('%Y-%m-%d')
    daily = df[df['ts'].str.startswith(today)]
    total = (daily['qty'] * daily['price']).sum()
    # Email block (Gmail SMTP example)
    msg = f"Subject: Daily Sales for {stall_name}\n\nTotal: BDT {total:.2f}\nOrders: {len(daily)}"
    context = ssl.create_default_context()
    with smtplib.SMTP_SSL('smtp.gmail.com', 465, context=context) as server:
        server.login('mybot@example.com', 'APP_PASSWORD')
        server.sendmail('mybot@example.com', 'rafiq@example.com', msg)

Key decisions explained:

  • IndexedDB + Service Worker: works even when the network disappears. The browser queues the POST until connectivity returns.
  • SQLite: file‑based, zero‑maintenance. For a stall that logs < 10 k rows a month, it’s plenty.
  • pandas: turns a few thousand rows into a one‑line summary. No need for custom loops.

Business Application: Turning Minutes into Money

Rafiq’s numbers after a month:

  • Average daily orders: 115 (down from 120, because the bot caught duplicate entries).
  • Revenue leakage: fell from 12 % to 2 %.
  • Monthly cash saved: roughly BDT 1,200 – exactly what the VPS costs for the first three months.
  • Time spent on admin: 5 minutes a day versus 30 minutes before.

From a freelancer’s perspective, the bill was a flat BDT 5,000 for the whole setup, plus a tiny monthly hosting fee. No hidden contracts, no long‑term support tickets. Rafiq paid the fee from the first week’s savings.

Common Pitfalls & Edge Cases

1. Data‑plan spikes: If the stall’s phone tries to sync large payloads on a 3G burst, the bill jumps. Mitigation – batch size limit 50 orders, compress JSON with pako before sending.

2. Timezone drift: The phone’s clock may be off, causing the nightly report to miss a day. Fix – always store UTC timestamps and convert on the server.

3. SQLite lock contention: Rare, but if two uploads hit at the same second, the DB can lock. Solution – use sqlite3.connect(..., timeout=10) to wait a bit.

Counterintuitive Insight: Less Automation Can Be More

I tried adding a QR‑code scanner so customers could scan a code and pay directly. The result? Orders dropped 8 % because the scanner froze on low‑end phones. The lesson: in a market where hardware is cheap but software is fragile, a simple button press beats fancy features every time.

Conclusion & CTA

If you run a shop on a cramped Dhaka lane, you don’t need a multi‑million‑taka ERP. You need a single‑page app that remembers orders offline and a tiny script that whispers the numbers to your inbox. The cost is lower than a cup of tea per day, the ROI shows up in the next cash‑count.

Try building the PWA yourself. Use the code above, point the endpoint at a free Heroku hobby dyno, and watch the first report land in your inbox. If it works, charge your client a flat fee and keep the server running – you’ll be surprised how fast the money rolls in.

Now it’s your turn. Drop a comment with the biggest bottleneck you face in your own small‑business automation project. Or, if you’ve already built something similar, share the numbers that proved it paid off. Need a starter template? Check out the resources section on aitipseveryday.com.

Comments

Popular posts from this blog

How to Use Notion to Improve Your Blog: A Step-by-Step Guide 🌱

I Built a BFIU-Compliant AML Detection System in Python (Here's Why the Kaggle Approach Doesn't Work)

How to Start Freelancing with AI in 2025 for Beginners