Skip to content

Release: coupons on the ordering page, and a voice line that hears the customer - #890

Merged
sridharkalaibala merged 10 commits into
mainfrom
develop
Sep 18, 2026
Merged

sridharkalaibala merged 10 commits into
mainfrom
develop

Conversation

@sridharkalaibala

Copy link
Copy Markdown
Contributor

Promotes develop to main. Three merged pull requests, all mine.

Merge with a merge commit, never a squash.

#887 - a coupon works on the ordering page

You have shipped coupons since the loyalty release and the channel they exist to drive could not take one: every coupon route sits behind an authenticated user, and a customer at a table has no account.

Two doors. The preview says whether a code is real and what it offers, and never a money figure for a basket. The order decides the value, once, from your own coupon document. An unknown code refuses the order rather than quietly charging full price, a coupon cannot dodge a minimum, and the redemption is recorded only after the order exists.

#888 - a shop with no tables has no table rules

"table restriction and restaurant oriented stuff only when restaurant enabled."

There was one fulfilment default for every shop, so a hardware shop that had never opened the settings page was telling customers it had tables. And the table limit asked whether a table number had arrived, never whether the shop runs table service - so a retail counter whose printed code carried a segment refused customers with "Table 5 already has an open order".

#889 - the line hears the customer, not the room

"live conversations are charged so much. second outside talk is the problem."

Both had one root. The voice page decides who is speaking by distance, and automatic gain control exists to destroy exactly that ratio - it was lifting the next table toward the threshold meant to exclude them. The hands-free gate was fighting its own microphone.

Plus a ninety-second idle hang-up: a realtime line bills for the time it is held, and a phone put face down kept a paid connection open until the tab closed.

What to watch

The frontend deploy carries all three customer-facing halves. The API deploy carries the coupon pricing and the table rule.

The first thing worth checking on a real shop is a coupon: type a code that does not exist and confirm the order is refused rather than charged at full price.

Still red on main

Test build (main) is failing and the fix is #883, open and green. It is a test pinning an old spelling, not a regression - but it means every branch inherits a red build until it merges.

Verified before opening

Each branch green before merge; scripts/check-locally.js passes all seven on develop.

The product has shipped coupons since the loyalty release, and the channel
they exist to drive could not take one: every coupon route sits behind an
authenticated user, and a customer at a table has no account and never
will. A shop could print a code on a flyer and its own QR menu would
refuse it.

TWO DOORS, and the split between them is the design.

The PREVIEW says whether a code is real and what it offers - percent or
amount, the minimum spend, the cap - so somebody can see the terms before
committing. It never returns a money figure for a basket. Working one out
would mean pricing every line a second time away from the order path, and
two places that price a basket are two places that will one day disagree
about what a customer owes.

The ORDER says what it is worth, once, from the shop's own coupon
document, through the same service the till uses. The request carries a
CODE and nothing about money: a page that could name the value of a coupon
could name one nobody published.

AN UNKNOWN CODE REFUSES THE ORDER rather than quietly dropping the
discount. Somebody who typed a code, saw a price and pressed pay must not
be charged more than the number they agreed to.

A COUPON CANNOT DODGE A MINIMUM. The delivery charge and the minimum-order
check are worked out on the food BEFORE any coupon, so a shop's "we do not
send out under 200" still means 200 of food. It cannot make the shop pay
the customer either: the discount applies to the food, never the delivery
fee, and never below zero.

THE REDEMPTION IS RECORDED AFTER THE ORDER EXISTS. A coupon with a usage
limit is a promise to everybody who has not used it yet, and counting one
against an order that then failed to save would spend somebody else's turn
on nothing. `apply` is idempotent per sale, and a failure to record is
logged rather than fatal - the customer's order is worth more than the
shop's note about it.

One shape of refusal on the public door: a code that does not exist and
one that has run out answer the same way, so it cannot be used to read a
shop's coupon list one guess at a time. Behind the same flood limiter as
the placed-order routes, and declared in the anonymous-route allow-list
with its reason.

Nine new strings with Tamil in both copies of the dictionary. No example
code in the placeholder: an invented one is something a customer might
actually type.
Owner: "table restriction and restaurant oriented stuff only when
restaurant enabled. otherwise treat that as normal retail shop."

Two things were doing it wrong, and both worked, which is why neither had
been noticed.

ONE DEFAULT FOR EVERY SHOP. `[dine_in, takeaway]` went out whatever kind
of shop it was, so a hardware shop that had never opened the settings page
was telling customers it had tables. The customer's page filtered it out
afterwards - correctly - which meant the payload itself said something
untrue and only the page's manners hid it. A page that has to correct the
server's answer is a page that can forget to.

A retail shop now defaults to pickup, and a stored list that somehow says
dine_in has it removed for a retail shop whatever the document claims.

THE TABLE LIMIT ASKED THE WRONG QUESTION. It asked whether a table NUMBER
had arrived, never whether the shop runs table service. A printed code can
carry a segment for all sorts of reasons - a venue, a unit, a code reused
from a floor plan somebody abandoned - and the moment one did, a retail
counter refused a customer's order with "Table 5 already has an open
order".

DELIVERY IS IN NEITHER DEFAULT, deliberately. A shop that has not said it
delivers must not be offered to a customer as delivering: that takes an
order nobody can fulfil, and the customer finds out when nothing arrives.
Missing an option is a smaller harm than promising one. A shop that HAS
chosen delivery is believed - the default is silence, not a ban.

