Customers Get Their Own App, Orders Get Real Tracking, and a Camera That Wouldn’t Cooperate

         By: Muhammad Faisal

This week’s work on IQ-POS was less about one feature and more about a shift in how the whole system thinks about customers — they went from being rows in a retailer’s contact list to having an actual app of their own: a catalog, a cart, checkout, order tracking, ratings, and notifications. Alongside that, a few smaller but stubborn bugs got tracked down. Here’s the full story, in the order it actually happened.

1. Giving Customers Their Own Identity, Not Just a Retailer’s Contact Book

Before this week, “customer” data lived entirely inside the retailer’s world — a shopkeeper could list, create, update, and delete customer records under their own account, the same way they’d manage suppliers. That’s fine for a retailer’s private contact book, but it’s a completely different thing from a customer having their own account and profile they control.

The fix was to split these apart cleanly:

  • The old retailer-managed customer CRUD routes were removed entirely.
  • In their place, two new endpoints were added — one to fetch a customer’s own profile, one to update it — both gated behind a customer login, and both scoped so a customer can only ever see or touch their own data.

This was a short piece of work, but it mattered: it drew a hard line between “a retailer’s private notes about people they’ve sold to” and “a customer’s actual account,” which is the foundation everything else this week was built on top of.

2. Building the Customer-Facing App from Scratch

This was the bulk of the week. Before this, IQ-POS had no real customer-facing shopping experience — just the retailer’s POS side. The goal was to build one: browse products across every shop, add things to a cart, and check out.

A handful of product decisions shaped the whole feature:

  • Carts can span multiple shops. A customer isn’t locked into buying from one retailer at a time — the same cart can hold a drink from one shop and groceries from another, or just from one, depending on what they actually add.
  • Clicking a shop filters the same screen rather than navigating to a separate shop page. It keeps browsing feel continuous instead of bouncing between views.
  • Product variants were dropped app-wide. Rather than half-supporting sizes/colors on the customer side while the rest of the app doesn’t really use them, the decision was to remove variant handling completely, everywhere.
  • Pricing reuses the existing DTO engine. Retailers already had a pricing model supporting tax, discount, and a fixed offer price on inventory items. Rather than inventing a second pricing system for the customer catalog, the same calculation and the same final price shown on a retailer’s DTO-enabled product card is what a customer sees at checkout.
  • All five existing languages carried over — English, Urdu, Spanish, Arabic, and Chinese — so the new module didn’t become the one part of the app that’s English-only.

Because there was no backend yet for any of this, the first version was built entirely against mock data, with a written backend contract describing exactly what the real catalog, cart, and checkout endpoints should look like once they existed. That contract turned out to be worth writing down carefully — when the real endpoints came online later, having an explicit shape to compare against made it obvious where the frontend and backend disagreed (small things, like the frontend expecting id where the backend returned orderId), instead of discovering those mismatches through confusing runtime bugs.

Checkout went through its own small evolution. It started as a mock Stripe flow, with real Stripe Elements planned for a later sprint. That plan changed mid-project: the decision was made that the app will be cash-on-delivery only. Stripe was removed from both the backend and frontend checkout paths, and orders are now created as “confirmed, unpaid,” with stock still deducted immediately and safeguards in place against duplicate submissions.

3. Shops, Publishing, and Letting Retailers Control What’s Visible

A retailer’s internal inventory and what a customer actually sees in the app aren’t the same thing — a product existing in stock doesn’t mean it should show up in the public catalog yet. To handle that, a publishing layer was added on top of inventory:

  • Retailers can now maintain multiple shop profiles (name, address, contact info, an active/inactive flag, and an image) instead of being limited to one storefront.
  • Each product can be published or unpublished per shop, either one at a time from its inventory card, or in bulk — publish-all or unpublish-all for an entire category at once, once it became clear that toggling dozens of products individually wasn’t realistic for a retailer with a large catalog.
  • Unpublishing a product only hides it from the customer catalog; it never touches the underlying inventory record.

This turned into its own dedicated screen (a Shops page) rather than being buried inside the existing profile settings, since a retailer managing several shops needs a proper home for that, not a sub-section.

4. Orders That Actually Track Progress

Early on, an order was really just a payment record. That stopped being good enough once multi-shop carts existed — if a customer orders from two shops in one checkout, “the order” isn’t a single thing moving through one pipeline; it’s really two (or more) independent fulfillments that each move at their own pace.

The fix was to split checkout into per-shop fulfillment records, each with its own status: pending → accepted → on the way → delivered. A customer’s overall order only counts as “accepted” once every shop involved has accepted, and the customer can only confirm final receipt once every shop has delivered. That mirrors how it actually works in real life — a customer isn’t going to consider their order “on the way” if half of it hasn’t even been accepted yet.

