FoodHubbie ERP · Admin

← Back to dashboard

Manager PIN & Discount Approval Ceiling — User Manual

Covers the two gates shipped together:

GateWhat it protectsWhere
Approval ceilingHuman-typed discounts above a set % of the billTable bills (Confirm Payment) and POS walk-in sales (Record Sale)
Void PINEvery payment voidTable card → Void Payment

Both use the same Manager PIN, both fail open by design (see §10).

Worked example used throughout: ceiling = 15%, bill subtotal = ₹1,000.


1. What this feature does

A manual discount is one a staff member types in by hand (an amount or a percentage), as opposed to a coupon, a first-order offer, or a category offer that the system applies on its own.

Before this feature, a staff member could type 900 off a 1,000 bill and nothing asked a second person. Now:

Every approval is written to the audit log with who approved it.


2. What this is NOT

Ceiling / Manager PINMax cap (already existed)
WhereSettings tab → Discount ApprovalDiscount editor → Max cap (₹, optional)
Applies toManual discounts only, at settlementAuto discounts (coupons, offers), per definition
Question it answers"Is a person allowed to give this much away?""How much can this one offer ever give away?"
Unit% of the billflat ₹

They are independent. An auto discount can be capped by Max cap and still never ask for a PIN. A manual discount is never bounded by Max cap.


3. Where the settings live

Admin app → Settings tab → Discount Approval group (directly below Delivery Security).

Discount Approval
├── Approval Ceiling (% of bill)   [ e.g. 15 ]
└── Manager PIN                    [ •••• ]

Then press Save Settings.

Important: settings are per outlet. Each outlet has its own ceiling and its own PIN, stored at:

businesses/{businessId}/outlets/{outletId}/settings/Security/
├── discountCeilingPct   e.g. 15
└── pinHash              e.g. 2bb80d537b1da3e38bd30361aa35775aa0e7f9fb...

Only the hash of the PIN is stored — never the PIN itself. Only admins can read this node (riders cannot).


4. Configuring it — every save outcome

Press Save Settings with the Discount Approval fields filled as follows:

Ceiling fieldPIN fieldResult
154711Saved. Ceiling 15%, PIN set. Gate active.
15(blank)Ceiling saved as 15. Existing PIN kept — a blank field never wipes it.
04711Ceiling saved as 0 → ceiling off. PIN still stored, still required for voids.
(blank)(blank)Ceiling saved as 0 (blank → 0), PIN unchanged. Feature off for discounts.
1504711Clamped to 100. Never stored above 100.
-54711Clamped to 0.
15123Rejected → toast "Manager PIN must be 4 to 12 digits." Nothing is saved (not even the ceiling).
151234567890123Rejected → same toast, too many digits. Nothing saved.
15abcdRejected → same toast, digits only. Nothing saved.
154711 on non-HTTPSRejected → toast "PIN could not be hashed — the app must run over HTTPS." Nothing saved.

Changing the PIN: type a new 4–12 digit number into the field and Save. The placeholder tells you the current state:


5. How the ceiling is calculated

discount % = discount ÷ subtotal × 100
PIN required  ⇔  ceiling > 0  AND  subtotal > 0  AND  discount % > ceiling

With ceiling = 15% and subtotal = ₹1,000:

You applyAmount% of billPIN asked?Why
Manual ₹50₹505%Nobelow ceiling
Manual ₹150₹15015.0%Noexactly at ceiling — the test is strictly >
Manual ₹151₹15115.1%Yesfirst value above ceiling
Manual ₹500₹50050%Yesabove ceiling
Percent 10%₹10010%Nobelow ceiling
Percent 15%₹15015%Noexactly at ceiling
Percent 16%₹16016%Yesabove ceiling
Coupon ₹300 (auto)₹30030%Noauto discounts are never gated
First-order 20% (auto)₹20020%Noauto discounts are never gated
Category 25% (auto)₹25025%Noauto discounts are never gated
Manual ₹900, ceiling 0₹90090%Noceiling disabled
Any discount, subtotal ₹0——Nozero bill, nothing to approve

Different bill size, same ceiling — the ceiling is a percentage, not a flat amount:

SubtotalCeiling 15%PIN needed above
₹20015%₹30.01
₹1,00015%₹150.01
₹10,00015%₹1,500.01

6. Discount gate — every possible outcome

Triggered from two places, both automatic:

6.1 Outcomes before any PIN is shown

#SituationWhat you seeDoes the payment proceed?
1Discount is auto (coupon, first-order, global, category offer)nothingYes — gate skipped entirely, no settings read
2Manual discount, ceiling = 0 (or blank)nothingYes — ceiling off
3Manual discount at or below ceilingnothingYes
4Discount is 0 (nothing applied)nothingYes
5Settings can't be read (offline, permission)browser console warningYes — fail open
6Manual discount above ceilingPIN prompt (see 6.2)depends on 6.2

6.2 Outcomes once the PIN prompt is shown

The prompt reads "Manager Approval", has a password field and two buttons: Cancel and Approve. It also states what you are approving:

This 15.1% discount is above the 15% approval ceiling.

The percentage is rounded to one decimal place, so ₹151 off a ₹1,000 bill shows 15.1 and ₹500 off shows 50.0. The ceiling is the value saved in Settings.

#You doWhat you seeResult
7Type the correct PIN, press Approve (or Enter)prompt closesPayment proceeds. Audit entry discount.pin.approved written with who approved it, the discount, the subtotal and the ceiling.
8Type a wrong PIN, press Approvetoast "Incorrect manager PIN. Try again."; prompt closes and reopensNothing happens. Try again or cancel.
9Type wrong PIN several times, then the right onewrong → toast each time, then proceedsPayment proceeds on the correct attempt.
10Press Cancelprompt closesPayment aborted. Nothing is written. The bill review / cart stays exactly as it was.
11Press Escapeprompt closesSame as Cancel — aborted.
12Click outside the promptprompt closesSame as Cancel — aborted.
13Press Approve with an empty fieldprompt closesSame as Cancel — aborted (an empty field is not a PIN).
14Press Enter with an empty fieldnothing happensPrompt stays open.
15Ceiling is on but no PIN has ever been settoast "Manager PIN is required for this action but none is set — configure it in Settings."Yes — fail open, so a half-configured setup never blocks billing.
16Browser lacks crypto (site opened over plain http:// IP)console warningYes — fail open.

6.3 After you approve

Table bill: the payment-method step follows. If you approve the PIN but then don't select a payment method, nothing is written and you will be asked for the PIN again the next time you click Confirm Payment — approval is not cached.

POS: the order is written immediately. If you cancel the PIN, your cart is preserved — nothing is cleared, you can adjust the discount and try again.


7. Void gate — every possible outcome

Reachable from all four entry points — the table card button, the table drawer button, the delegated action and the window.__tables export — but they all run one function, so the gate applies to every void.

Flow: click Void Payment → confirmation dialog → PIN prompt. The prompt states what you are authorising:

Authorise voiding this table bill. This cannot be undone.

For a split/group bill the same message reads group bill instead of table bill.

#You doResult
1Click Void Payment, then Cancel on the confirmationNothing happens. No PIN asked.
2Confirm, then type the correct PINVoid executes. Orders revert to Served, discount usage is reverted. Audit entry void.pin.approved written with the table and group.
3Confirm, then type a wrong PINtoast "Incorrect manager PIN. Try again.", prompt reopens. Nothing changed.
4Confirm, then Cancel / Esc / click outside / empty ApproveVoid aborted. Payment stays void-free, orders untouched.
5Confirm, but no PIN is settoast "Manager PIN is required for this action but none is set — configure it in Settings." → void proceeds (fail open).
6Confirm, settings unreadableconsole warning → void proceeds (fail open).

The confirmation dialog comes first, so you always know what you are authorising before you are asked for the PIN.


8. Every message you can see

MessageLevelMeaningWhat to do
Manager PIN is required for this action but none is set — configure it in Settings.warning (5s)Ceiling or void gate is on but no PIN existsSet a PIN in Settings → Discount Approval
Incorrect manager PIN. Try again.error (3s)Wrong PIN typedRe-enter, or cancel
Manager PIN must be 4 to 12 digits.errorPIN field had a bad value on saveUse 4–12 digits only
PIN could not be hashed — the app must run over HTTPS.errorBrowser refused crypto on insecure originOpen the app over https://
Processing... (POS button)spinnerSale is running — including the PIN promptCancel the prompt to stop
Browser console: approval settings unreadableconsolesettings/Security couldn't be readCheck connection/rules; action fails open
Browser console: PIN hashing unavailable — failing openconsolecrypto.subtle missingUse HTTPS

9. The audit trail

Every successful approval writes one row to businesses/{businessId}/outlets/{outletId}/logs/audit:

FieldDiscount approvalVoid approval
actiondiscount.pin.approvedvoid.pin.approved
uidwho typed the PINwho typed the PIN
adminEmailtheir emailtheir email
outletoutlet idoutlet id
timestampserver timeserver time
details.discountValuee.g. 151—
details.subtotale.g. 1000—
details.ceilingPcte.g. 15—
details.tableId—e.g. t1
details.groupId—group id, or session

Failed attempts are not logged (only the approval is), so the trail stays readable.


10. Limitations you must know

  1. This is not a security boundary. There are no Cloud Functions on this plan, so the PIN is checked inside the browser. Anyone who opens devtools can bypass it. Treat it as accountability — you will see who approved what — not as protection against someone determined to bypass it.
  2. The stored hash can be cracked. A 4-digit PIN has only 10,000 combinations; the SHA-256 hash stops casual shoulder-surfing of the database, not a determined attacker who can read it. Longer PINs (up to 12 digits) are meaningfully better.
  3. Fail open is deliberate. Unreadable settings, no PIN configured, or no crypto all allow the action with a warning. The reason: a misconfigured ceiling must never stop a restaurant from taking payment mid-service. The flip side — if you never set a PIN, the ceiling does nothing.
  4. Set both fields, or neither. A ceiling with no PIN is a warning on every large discount and no actual control.
  5. Per outlet. Changing the PIN on one outlet does not change it on another.
  6. Not live-tested. Verified by unit checks, build and static inspection; not exercised end-to-end against a live database.


Expenses Module — User Manual

The Expenses module lets restaurant admins log, categorize, track, and report on business expenses (rent, utilities, payroll, supplies, etc.). It lives in the Admin dashboard as a top-level tab with five sub-tabs.

1. Overview & Navigation

Location: Admin dashboard → Expenses tab (💰 icon) in the left sidebar.

Sub-tabs:

Sub-tabPurpose
TodayRapid entry for today's expenses; live category totals
HistoryFilterable history with date range, category, status, search
CategoriesManage expense categories, budgets, alert thresholds
ReportsCharts & tables: monthly, category, outlet, trends; export CSV/PDF
SettingsAuto-approve threshold, receipt threshold, currency, retention, workflow

2. Top Bar Controls

Located at the top-right of the Expenses tab header:

2. Today — Rapid Entry

Fast logging for today's expenses with live totals.

Header: Shows today's date and TOTAL TODAY amount.

Category chips: Horizontal scrollable chips showing each category with its color/icon. Each chip shows the category's total for today. Tap "+ Add new" to create a new category inline.

Form fields:

Submit — logs expense, clears amount for rapid re-entry, focuses amount for next entry

Ceiling guard: If the day's total + new amount exceeds the discount ceiling % of today's projected revenue, a Manager PIN is required before saving.

2. History — Browse & Filter

Filter bar (sticky): Date range (From/To), Category multi-select, Outlet dropdown, Status (All/Approved/Pending), Search box.

Table columns: Date | Category | Description | Amount | Outlet | Receipt (icon) | Status (badge) | Actions (Edit/Delete).

Row actions: Edit (opens pre-filled modal), Delete (soft-delete with confirmation), View Receipt (opens full-screen preview).

Bulk actions: Select multiple rows → Bulk Approve (requires PIN), Bulk Delete, Export Selected.

3. Categories — Master Data

List view: Name, color, icon, monthly budget, alert threshold %.

Add/Edit modal fields:

System categories (seeded, non-deletable): Rent, Utilities, Payroll, Supplies, Marketing, Maintenance, Misc.

Drag-to-reorder for display priority.

4. Reports — Analytics

Four report cards with Chart.js charts and exportable tables:

  1. Monthly Summary — Bar chart (Actual vs Budget) + table: Month | Total | Budget | Variance
  2. Category Breakdown — Doughnut chart + table: Category | Amount | % of Total | Budget | Alert status (Over Budget / On Track)
  3. Outlet Comparison — Horizontal bar chart + table: Outlet | Total | This Month | Last Month
  4. Trend Lines — Line chart of daily spend over selected range

Export: Each report has CSV and PDF buttons (top-right of Reports sub-tab).

4. Settings — Global Config

SettingDefaultDescription
Auto-approve ThresholdRs. 5,000Expenses ≤ this amount auto-approve; above requires Manager PIN
Require Receipt AboveRs. 1,000Receipt upload required for expenses above this amount
Default Currency DisplayRs.Choose Rs. or ₹ symbol
Retention Policy (days)90Days to keep soft-deleted expenses
Approval WorkflowSingle Manager PINSingle or Dual Manager PIN (future)

All settings are saved to businesses/{bid}/outlets/{oid}/settings/Expenses and are per-outlet.

5. Receipt Handling

5. Ceiling Integration

The expense ceiling shares the Discount Approval ceiling from Settings → Discount Approval:

6. Export — CSV & PDF

6. Soft Delete & Audit

7. Keyboard & Accessibility

8. Troubleshooting

SymptomCauseFix
Categories not loading in dropdownNo categories created yetGo to Categories sub-tab → "Seed Default Categories" or add manually
Receipt upload failsFile too large (>2MB) or unsupported typeUse JPG/PNG/PDF under 2MB
PIN prompt appears on small expenseAuto-approve threshold set too lowIncrease Auto-approve Threshold in Settings
Ceiling PIN keeps appearingDay total + new expense exceeds ceiling % of projected revenueCheck ceiling % in Settings → Discount Approval
Categories not showing in ReportsNo expenses logged for those categoriesLog at least one expense per category
PDF export missing brandingPDF lib not loadedRefresh page; check console for errors

11. Quick troubleshooting

SymptomCauseFix
Never get asked for a PIN on a huge discountCeiling is 0, or the discount is a coupon/offer rather than a manual oneSet a ceiling > 0; check the discount is typed in manually
Asked for a PIN on a small discountCeiling is set very low (e.g. 1)Raise the ceiling
"…none is set…" toast every timeCeiling configured, PIN never savedSave a 4–12 digit PIN
PIN rejected even though you typed it rightYou changed it on a different outletSettings are per outlet
Settings won't save at allBad PIN value (too short/long/non-numeric)Fix the PIN field — the whole save aborts on it
Approving the PIN then being asked againPayment method wasn't selected before the PINSelect the payment method, then confirm

🔐 Staff Management Module — User Manual

The Staff Management module lets restaurant admins create, edit, and manage staff accounts with role-based access control.

1. Overview & Navigation

Location: Admin dashboard → Settings tab → Staff Management sub-tab.

Features:

1. Creating a Staff Account

Steps:

  1. Go to Settings → Staff Management sub-tab
  2. Click "Add Staff"
  3. Fill in the form:
  1. Click Create Staff
  2. System creates Firebase Auth user + staff record
  3. Temporary password generated and shown once — share securely
  4. Staff receives welcome email with temporary password

Validation:

2. Managing Staff

List View: Shows all staff with name, role, outlet, status, last signed in

Actions per row:

Role Permissions:

ActionOwnerManagerCashierWaiterKitchenRider
Create Staff✅❌❌❌❌
Edit Staff✅Own outlet❌❌❌
Delete Staff✅Own outlet❌❌❌
Reset Counter PIN✅Own outlet❌❌❌
Change Discount Ceiling✅Own outlet❌❌❌
Reset Password✅Own outlet❌❌❌
View Audit Log✅Own outlet❌❌❌

3. Counter PIN Shift Sign-In

Purpose: Staff sign in for a shift using a 4-digit PIN instead of email/password.

Flow:

  1. Staff clicks "Start Shift" on POS/Table app
  2. Enters 4-digit PIN
  3. System verifies against counterPinIndex (reverse index: hash → UID)
  4. On success: stores counterStaffUid in sessionStorage, updates lastSignedIn
  5. Shift active until "End Shift" clicked or browser closed

Security:

End Shift:


🔐 Security Audit Module — User Manual

The Security Audit module automatically checks your restaurant's security posture and provides one-click fixes.

Location: Admin → Settings → Security Audit sub-tab

1. Automated Checks

CheckSeverityDescription
Staff with no Counter PINCriticalStaff can't sign in for shifts
No Manager PIN configuredCriticalCeiling/void gates non-functional
Staff with ceiling = 0 but role allows discountsHighCashiers/Managers can give unlimited discounts
Inactive staff > 90 daysMediumStale accounts
Multiple owners per outletHighOnly one owner per outlet

2. Automated Fixes

FindingFix ActionWhat it does
Missing Counter PINSet Counter PINGenerates 4-digit PIN, shows modal with PIN
Missing Manager PINSet Manager PINOpens Staff Management, focuses PIN field
Zero ceiling on discount roleSet Outlet CeilingOpens Staff Management, focuses ceiling field
Multiple ownersDemote Extra OwnersShows list → confirm → demotes to Manager

All fixes require Manager PIN confirmation (same PIN as discount/void gates).


📋 Security Audit Manual Verification Checklist

CheckStatus
All staff have Counter PIN☐
Manager PIN configured☐
No zero-ceiling discount roles☐
Only one owner per outlet☐
No stale active staff (>90 days)☐
Audit log accessible☐

📋 Change Password — User Manual

Location: Settings → Security Audit tab → Change Password section

How to change your password:

  1. Go to Settings → Security Audit sub-tab
  2. Scroll to Change Password section
  3. Fill in:
  1. Click Update Password
  2. System will:

Password Requirements:

Security: Requires re-authentication with current password before changing (Firebase requirement).


📋 Forgot Password Flow

Location: Login page → "Forgot Password?" link

  1. Click "Forgot Password?" on login screen
  2. Enter registered email address
  3. Click Send Reset Email
  4. Check email inbox (check spam folder)
  5. Click reset link in email
  6. Set new password (min 8 characters)
  7. Return to login, sign in with new password

Note: Reset link expires in 1 hour. Link can only be used once.


📋 Forgot Counter PIN Flow

  1. Go to POS / Tables app
  2. Click "Forgot PIN?" on Counter PIN prompt
  3. Enter registered email
  4. System sends reset email with temporary PIN
  5. Use temporary PIN to sign in
  6. System prompts to set new PIN immediately

📋 Counter PIN Shift Sign-In — Quick Reference

ActionSteps
Start ShiftClick "Start Shift" → Enter 4-digit PIN → System verifies via counterPinIndex reverse index → Stores counterStaffUid in sessionStorage → Updates lastSignedIn
End ShiftClick "End Shift" → Clears counterStaffUid from sessionStorage → Updates lastSignedIn timestamp
Forgot PINClick "Forgot PIN?" → Enter email → System sends reset email with temporary PIN → Sign in with temp PIN → Prompted to set new PIN

Security Notes:


📋 Forgot Password Flow

  1. Click "Forgot Password?" on login page
  2. Enter registered email address
  3. Click "Send Reset Email"
  4. Check email (check spam folder)
  5. Click reset link in email (valid for 1 hour, single use)
  6. Set new password (min 8 characters)
  7. Return to login, sign in with new password

📋 Change Password (Logged In)

Location: Settings → Security Audit tab → Change Password section

  1. Enter Current Password
  2. Enter New Password (min 8 chars)
  3. Enter Confirm New Password (must match)
  4. Click Update Password
  5. System re-authenticates with current password
  6. Updates password in Firebase Auth
  7. Clears all fields
  8. Shows success toast

Real-time match indicator: Green ✓ when passwords match, red ✗ when they don't.


📋 Email Index & Counter PIN Index — Technical Details

Email Index (emailIndex/{hash})

Counter PIN Index (counterPinIndex/{hash})


📋 Budget Numbers — Rupees (Not Paise)

All budget amounts in the system are stored and displayed in Rupees (₹), not paise.

CategoryMonthly Budget (₹)
Rent50,000
Utilities10,000
Payroll2,00,000
Supplies5,000
Marketing10,000
Maintenance3,000
Misc2,000

Note: Earlier versions stored amounts in paise. All new code uses Rupees. The fmtMoney() formatter handles display.


📋 Quick Reference — Keyboard Shortcuts

ActionShortcut
Submit expense formEnter (in amount field)
Close any modalEscape
Next fieldTab
Previous fieldShift + Tab
Open Forgot PasswordF (on login page)
Open Add ExpenseCtrl/Cmd + E (Expenses tab)
Open SettingsCtrl/Cmd + ,
Close modalEscape

📋 Quick Troubleshooting

SymptomLikely CauseFix
"Manager PIN required but none set"Ceiling > 0 but no PIN savedSet PIN in Settings → Discount Approval
"Incorrect manager PIN"Wrong PIN enteredRe-enter or Cancel
"PIN could not be hashed"Running on HTTP, not HTTPSUse https:// or localhost
Categories not in dropdownNone created yetGo to Categories sub-tab → "Seed Default Categories"
Receipt upload failsFile > 500KB or wrong typeUse JPG/PNG/PDF under 500KB
Ceiling PIN on small expenseAuto-approve threshold too lowIncrease threshold in Settings → Expenses
PDF export missing brandingChart.js not loadedRefresh page, check console
Categories missing in ReportsNo expenses in that categoryLog at least one expense per category
Ceiling PIN keeps appearingDay total + new expense > ceiling% of projected revenueCheck ceiling % in Discount Approval settings

📋 Blaze Plan Features (Future)

Note: These features require Firebase Blaze plan (pay-as-you-go) and Cloud Functions. They are not available on the free Spark plan but are designed for future migration.

FeatureDescriptionFiles to Modify
Custom Claims for RolesServer-side role tokens via Cloud Functionsfunctions/src/auth-triggers.ts, staff-management.js
Server-side PIN VerificationPIN checked in Cloud Function, not browserfunctions/src/pin-verification.ts
Server-side Expense CeilingCeiling enforced in Cloud Functionfunctions/src/expense-ceiling.ts
OTP Rate LimitingRate limit per phone/IP in Cloud Functionfunctions/src/otp-rate-limit.ts
Custom Claims SyncSync role changes to Custom Claimsfunctions/src/auth-triggers.ts

Migration Path: When upgrading to Blaze plan, deploy Cloud Functions, update database rules to use auth.token.claims.role, and remove client-side PIN/ceiling checks.


End of Manual — Last updated: 2026-09-27 | Version 5.3.6