The fulfilment words were already right and are untouched: the page maps a
retail "takeaway" to "pickup" and a restaurant "pickup" to "takeaway", so
the two are one thing wearing the word its customer would use.

One existing test pinned the exact text of the guard it was protecting.
Its stated intent - that `openTableLimit > 0` gates the whole block - is
preserved and strengthened, so the assertion now allows further conditions
in the same `if`. That is the third source-regex test this week to fail on
a spelling rather than a behaviour.
Owner: "live conversations are charged so much. second outside talk is the
problem while do live conversation. but press and talk not good ux."

Both complaints have one root, and it is not the design - the design was
already right and was being fought by a microphone setting.

The page decides who is speaking by DISTANCE. The customer is at arm's
length; the next table is three metres away, and sound falls off fast
enough that the near one is many times louder. That ratio is what
NEAR_ENOUGH measures, and it is a good discriminator: people cannot be
told apart by a microphone, but near and far can.

AUTOMATIC GAIN CONTROL EXISTS TO DESTROY EXACTLY THAT RATIO. It quietens
the loud and lifts the quiet until everything arrives at one level, so the
table behind was being amplified up toward the threshold meant to exclude
them. The hands-free gate was working against its own microphone, which is
why press-and-talk felt like the only thing that worked.

It is off now. The cost is real and worth naming: a softly spoken customer
in a loud room arrives quieter at the far end. That is the right trade. A
line that mishears a quiet customer asks them to repeat; a line that hears
the next table answers a question nobody asked, on the shop's money.

Noise suppression and echo cancellation stay - those help and do not
flatten the distance.

AND A LINE NOBODY IS TALKING TO NOW HANGS ITSELF UP. A realtime line bills
for the time it is held, not only for what is said into it, and the only
things that closed one were an order going through or the customer
pressing the button again. A phone put face down on a table kept a paid
connection open until the tab was closed. Nobody ever sees that happen; it
arrives at the end of the month.

Ninety seconds, measured on NEAR speech rather than on any sound - a busy
restaurant must not hold the line open on the room's behalf, which is the
same distinction the gate makes, used for the other purpose. The assistant
speaking counts as activity, so an answer somebody asked for is never cut
off mid-sentence, and a fresh line starts its patience from the moment it
opens.
…ot-the-room

The line hears the customer, not the room
…table-rules

A shop with no tables has no table rules
…ng-page

A coupon works on the ordering page
Owner: "so table order, take away (people who visited, pick up, delivery
these kind of methods are there. so order should have these for
restaurant. for retails shops may have pickup, delivery options."

The model was already right and there was no way to use it. Every shop ran
on one hard-coded default - dine in and takeaway - because nothing in the
settings page ever wrote `fulfilment`. A shop that delivered could not say
so, and a counter that did not was being described as having tables.

Three boxes on the Online Ordering form: eat here, collected at the
counter, delivered to an address. Saved through the same endpoint the rest
of that form uses, into the same field the customer's page already reads.

NOTHING TICKED MEANS THE DEFAULT, not "offers nothing". A shop that has
never been asked is drawn unticked rather than pre-filled: a default shown
as a tick reads as a decision somebody made, and the next person to look
would have no way to tell the two apart. The help text under the boxes
says what nothing ticked means.

The table box is hidden for a shop with the Restaurant module off. Ticking
it there would do nothing - the server strips dine_in from a retail shop's
list whatever the document says - and a control that cannot take effect is
worse than no control. Read tolerantly, because the console caches that
switch as the string 'enable'.

The form only posts the list when the boxes are on screen, so a screen
that never drew them cannot wipe what a shop chose.

Five lang_ keys, translated into all seventeen packs.
The first accessibility pass over the customer pages, and it leaves behind
a ratchet rather than a one-off. Contrast is the thing that regresses
silently: a colour is always chosen by eye, and the eye choosing it is
looking at a good screen in a well-lit room.

ONE VALUE WAS WRONG. `#0f8a3d`, used for "ok" and for the veg marker on
both pages, is 4.45:1 on white against the 4.5:1 that AA asks of body text.
Close enough that nobody would notice by looking, and either side of a line
that is not a judgement call. It is #0e8038 now - 5.04:1, and the same
green to look at.

WHAT WAS ALREADY RIGHT, and I checked rather than assumed:

  - every other text colour on both pages clears AA, in both themes
  - the menu's dark palette is comfortable: nothing under 6.9:1
  - the language attribute is set from the chosen language at load, and the
    toggle RELOADS rather than swapping text in place, so it can never
    disagree with the words on screen. I suspected this one and was wrong
  - focus styles exist, reduced motion is honoured, images carry alt text,
    buttons carry labels, and the sort radios are wrapped in their labels
    rather than orphaned

The test checks its own arithmetic against known WCAG values, because a
contrast test with a wrong formula passes everything.

WHAT IS STILL MISSING, and is not a colour: /order has no dark mode at all
while /menu has one. A menu read across a dinner table in the evening is
the common case, and the ordering page is the one people spend longer on.
That wants somebody watching it render, not a ratio, so it is named here
rather than guessed at.
@sridharkalaibala
sridharkalaibala merged commit 4d1e8e3 into main Sep 18, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant