Case studies / Tigg POS · Jul – Sep 2026
A tracked item cannot enter the cart incomplete
Businesses that track batches, expiry or serial numbers need that identity to survive every step. I designed batch and serial tracking across eight surfaces and built the POS frontend, prototyping the flows directly in code.
Role
Timeline
Team
Platform
Not mine
Measured outcome
the key decision
Selection lives inside the line editor, so an item can't be added without its batch or serial.
how deep do you want to go?
Whose unit is this?
A customer buys three strips of paracetamol. Two come from a batch that expires in October, one from a batch that expires next March. Tigg POS used to record that sale as "Paracetamol 500 mg × 3". The invoice could not say which batch left the shop, or whether one of them had already expired.
The same gap showed up with phones. A shop sells two handsets, scans both serial numbers, and the cashier then taps "+" on the cart row to make it three. The cart now says three phones and knows two identities. A month later, when one comes back under warranty, nobody can say which unit it is.
Before this work, the POS treated every product as a quantity. A cart line was product × unit × quantity, and two lines with the same product and unit merged into one. That works for tea and biscuits. It fails for anything where the units are not interchangeable.
The brief asked for batch and serial number tracking across Tigg. The problem underneath it was narrower and harder: identity has to survive every step, from product setup to the sale, the invoice, the return and the report. A settings toggle does not do that on its own.
Brief, role and constraints
The CEO wrote the brief as eight design tasks, one per surface, in July 2026. I was the only UI/UX designer on the team.
| Me | Others (by role) | |
|---|---|---|
| Design of all eight surfaces, POS and accounting | Yes | CEO wrote the brief and set priorities |
| POS web frontend | Yes | Senior frontend engineer reviewed and merged |
| Accounting web frontend | No | Senior frontend engineer |
| Backend: stock, validation, reports | No | Backend engineer |
| Testing | No | QA |
| Launch promo artwork | Yes |
So the claim is specific: I designed all eight surfaces and built the POS frontend. I did not build batch and serial tracking end to end. The accounting screens and the backend belong to other people.
Constraints that became requirements
- Two independent switches. The brief said a business can turn on batch tracking, serial tracking, or both. A product can be flagged for either, but a product flag only counts when the organisation has that switch on.
- VAT-inclusive prices. Many Tigg businesses price with 13% VAT included. A batch carries its own selling price, and that price has to reach the line as the final price, without VAT being added a second time.
- Barcode scanners. A scanner types characters and presses Enter, fast. The field has to accept a stream of scans, reject a duplicate, and keep focus where the next scan goes.
- Warehouses per location. Whether a serial is in stock depends on the location's default warehouse. A location without one cannot check serials at all.
- The server knows the truth. Only the backend knows whether a serial is in stock, already sold, or eligible for return. The POS can check for blanks and duplicates locally; everything else is a question it has to ask.
- Devices. The POS runs in desktop and tablet browsers at the counter.
What I knew, and how
There was no merchant interview or usability test for this feature. I want to say that plainly, because the rest of the case depends on reasoning rather than observed behaviour.
What I did have:
- The brief and the CEO. The CEO explained each task when it was assigned. Across my work, what I know about user problems comes mostly from QA, support and the CEO.
- Zoho, which I used as a reference product at the time.
- Domain modelling. Working out what a batch, a serial number and an order line had to carry, and what the server could answer, was most of the early work. It is technical discovery, and I count it as research.
- Designing in the browser. I designed the POS flows directly in code, prototyping them in the running app against sample data before the real stock endpoints were ready. That let me feel how a scanner stream, a focus change or a held dialog behaved, which a static frame does not show.
A competitor review, done later
While writing this case in 2026, I compared the shipped design with public documentation for other products. This review was not part of the original work.
| Product | Serial or lot at the sale | Required before the sale completes? | Expired stock |
|---|---|---|---|
| Odoo POS | A dialog asks the cashier to type or scan it | No: its documentation says the sale can complete without it | Handled in inventory, not at the counter |
| ERPNext | A serial and batch bundle per transaction | Yes | Batches fetched by a rule such as expiry |
| Zoho Inventory | Batches chosen on the invoice | Not documented | First-expiring batch listed first |
| BUSY | Batches listed with stock | Not documented | Can hide expired batches |
| Square, Loyverse, Shopify POS | No native batch or expiry; Square has no native serials | n/a | n/a |
| Tigg POS (shipped) | The line editor asks for exactly what the product needs | Yes | Warn and confirm |
Tigg ended up between Odoo's permissive "record it if you can" and ERPNext's heavier bundle model. Identity is required at the sale. Expiry is a warning.
Who I designed for
These are assumption-based proto-personas, drawn from the brief and the product's scope, not from interviews. The shop types are illustrative.
- The counter cashier at a pharmacy-type shop, with a queue. Needs the next required field focused, nothing half-finished in the cart, and errors that say how to fix them.
- The electronics shop owner, handling a warranty return of one unit out of three. Needs to return a specific serial and see each unit's status.
- The admin setting up the business. Needs two plain switches that say what they hide.
- The inventory person, looking for stock that expires in the next 30 days, per warehouse.
One capability, eight surfaces
I arranged the eight surfaces along the life of a tracked item: switch the feature on, flag a product, bring stock in, move it through documents, sell it, inspect it, and drill into it. Seven of them are covered here.
Two switches, four modes
The organisation settings got two cards in the style the existing inventory settings already used, each with On and Off. The copy says what Off does, because that is the surprising part:
Batch Number Tracking. Track stock in numbered batches with their own manufacture and expiry dates. Turning this off hides the Batch field on order lines, even for products still flagged as batch-tracked.
The product form then gets two Yes/No questions, "Batch Tracking" and "Serial Number Tracking". They are hidden for service products, and hidden when the organisation switch is off, so an admin never sets a flag that does nothing.
The two levels resolve to one of four modes per product: untracked, batch, serial, or both. The line editor shows only the inputs the mode needs.
Keeping the mode to a single resolved value mattered more than it looks. Every later surface, from the cart line to the refund dialog and the reports, asks the same question, "what does this product need?", and gets the same answer.
The page I deleted after two days
My first version was a standalone selection page. Tapping a tracked product opened a full screen titled "Select Batch & Serial Numbers", with its own quantity stepper, a scan field that turned serials into chips, a list of batch cards sorted by expiry, a "+" to add a batch, and an "Add to Cart" button. I built it against sample data at the end of July 2026 and removed it two days later.
Selection moved into the line editor, the screen that already opens when a cashier edits a line's unit, rate or discount.
I didn't write down my reasons at the time. Reading the prototype against what shipped, these are the ones I can reconstruct:
- Two quantities. The page had its own stepper. The line had a quantity too. Two sources for one number is how a line ends up with three phones and two serials.
- The line editor already owned price. Rate, unit, discount and the VAT-inclusive handling already lived there. A batch's selling price had to land in that same logic, not in a copy of it.
- One place to come back to. If a line was ever incomplete later, I would need to send the cashier somewhere to fix it. That place should be the editor they already know, not a second page.
- Fewer ways to add. "Add to Cart" on a separate page was a second way to put an item in the cart, with its own rules.
What survived were the parts that were about the counter, not the page: scan-first serial chips, the duplicate and limit messages (same wording), batches sorted by expiry, the amber warning under 120 days, and the fields of the new-batch dialog.
Key decision: a tracked item waits outside the cart
The rule I built the rest around is in the title: a tracked item cannot enter the cart incomplete.
When a cashier taps or scans a tracked product, it does not go into the cart. The line editor opens with a pending line. The cashier scans serials or picks a batch there. Save puts it in the cart as its own line. Cancel leaves nothing behind.
The alternative was to add first and nag later: put the item in the cart and mark it as missing something. I chose not to. A half-filled line in a shared cart can be paid for, printed, or forgotten. Holding it outside the cart means the cart only ever contains lines that can be sold.
When are two lines the same line?
The old cart merged any two lines with the same product and unit. I changed what "the same" means. Two lines merge only if they share product, unit and batch, and neither carries serial numbers. A serialised line is always its own line, because each unit is a different thing.
This was the most consequential choice in the feature, and no mock-up would have shown it. It decides what the invoice says, what a return can reverse, and what the report counts.
Asset to add
Screenshot from a test company: POS → Retail order → tap a product flagged for both batch and serial, quantity 3 → line editor with Serial Numbers (2/3), two serial chips, and the Batch select closed on a chosen batch. 2x PNG, desktop width.
At the counter: scan first
Most of the interaction design is in two fields: Serial Numbers and Batch.
The serial field
The label carries a counter, "Serial Numbers (0/3)", in red until it reaches the quantity, then green. The field is focused when the editor opens, and the placeholder says how it works: "Scan or type, then press Enter". Enter submits, because a scanner ends every scan with Enter. After each accepted serial, the input clears and focus comes straight back, so the next scan lands in the right place.
Each serial becomes a removable chip set in a monospace font. Looking back, that was a legibility choice: serials mix letters and digits, and a fixed-width face makes 0 and O, or 1 and l, easier to tell apart when a cashier is checking a chip against a box.
The messages say what happened and what to do:
| Situation | What the cashier sees |
|---|---|
| Same serial scanned twice | "That serial is already added to this line." (checked locally, no request) |
| Server says no | The server's reason, or "That serial is not valid." |
| Network fails | "Could not validate the serial number — please try again." |
| Count reached | "Quantity limit reached — increase quantity to add more." The input is disabled. |
| Quantity lowered after scanning | How many extra serials there are, and to remove them or raise the quantity |
| Location has no default warehouse | The field is disabled: "This location has no default warehouse set. Contact an admin to configure one." |
"Not valid" and "could not check" are deliberately different. A dropped connection is not evidence that a serial is wrong, and it should not read like one.
Asset to add
Screenshot from a test company: POS line editor for a serial-tracked phone, quantity 3, two serials already added as chips, third serial just scanned showing 'Checking...' on the Add button. 2x PNG.
When a product needs both
For a product tracked by both, the serial field comes first. When the last serial is accepted, focus moves to the Batch select. My reasoning, in retrospect: scanning units is the repeated action and the batch is chosen once, so the repeated action should need no extra tap. The batch could have come first; this order keeps the cashier's hands on the scanner longest.
Warn, don't block: expiry
The Batch select opens on focus and lists batches earliest expiry first. Each option shows the batch number, "Expires" with the date, and the balance on the right. Under 120 days the date turns amber. Past its date the option reads "Expired" in red. The colour is never the only signal; the word is always there.
A new batch can be added from the top of the menu, with Batch No., Expiry Date and Selling Price required, and Manufacture Date and Purchase Price optional. Once saved, it is selected and its price applied.
Choosing an expired batch
Picking an expired batch does not apply it. The POS holds the choice and asks:
Batch Already Expired. This batch has already expired. Do you still want to continue?
Continue applies it. Cancel drops the held choice and the field goes back to what it showed before, so the screen never displays a batch the line doesn't have.
Expired batches are warned about, not blocked. That is how it shipped, and it is a policy rather than a law of nature. The reasons I would give for it now:
- a shop may have a legitimate reason to sell or clear stock at or near its date;
- the date on record may be wrong, and the person at the counter is the one holding the box;
- blocking the counter while a queue waits is expensive, and the cashier has no way to fix the data there.
Other products choose differently: BUSY can hide expired batches, and Odoo and ERPNext pick by expiry in inventory. A setting per organisation (warn, hide, or require a manager) with a note when someone overrides the warning is the obvious next step.
Asset to add
Screenshot from a test company: POS line editor for a batch-tracked product → open the Batch select → choose a batch whose expiry date has passed → the 'Batch Already Expired' confirmation dialog on top. 2x PNG.
Repair, then return by serial
When the quantity changes later
The cart row still has a quantity stepper outside the editor. A cashier can raise a phone line from two to three after scanning two serials. The first version of the guard stopped the save with a toast. That told the cashier something was wrong but not where.
The shipped version routes. When Save or Pay is pressed, the POS finds the first line that breaks the rule, shows "Fill in the required batch/serial number details before saving the order.", and opens that line in the editor with focus in the serial field. The same guard runs on the buttons of every order type.
At payment, every serial is checked with the server once more before the invoice is created, in case stock changed while the order was open.
Returns reverse units, not numbers
A return has to say which units came back. The refund dialog lists each invoice line with what can still be returned: the quantity sold minus what was returned before. Lines with nothing left are hidden. For a serialised line, the cashier ticks the specific serials; each one is checked with the server as it is ticked. The counter stays red until the ticked serials equal the return quantity, a remark is required, and only the ticked serials are sent back. A returned item is not for sale again unless it is restocked.
Sell three phones, then return one by serial
Step 1 / 7
The phone opens the line editor, not the cart
Phone X2 128 GB is serial-tracked, so tapping it opens a pending line. The cashier sets the quantity to 3. Nothing is in the cart yet.
- Tracking
- Serial
- Quantity
- 3
- Serial Numbers
- 0/3
- In cart
- No
Arrow keys work too
All steps as a table
| # | State | What happens |
|---|---|---|
| 1 | Pending line | The phone opens the line editor, not the cart. Phone X2 128 GB is serial-tracked, so tapping it opens a pending line. The cashier sets the quantity to 3. Nothing is in the cart yet. |
| 2 | Incomplete | Two serials scanned, save refused. Two scans become chips. The cashier presses Save too early; the line stays in the editor and says what is missing. |
| 3 | Saved | The third scan completes the line. The counter turns green and Save puts the phones in the cart as their own line, with the serials shown under the product name. |
| 4 | Invoiced | Serials re-checked, invoice issued. At payment the POS asks the server about all three serials again, then creates the invoice. The invoice lists the serials under the product. |
| 5 | Refund open | The customer brings one phone back. The refund dialog shows what can still be returned. The cashier ticks the line, sets quantity 1, and ticks the serial on the box. |
| 6 | Returned 1 | Only the ticked serial goes back. With a remark filled in, Proceed to Payment completes the refund. Serial 88121 is returned; it is not for sale again unless it is restocked. |
| 7 | Refund open | A week later, only two can come back. The dialog caps the quantity at 2. Ticking 88121 again is rejected by the server check, shown beside that serial. 88122 is accepted. |
Asset to add
Screenshot from a test company: POS → Invoices → open an invoice with a serialised line of quantity 3 → Initiate Refund → tick the line, quantity 1, one serial ticked showing (1/1 selected), Remarks filled. 2x PNG.
What shipped, and what I don't know
- surfaces designed
- 8surfaces designed
- tracking modes from two switches
- 4tracking modes from two switches
- states designed for the serial field
- 13states designed for the serial field
- measured outcome
- Nonemeasured outcome
The POS side was released in a tagged release in September 2026. Along with the sale and return flow, it included:
- one Batch / Serial Number page in Inventory, with a tab for each (two separate pages were merged into one in late August), and a detail drawer per batch or serial with its dates, prices, status and recent transactions;
- serial statuses Available, Not Available and Oversold;
- batch and serial numbers under the product name on invoices, credit notes and sales orders;
- a Product Batch Report that answers "what expires in the next N days?", grouped by product or warehouse, and a Product Serial No Report filtered by status;
- report permissions that disappear when the organisation switch is off.
Asset to add
Screenshot from a test company: POS → Reports → Inventory Report → Product Batch Report, filter Expires In: Less Than 30 days, grouped by Warehouse, one group expanded. 2x PNG, desktop width.
Because I designed these flows in the browser and built the POS frontend myself, the POS behaviour on this page is the behaviour in the release. The accounting screens were built by the senior frontend engineer from my designs; I haven't reviewed them closely enough to say how closely they match.
Feedback
Good feedback.Reported to me by the CEO
That is the whole of the feedback I can cite. It is informal, and I don't know which businesses it came from.
What is not measured
Measured outcome: none. There are no adoption numbers, no count of businesses with tracking switched on, no checkout-time data, no error rates and no usability test. If I could instrument it, I would look at:
- how often a tracked line is sent back by the repair route, per order type;
- time from tapping a tracked product to saving its line;
- serial check failures by reason: duplicate, refused, network;
- how often cashiers continue past the expired-batch warning versus cancel.
I also designed the launch promo artwork, on the Figma page "Batch and Serial No".
Asset to add
Figma export: file with the page 'Batch and Serial No' → the launch promo posts (1080 × 1080), exported as 1x PNG, two or three side by side.
What I learned
Line identity is a domain decision, not a code detail. Deciding when two cart lines are "the same" shaped the invoice, the return and the report. It was invisible in every mock-up and it was the choice that mattered most.
Don't let incomplete things into shared state. Holding a tracked item outside the cart until it was complete removed a whole class of half-filled lines, instead of adding warnings to catch them.
Put enforcement where the fix is. A toast that says "something is missing" hands the problem back to the cashier. Opening the exact line, with focus in the right field, solves it. The same thinking applies to the expiry warning: warn at the moment of choice, where the person holding the box can decide.
Every state of the serial field
The serial field is small, but it is where the cashier spends most of the time on a tracked sale. I designed thirteen states for it: a happy path of four, four rejections, and five imposed from outside the field.
| State | Trigger | What the field does |
|---|---|---|
| Empty | Quantity N, nothing scanned | Red counter (0/N), focused, placeholder explains Enter |
| Checking | Enter or Add | Input and button disabled; button reads "Checking..." |
| Added | Server accepts | Chip added, input cleared, focus returned |
| Complete | Count equals quantity | Green counter; for batch-and-serial products, focus moves to Batch |
| Duplicate | Serial already on this line | Refused locally, no request |
| Rejected | Server refuses | The server's reason shown under the field |
| Transport failure | Network error | "please try again"; nothing added |
| Bad request | A fault in the request itself | Not shown to the cashier; treated as a bug |
| Limit reached | Count at quantity | Input disabled, with a hint to raise the quantity |
| Warehouse loading | Location's warehouse still loading | "try again in a moment" |
| Blocked | Location has no default warehouse | Field disabled, message names who can fix it |
| Incomplete after save | Save pressed early | Field marked, "Add m more serial number(s) to continue." |
| Overfilled | Quantity lowered after scanning | Says how many extra, and the two ways out |
Two small behaviours carried a lot of weight. Focus is handed back after the field re-renders, not during, because otherwise the next scan landed nowhere. And a quantity with a decimal (2.5 of a serialised item) asks for the whole number of serials below it, so the rule never asks for half a serial.
Where each rule is enforced
The serial count is checked four times, on purpose, because the quantity can change in more than one place.
| Layer | What is checked | What the cashier sees |
|---|---|---|
| Entry | Blank, duplicate, limit, warehouse, server check per serial | Message under the field; nothing added |
| Line save | Serial count equals quantity; batch chosen if needed | Fields marked; stays in the editor |
| Order save or pay | First line in the cart that breaks the rule | Toast, then that line opens in the editor |
| Payment | Count again, and every serial re-checked with the server | Server's message; stays on payment |
| Refund | Refundable quantity; ticked serials equal quantity; each serial checked as a return; remark present | Per-serial errors; Proceed disabled until complete |
Edge cases I designed for
- The same product from two batches becomes two lines; the same batch twice merges.
- A serialised product added twice never merges.
- Quantity raised from the cart row: routed back to the line on save or pay.
- Quantity lowered after scanning: the field says how many extras to remove.
- A duplicate scan is refused without a request.
- The network drops mid-scan: the serial is not accepted, and the message says to try again, not that the serial is wrong.
- The organisation switches tracking off: the fields disappear, even for products still flagged.
- A location without a default warehouse: serial entry blocked, with a message for the admin.
- A selected batch that isn't on the current page of search results still shows correctly.
- Expired batch chosen, then cancelled: the previous choice stays.
- A batch price under VAT-inclusive pricing is the final price, not taxed again.
- A user without permission to change rates keeps the line's rate; the batch price is not applied.
- A second partial return only offers what is left.
- A serial that was already returned, or wasn't in this sale, is refused by the server check.
- Inactive products still appear in historical reports.
Building the POS frontend
I built the POS side over about seven weeks, from late July to the release in September 2026. The order below is the order the behaviour settled in.
- Jul 2026Brief in eight tasksThe CEO wrote the design tasks, one per surface.
- Late JulStandalone page, then the line editorThe selection page was built on sample data and removed two days later; selection moved into the line editor.
- Mid AugReal stock data and server checksSample data replaced by real batches; each serial checked with the server as it is entered.
- AugScanner path and focusScanning a tracked product now opens the pending editor too; focus handed back reliably between scans.
- Late AugOne management page, reports, split linesBatch and serial pages merged into one tabbed page; batch and serial reports; two batches of one product became two lines.
- Late AugReturns by serialRefundable quantity accounts for earlier returns; each ticked serial is checked.
- Sep 2026Route to the lineThe save guard stopped only toasting and started opening the line that needs fixing.
- Sep 2026Tagged releaseReleased in a tagged release in September 2026, with small fixes a week later: finding products by code when adding a batch, and lists refreshing after a new batch.
Tradeoffs
- One server check per serial. Simple to explain and precise in its errors ("this serial" rather than "one of these"). The cost is one request per serial at payment, which run in parallel.
- Required identity. I chose data quality over a slightly faster sale. A cashier can't finish without the serials. For a shop that will handle the warranty return later, that is the point.
- A full editor, not a pop-up. The line editor has more context than a dialog, and it is the same place the repair route opens. It is one more screen change than a small pop-up would be.
- Warn, not block, on expiry. Flexible, but it relies on judgement, and today there is no record of who continued past the warning.
Keeping the release clean
The feature was developed alongside other unreleased work. To ship only this feature, I rebuilt the release from the production line of work and carried the tracking changes across, so nothing unfinished from elsewhere reached customers. It took more than one attempt. I think of this as design work for my teammates: QA and the reviewer were testing exactly what would ship.
There are no automated tests specific to this feature. QA tested it in the test environment before release.
Validation plan and notes on sources
What I would test with people
A moderated test with three to five cashiers, thinking aloud, on five tasks:
- sell three serialised phones by scanning;
- sell from an expired batch;
- change a quantity from the cart, then pay;
- return one unit of three;
- find the batches that expire in the next 30 days at one warehouse.
I would watch for where the cashier looks after the repair route opens the editor, and whether "Expired" is read before or after the dialog appears.
What I would change next
- A per-organisation expiry policy (warn, hide, or require a manager), with a note on override.
- An optional first-expiring batch pre-selected, with manual override.
- Hiding already-returned serials in the refund picker, instead of relying on the server to refuse them.
- Bikram Sambat display for expiry dates, if customers ask for it; dates are shown in AD today.
- Bringing POS and accounting copy and behaviour into one written spec.
Sources
- Product behaviour, states and microcopy: the released POS, which I built.
- Role and scope: the CEO's brief and my own account.
- Reasons marked as retrospective (the deleted page, serial before batch, monospace chips, warn not block) are my reading after the fact; I didn't record them at the time.
- Competitor comparison: public documentation for Odoo, ERPNext, Zoho, BUSY, Square, Loyverse and Shopify, read in September 2026, after the work.
- Every figure uses fictional products, serials and prices. Diagrams and wireframes are reconstructions; screenshots will come from a test company.