On the retailer side, this meant new endpoints for listing orders and updating a fulfillment’s status, with ownership checks making sure a retailer can only see and update fulfillments tied to their own shop. On the customer side, it meant a tracking view distinct from plain order history, and a way to confirm “yes, I received this” once everything’s delivered.

Along the way, stock deduction rules got nailed down clearly, since it’s easy to get this subtly wrong: stock reduces the moment an order is placed (even though COD means it’s technically still unpaid), and separately, a retailer’s own bill reduces stock the moment that bill is completed. Just adding something to a cart, or leaving a bill open and unpaid, never touches stock. Written out as a rule like that, it sounds obvious — but it’s exactly the kind of thing that causes overselling bugs if it’s only implied rather than explicit somewhere.

5. Ratings, and Deciding Who’s Actually Allowed to Leave One

Star ratings for products and shops came together in two passes. The backend groundwork — schema, validation, computing averages and star distributions, letting a customer view or delete their own review — came first, but the part that actually lets someone submit a review was deliberately left disconnected until one real question got answered: who should be allowed to rate something?

Letting anyone rate anything invites fake or drive-by reviews; requiring too much friction makes the feature useless. The rule that got settled on:

  • To rate a product, a customer must have actually ordered that specific product.
  • To rate a shop, a customer must have bought at least 10 total units from that shop across their order history — not necessarily 10 different products, just 10 units total.
  • Only orders that were confirmed, paid, and delivered count toward either of those — a cancelled or still-pending order doesn’t unlock the ability to rate.

Once that policy existed, wiring up the actual submit/edit/delete endpoints, permissions, and moderation tools was straightforward, and the customer-facing UI (star input, review lists, pagination, eligibility messaging when someone hasn’t qualified yet) followed shortly after.

6. Telling Customers When Their Order Is Actually Moving

A tracking screen is only useful if a customer thinks to go check it. The notification system exists to close that gap. It’s built as two parallel channels — a persistent in-app inbox with an unread count and a live-updating stream, and an email sent through Nodemailer with retry logic so a temporary email failure doesn’t also fail the order update itself.

Right now there’s exactly one trigger: when a retailer moves a shop’s fulfillment from “accepted” to “on the way,” the customer gets both an in-app notification and an email saying that shop’s part of the order is on its way. For a multi-shop order, each shop dispatching independently sends its own notification — so a customer with a two-shop order gets told twice, once per shop, rather than one vague “something is happening” message. Re-triggering the same status doesn’t spam a duplicate notification.

That’s intentionally the only trigger for now. Order creation, acceptance, full delivery, and receipt confirmation don’t yet send anything — those are natural next candidates, but weren’t in scope yet. On the frontend, the notification bell and drawer were added to the customer header, reusing the existing session and theme rather than building a separate notification-specific auth or design.

7. The Barcode Scanner That Wouldn’t Scan

Not everything this week was a new feature — some of it was tracking down a bug that was more confusing than it should have been. The barcode scanner would open the camera fine, sit there in a scanning state, and never actually detect a barcode. No error on screen, nothing obviously broken — just silence.

The first pass at a fix targeted an outdated assumption: the scanner depended on the browser’s native BarcodeDetector API, which isn’t available in every browser, and it was failing to even get to the camera step in some cases. That got replaced with the app’s existing barcode library and clearer error messaging.

That fix wasn’t quite enough — the camera opened correctly, but detection still wasn’t happening. The next round of investigation pointed at frame quality: the scanner was likely working with a lower-resolution camera feed than it needed. The follow-up fix switched it to read full-resolution frames with a more thorough decoding pass, and added visible feedback (the detected barcode, or a lookup failure) so it’s no longer a silent black box either way.

The honest state of this one: it’s fixed based on code-level reasoning, but a live camera test — someone actually pointing a real barcode at a real webcam — is still the thing that will confirm it, since this class of bug (camera + decoding timing) is exactly the kind that’s hard to fully verify without physically trying it.

Recap: What Actually Changed This Week

  • Customers are now first-class accounts, not entries in a retailer’s contact list.
  • A full shopping flow exists — multi-shop cart, DTO-aware pricing, and checkout — even though it started on mock data before the real backend caught up.
  • Publishing is now a deliberate step, separate from inventory, so retailers control what’s actually visible to customers.
  • Orders are tracked per shop, not as one opaque blob, which matches how multi-shop carts actually behave in reality.
  • Ratings required a policy decision before an API decision — “who’s allowed to rate this” mattered more than the schema.
  • Notifications started narrow on purpose — one clear trigger, done well, rather than five triggers done vaguely.
  • A silent bug (scanner sees nothing, says nothing) needed better instrumentation before it could be fixed — visible feedback isn’t just a UX nicety, it’s often the fastest path to finding the actual bug.

Leave a Reply

Your email address will not be published. Required fields are marked *