A full-featured billing and customer loyalty system built for Pareez Unisex Professional Salon. Supports multi-branch operations, a tiered cashback/wallet system with per-branch configuration, PDF receipts, WhatsApp sharing, and QR-code bill links.
Creating a bill — live receipt preview, branch selection, and customer search:
| Wallet & rewards at checkout | Shareable receipt page (QR / WhatsApp link) |
|---|---|
![]() |
![]() |
| Customer management with tiers & wallets | Bill history with day-wise totals |
|---|---|
![]() |
![]() |
Bill preview dialog — download PDF, share to WhatsApp, or show the QR code:
- Screenshots
- Architecture Overview
- Tech Stack
- Project Structure
- Getting Started
- Features
- Database Schema
- Rate Resolution Flow
- Adding a New Branch
- Customizing Cashback Rates
- Deployment
- Scripts Reference
+-------------------------------------------------------+
| Next.js App |
| (App Router, React 19) |
+-------------------------------------------------------+
| Pages | Components |
| / (main app) | BillForm, BillHistory |
| /bill/[id] (public) | CustomerList, CustomerForm |
| | BranchSelector |
| | WalletDisplay, TierBadge |
| | LoginScreen, ProtectedRoute |
+-------------------------------------------------------+
| Library Layer |
| |
| firestore.ts branch-config.ts wallet.ts |
| branches.ts cache.ts auth.ts |
| pdf-generator.ts whatsapp.ts types.ts |
+-------------------------------------------------------+
| Firebase Services |
| |
| +------------------+ +---------------------------+ |
| | Firestore | | Firebase Auth | |
| | (database) | | (email/password + | |
| | | | custom claims) | |
| +------------------+ +---------------------------+ |
+-------------------------------------------------------+
User selects branch + customer + payment method
|
v
resolveAllRates(branchId, tier, paymentMethod)
|
v
+--------------------------------------+
| Firestore: branches/{id}/config/ |
| cashbackConfig |
+--------------------------------------+
|
+-----------+-----------+
| |
payment method payment method
NOT eligible IS eligible
| |
v v
rates = 0 dayConfig[today][tier]
hide wallet UI -> cashbackRate
-> maxRedemptionRate
|
v
calculateCashback()
calculateMaxRedemption()
|
v
processBillWithWallet()
(Firestore transaction)
|
v
Bill saved + Wallet updated atomically
Client Request
|
v
In-Memory Cache (SimpleCache)
|-- hit --> return cached data
|-- miss --> Firestore read --> cache result (TTL-based)
Cache Keys and TTLs:
customers 5 min
branches 5 min
bills_{branchId} 2 min
branch_config_{id} 5 min
| Layer | Technology |
|---|---|
| Framework | Next.js 16 (App Router) |
| UI | React 19 + Tailwind CSS 4 |
| Components | shadcn/ui (Radix UI primitives) |
| Icons | Lucide React |
| Database | Cloud Firestore |
| Auth | Firebase Authentication |
| jsPDF + html2canvas | |
| QR Codes | qrcode |
| Dates | date-fns |
| Language | TypeScript 5 |
pareez-billing/
|
+-- src/
| +-- app/
| | +-- layout.tsx Root layout with metadata
| | +-- page.tsx Main app (tabs: billing, customers, history)
| | +-- bill/[billId]/page.tsx Public bill preview page
| | +-- globals.css
| |
| +-- components/
| | +-- ui/ shadcn/ui primitives (15+ components)
| | +-- BillForm.tsx Main billing form
| | +-- BillHistory.tsx Bill list + preview
| | +-- BillPreview.tsx Bill detail modal
| | +-- BillQRCode.tsx QR code generation
| | +-- BranchSelector.tsx Branch picker (admin/user aware)
| | +-- CustomerList.tsx Customer table + wallet management
| | +-- CustomerForm.tsx Add/edit customer dialog
| | +-- CustomerSearch.tsx Search-as-you-type widget
| | +-- WalletDisplay.tsx Wallet balance + tier progress
| | +-- WalletAdjustmentDialog.tsx Admin manual wallet adjust
| | +-- WalletTransactionHistory.tsx
| | +-- TierBadge.tsx Membership tier badge
| | +-- LoginScreen.tsx Auth UI
| | +-- ProtectedRoute.tsx Auth guard wrapper
| | +-- NewBillFAB.tsx Floating action button
| | +-- Logo.tsx Header logo
| | +-- BillLogo.tsx Bill receipt logo
| |
| +-- contexts/
| | +-- AuthContext.tsx Auth state provider
| |
| +-- lib/
| +-- types.ts All TypeScript interfaces and types
| +-- firebase.ts Firebase app initialization
| +-- firestore.ts Firestore CRUD + wallet transactions
| +-- auth.ts Auth helpers (login, claims)
| +-- branch-config.ts Branch cashback config (core module)
| +-- branches.ts Branch CRUD operations
| +-- wallet.ts Tier logic + cashback calculations
| +-- cache.ts In-memory TTL cache
| +-- pdf-generator.ts jsPDF bill generation
| +-- whatsapp.ts WhatsApp sharing helper
| +-- currency.ts INR formatting (Indian locale)
| +-- phone-mask.ts Privacy masking (987***10)
| +-- validation.ts Input validation
| +-- utils.ts Misc utilities
|
+-- scripts/
| +-- initialize-branches.js Create branches in Firestore
| +-- set-user-claims.js Assign user roles and branch access
| +-- start-firebase-emulator.sh Launch local Firestore emulator
|
+-- public/
| +-- logo.jpg Salon logo
|
+-- firestore.rules Firestore security rules
+-- firebase.json Firebase project config
+-- next.config.ts Next.js config (React Compiler enabled)
+-- package.json
+-- tsconfig.json
- Node.js >= 18
- npm >= 9
- A Firebase project with Firestore and Authentication enabled
git clone <repo-url>
cd pareez-billing
npm installCreate a .env.local file in the project root:
NEXT_PUBLIC_FIREBASE_API_KEY=your-api-key
NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN=your-project.firebaseapp.com
NEXT_PUBLIC_FIREBASE_PROJECT_ID=your-project-id
NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET=your-project.appspot.com
NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID=123456789
NEXT_PUBLIC_FIREBASE_APP_ID=1:123456789:web:abcdefOptional (for local development with emulator):
NEXT_PUBLIC_FIRESTORE_EMULATOR_HOST=localhost:8080-
Enable Firestore in the Firebase Console (native mode).
-
Enable Authentication with Email/Password sign-in method.
-
Deploy security rules:
firebase deploy --only firestore:rules
-
Download your service account key from Firebase Console > Project Settings > Service Accounts > Generate New Private Key. Save it as
scripts/serviceAccountKey.json. -
Create branches: Edit the branches array in
scripts/initialize-branches.jswith your branch details, then:node scripts/initialize-branches.js
Note down the branch IDs printed to the console.
-
Create users and assign roles: First create users in Firebase Console > Authentication > Add User, then configure their emails and branch IDs in
.env(see.env.example):# .env [email protected] [email protected]:branchDocId1,[email protected]:branchDocId2
node --env-file=.env scripts/set-user-claims.js
# Option A: Against live Firebase
npm run dev
# Option B: With Firestore emulator (recommended for development)
npm run start:firebase # Terminal 1 (starts emulator on port 8080)
npm run dev # Terminal 2- Create bills with multiple service line items (name, price, per-service discount, optional staff)
- Auto-generated bill numbers in format
PRZ-YYYYMMDD-001 - Additional bill-level discount on top of per-service discounts
- Payment method selection: Cash, Card, UPI
- Wallet redemption: deduct from customer's cashback balance
- Cashback earning: credited to customer wallet after bill creation
- Edit bills after saving (service/discount changes; wallet transactions are already processed on first save)
- Full bill preview with breakdown of all amounts
- Add customers with name, optional phone number, optional date of birth
- Phone number duplicate detection (prevents two customers with the same number)
- Customer search by name or phone
- Edit and delete customers
- Phone number privacy masking in bill previews and PDFs (shows
987***10) - Branch-aware welcome bonus on customer creation
Customers earn cashback and unlock higher reward tiers based on their lifetime spend.
Tier Lifetime Spend Range Default Cashback Default Max Redemption
----------- ---------------------- ---------------- ----------------------
Bronze Rs.0 - Rs.4,999 5% 10%
Silver Rs.5,000 - Rs.14,999 7% 12%
Gold Rs.15,000 - Rs.29,999 10% 15%
Platinum Rs.30,000+ 12% 20%
How it works:
- Cashback is earned on every bill above the minimum amount (default: Rs.200)
- Cashback is calculated on
billAmount - walletAmountUsed(net amount paid) - Customers can redeem up to
maxRedemptionRate% of the bill total from their wallet - New customers receive a welcome bonus (default: Rs.50)
- Tiers auto-upgrade when lifetime spend crosses a threshold
- Tiers auto-downgrade after inactivity:
- Bronze: never downgrades
- Silver: after 90 days of inactivity
- Gold: after 60 days
- Platinum: after 45 days
- Admins can manually adjust wallet balances with a recorded reason
Note: The rates above are the defaults. Each branch can override them via the branch cashback config.
Each branch can have its own cashback configuration stored in Firestore. This enables:
- Different rates per day of week (e.g., boosted "Tuesday Special" rates)
- Payment method eligibility (e.g., disable cashback/wallet for card payments)
- Custom welcome bonus and minimum bill amounts per branch
Rate Resolution Priority (highest to lowest):
1. Payment method eligibility
If eligiblePaymentMethodsForDiscount[paymentMethod] = false:
-> cashbackRate = 0, maxRedemptionRate = 0, wallet hidden
2. Day-of-week + tier rates
If payment method is eligible:
-> dayConfig[currentDayOfWeek][customerTier]
3. Default fallback
If no Firestore config document exists for the branch:
-> standard rates from the table above, all methods eligible
See Customizing Cashback Rates for how to configure per-branch rates.
- Download PDF: Professional receipt layout with salon branding, services table, discounts, wallet info, cashback earned, and social media links
- WhatsApp Share: Pre-formatted message with bill details and a link to the online bill preview, sent directly to customer's phone number
- QR Code: Scannable QR linking to the public bill preview page at
/bill/{billId}
Role Access
------ ---------------------------------------------------
admin All branches, wallet adjustments, full CRUD
user Assigned branch only, bill creation, customer ops
Roles and branch assignments are stored as custom claims on Firebase Auth JWT tokens. They are set via the admin script (scripts/set-user-claims.js). Users must log out and back in after claim changes take effect.
firestore/
|
+-- branches/{branchId}
| |-- name: string
| |-- address: string
| |-- phone?: string
| |-- createdAt: Timestamp
| |
| +-- config/cashbackConfig [subcollection document]
| |-- branchId: string
| |-- welcomeBonus: number (e.g. 50)
| |-- minBillForCashback: number (e.g. 200)
| |-- eligiblePaymentMethodsForDiscount:
| | cash: boolean
| | card: boolean
| | upi: boolean
| |-- dayConfig:
| | sunday:
| | bronze: { cashbackRate: 0.05, maxRedemptionRate: 0.10 }
| | silver: { cashbackRate: 0.07, maxRedemptionRate: 0.12 }
| | gold: { cashbackRate: 0.10, maxRedemptionRate: 0.15 }
| | platinum: { cashbackRate: 0.12, maxRedemptionRate: 0.20 }
| | monday: { ... same structure ... }
| | tuesday: { ... }
| | wednesday: { ... }
| | thursday: { ... }
| | friday: { ... }
| | saturday: { ... }
| |-- updatedAt: Timestamp
|
+-- customers/{customerId}
| |-- name: string
| |-- phone?: string
| |-- dateOfBirth?: string
| |-- wallet:
| | balance: number
| | lifetimeSpend: number
| | lifetimeEarned: number
| | lifetimeRedeemed: number
| | tier: "bronze" | "silver" | "gold" | "platinum"
| | tierUpdatedAt: Timestamp
| | lastActivityAt: Timestamp
| |-- createdAt: Timestamp
|
+-- bills/{billId}
| |-- billNumber: string (e.g. "PRZ-20260217-001")
| |-- customerId: string
| |-- customerName: string
| |-- customerPhone?: string
| |-- branchId: string
| |-- branchName: string
| |-- branchAddress: string
| |-- services: Array
| | { id, serviceName, price, discountAmount, staffName? }
| |-- subtotal: number
| |-- discountAmount: number
| |-- totalAmount: number
| |-- paymentMethod: "cash" | "card" | "upi"
| |-- cashbackEarned: number
| |-- walletAmountUsed: number
| |-- netPayableAmount: number
| |-- customerTierAtPurchase: string
| |-- walletBalanceAfter: number
| |-- cashbackRateApplied?: number (stored for historical reference)
| |-- maxRedemptionRateApplied?: number
| |-- createdAt: Timestamp
|
+-- walletTransactions/{txId}
|-- customerId: string
|-- type: "credit" | "debit" | "adjustment" | "welcome_bonus" | "tier_downgrade"
|-- amount: number
|-- billId?: string
|-- billNumber?: string
|-- description: string
|-- balanceAfter: number
|-- tierAtTransaction: string
|-- createdAt: Timestamp
|-- createdBy?: string (for admin adjustments)
branches Public read, authenticated write
customers Public read, authenticated write
bills Public read (for shareable bill links), authenticated write
walletTxns Public read, authenticated write
When a bill is being created, the system resolves the applicable cashback and redemption rates through the following process:
resolveAllRates(branchId, tier, paymentMethod)
|
v
getBranchConfig(branchId)
/ \
Firestore doc exists? No doc found
| |
v v
Parse config Use default config
(5-min cache) (all methods eligible,
| standard rates all days)
v
resolveRates(config, tier, paymentMethod)
|
+---- Is paymentMethod eligible? ----+
| |
NO YES
| |
v v
{ cashbackRate: 0, getDayOfWeek()
maxRedemptionRate: 0, |
isPaymentMethodEligible: false } v
config.dayConfig[today][tier]
|
v
{ cashbackRate: X,
maxRedemptionRate: Y,
isPaymentMethodEligible: true }
The resolved rates flow into the UI:
- If
isPaymentMethodEligibleisfalse, the wallet section is hidden and a notice is shown - If
true, cashback and redemption calculations use the resolved rates - The rates used are stored on the bill document (
cashbackRateApplied,maxRedemptionRateApplied) for historical accuracy
Option A: Via the admin script
Edit scripts/initialize-branches.js and add the branch to the branches array:
const branches = [
// ... existing branches ...
{
name: 'Pareez Salon - New Location',
address: '123 Main Street, City, State 000000',
phone: '+91 9876543210'
},
];Then run:
node scripts/initialize-branches.jsOption B: Directly in Firebase Console
Navigate to Firestore > branches collection > Add document with fields:
name(string)address(string)phone(string, optional)createdAt(timestamp, use server timestamp)
Note the auto-generated document ID.
If this branch needs custom rates, create a subcollection document at:
branches/{newBranchId}/config/cashbackConfig
See Customizing Cashback Rates for the document structure.
If you skip this step, the branch will automatically use the default rates (shown in the tier table above), with all payment methods eligible and standard welcome bonus/minimum bill amounts.
- Create a Firebase Auth user (email/password) in Firebase Console > Authentication > Add User
- Assign them to the branch via the admin script:
Edit scripts/set-user-claims.js:
// Add this line in the main() function:
await setUserClaims('[email protected]', 'user', 'NEW_BRANCH_ID_HERE');Run:
node scripts/set-user-claims.jsThe user will need to log out and back in for claims to take effect.
The new branch will appear in the branch selector dropdown for admin users. The assigned user will see only their branch after logging in.
Create or update the document at branches/{branchId}/config/cashbackConfig in Firestore (Firebase Console or programmatically).
{
"branchId": "abc123",
"welcomeBonus": 50,
"minBillForCashback": 200,
"eligiblePaymentMethodsForDiscount": {
"cash": true,
"card": false,
"upi": true
},
"dayConfig": {
"sunday": {
"bronze": { "cashbackRate": 0.05, "maxRedemptionRate": 0.10 },
"silver": { "cashbackRate": 0.07, "maxRedemptionRate": 0.12 },
"gold": { "cashbackRate": 0.10, "maxRedemptionRate": 0.15 },
"platinum": { "cashbackRate": 0.12, "maxRedemptionRate": 0.20 }
},
"monday": {
"bronze": { "cashbackRate": 0.05, "maxRedemptionRate": 0.10 },
"silver": { "cashbackRate": 0.07, "maxRedemptionRate": 0.12 },
"gold": { "cashbackRate": 0.10, "maxRedemptionRate": 0.15 },
"platinum": { "cashbackRate": 0.12, "maxRedemptionRate": 0.20 }
},
"tuesday": {
"bronze": { "cashbackRate": 0.08, "maxRedemptionRate": 0.15 },
"silver": { "cashbackRate": 0.10, "maxRedemptionRate": 0.18 },
"gold": { "cashbackRate": 0.12, "maxRedemptionRate": 0.20 },
"platinum": { "cashbackRate": 0.15, "maxRedemptionRate": 0.25 }
},
"wednesday": {
"bronze": { "cashbackRate": 0.05, "maxRedemptionRate": 0.10 },
"silver": { "cashbackRate": 0.07, "maxRedemptionRate": 0.12 },
"gold": { "cashbackRate": 0.10, "maxRedemptionRate": 0.15 },
"platinum": { "cashbackRate": 0.12, "maxRedemptionRate": 0.20 }
},
"thursday": {
"bronze": { "cashbackRate": 0.05, "maxRedemptionRate": 0.10 },
"silver": { "cashbackRate": 0.07, "maxRedemptionRate": 0.12 },
"gold": { "cashbackRate": 0.10, "maxRedemptionRate": 0.15 },
"platinum": { "cashbackRate": 0.12, "maxRedemptionRate": 0.20 }
},
"friday": {
"bronze": { "cashbackRate": 0.05, "maxRedemptionRate": 0.10 },
"silver": { "cashbackRate": 0.07, "maxRedemptionRate": 0.12 },
"gold": { "cashbackRate": 0.10, "maxRedemptionRate": 0.15 },
"platinum": { "cashbackRate": 0.12, "maxRedemptionRate": 0.20 }
},
"saturday": {
"bronze": { "cashbackRate": 0.05, "maxRedemptionRate": 0.10 },
"silver": { "cashbackRate": 0.07, "maxRedemptionRate": 0.12 },
"gold": { "cashbackRate": 0.10, "maxRedemptionRate": 0.15 },
"platinum": { "cashbackRate": 0.12, "maxRedemptionRate": 0.20 }
}
},
"updatedAt": "2026-02-17T00:00:00Z"
}In this example:
- Card payments never get wallet/cashback discount at this branch
- Tuesday has boosted rates for all tiers ("Tuesday Special")
- All other days use standard rates
| Field | Type | Description |
|---|---|---|
branchId |
string | Must match the parent branch document ID |
welcomeBonus |
number | Wallet credit for new customers created at this branch |
minBillForCashback |
number | Minimum bill total to earn any cashback |
eligiblePaymentMethodsForDiscount |
object | {cash: bool, card: bool, upi: bool} -- false disables both cashback earning and wallet redemption for that method |
dayConfig |
object | All 7 days required. Each day maps all 4 tiers to {cashbackRate, maxRedemptionRate} |
updatedAt |
timestamp | Last modified timestamp |
Rates are decimal fractions, not percentages:
0.05= 5%0.10= 10%0.20= 20%
You can also update config from code using saveBranchConfig():
import { saveBranchConfig, getDefaultBranchConfig } from '@/lib/branch-config';
// Start from defaults and customize
const config = getDefaultBranchConfig('YOUR_BRANCH_ID');
config.eligiblePaymentMethodsForDiscount.card = false;
config.dayConfig.tuesday.bronze.cashbackRate = 0.08;
await saveBranchConfig(config);The app is configured for Vercel deployment.
# Build locally to verify
npm run build
# Deploy via Vercel CLI
vercel --prodAlternatively, connect the GitHub repo to Vercel for automatic deployments on push.
Add all NEXT_PUBLIC_FIREBASE_* variables in Vercel > Project Settings > Environment Variables.
Deploy separately (requires Firebase CLI):
firebase deploy --only firestore:rules| Command | Description |
|---|---|
npm run dev |
Start development server at localhost:3000 |
npm run build |
Production build (TypeScript check + optimization) |
npm start |
Run production server |
npm run lint |
Run ESLint |
npm run start:firebase |
Start Firestore emulator (port 8080, UI on 4000) |
node scripts/initialize-branches.js |
Create branch documents in Firestore |
node scripts/set-user-claims.js |
Set user roles and branch assignments |





