# Business Logic, Rules & Validations — Swish Auto Care GMS --- ## 1. Job Card Rules ### Job Number Generation ```js // Format: SAC-YYYYMMDD-SEQ // SEQ = count of jobs created today + 1, zero-padded to 3 digits // Example: SAC-20260324-001, SAC-20260324-002 async function generateJobNo(db) { const today = new Date().toISOString().slice(0, 10); // YYYY-MM-DD const dateStr = today.replace(/-/g, ''); // YYYYMMDD const [rows] = await db.query( 'SELECT COUNT(*) as cnt FROM job_cards WHERE DATE(job_date) = ?', [today] ); const seq = String(rows[0].cnt + 1).padStart(3, '0'); return `SAC-${dateStr}-${seq}`; } ``` ### Status Transitions - New job → always starts as `'active'` - `active` → `done`: triggered by POST /job-cards/:id/close - **No reverse transition.** `done` cannot go back to `active` in demo. - Cannot edit `final_amount`, `payment_mode`, `closed_at` once set. ### Field Requirements by Job Type | Field | Full Job | Quick Wash | |-------|----------|-----------| | reg_no | Required | Required | | car_name | Required | Required | | owner_name | Required | Optional | | mobile_no | Required | Optional | | service_type | Required | Required | | assigned_staff_id | Required | Required | | estimated_amount | Optional | Optional | | notes | Optional | Not shown | ### Closing a Job Card ```js // Validate before closing: if (!final_amount || final_amount <= 0) throw 'Final amount required'; if (!payment_mode) throw 'Payment mode required'; if (job.status === 'done') throw 'Job already closed'; // On close: UPDATE job_cards SET status = 'done', final_amount = ?, payment_mode = ?, closed_at = NOW() WHERE id = ? AND status = 'active' ``` --- ## 2. Staff Code Generation ```js async function generateStaffCode(db) { const [rows] = await db.query( 'SELECT staff_code FROM staff ORDER BY id DESC LIMIT 1' ); if (rows.length === 0) return 'SAC-STF-001'; const last = rows[0].staff_code; // e.g. SAC-STF-004 const num = parseInt(last.split('-')[2]) + 1; return `SAC-STF-${String(num).padStart(3, '0')}`; } ``` --- ## 3. Attendance Rules ### Bulk Save (Upsert) ```sql INSERT INTO attendance (staff_id, attendance_date, status) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE status = VALUES(status), updated_at = NOW(); ``` ### Monthly Summary Calculation ```js function calculateAttendanceSummary(records) { const present = records.filter(r => r.status === 'present').length; const absent = records.filter(r => r.status === 'absent').length; const half = records.filter(r => r.status === 'half').length; // Half day counts as 0.5 for salary purposes const effectiveDays = present + (half * 0.5); return { present, absent, half, effectiveDays }; } ``` --- ## 4. Salary Calculation ### Formula ``` Calculated Salary = (Monthly Salary / 26) × Effective Days Present Where: Effective Days = present_days + (half_days × 0.5) 26 = standard working days per month denominator (industry standard) ``` ```js function calculateSalary(monthlySalary, daysPresent, halfDays = 0) { const effectiveDays = daysPresent + (halfDays * 0.5); const dailyRate = monthlySalary / 26; return Math.round(dailyRate * effectiveDays * 100) / 100; // 2 decimal places } ``` ### Admin Override - `calculated_salary` is always stored (what the formula says) - `paid_amount` is what admin actually pays (can be different) - Both fields stored in staff_payments — never lose the audit trail ### Payment Types - `salary` — regular monthly salary - `advance` — advance against next month's salary - `bonus` — extra payment (festival, performance) - `deduction` — penalty or loan recovery (stored as negative in accounts) For `deduction` type: store `paid_amount` as positive number, flag as deduction type. In accounts, treat as income-reducing or expense depending on business interpretation. For simplicity in demo: deduction payments are NOT shown in accounts expense — they are simply logged records. --- ## 5. Parts & Stock Rules ### Part Code Generation ```js async function generatePartCode(db) { const [rows] = await db.query( 'SELECT part_code FROM parts ORDER BY id DESC LIMIT 1' ); if (rows.length === 0) return 'SAC-PRT-001'; const last = rows[0].part_code; const num = parseInt(last.split('-')[2]) + 1; return `SAC-PRT-${String(num).padStart(3, '0')}`; } ``` ### Stock Deduction (Atomic Operation) ```js // Must check and deduct atomically to prevent overselling const [result] = await db.query( 'UPDATE parts SET stock_qty = stock_qty - ? WHERE id = ? AND stock_qty >= ?', [quantity, part_id, quantity] ); if (result.affectedRows === 0) { throw new Error('Insufficient stock'); } // Only insert sale record after successful stock deduction ``` ### Low Stock Alert ```js // is_low flag returned in GET /api/parts: const is_low = part.stock_qty <= part.low_stock_alert; ``` ### Price Snapshot - When a sale is recorded, store `unit_price` from the sale moment (not always current selling_price) - This allows price changes without affecting historical records --- ## 6. Cash Ledger Rules - `description` field is **mandatory** — reject if empty/whitespace - `amount` must be > 0 - `entry_type` must be 'in' or 'out' - `entry_date` defaults to NOW() but admin can edit (for backdating if they forgot) --- ## 7. Accounts Module — How It Aggregates The accounts view pulls from 4 sources: ```sql -- INCOME SELECT SUM(CASE WHEN status='done' THEN final_amount ELSE 0 END) as job_revenue FROM job_cards WHERE DATE(closed_at) BETWEEN ? AND ?; SELECT SUM(total_amount) as parts_revenue FROM parts_sales WHERE DATE(sale_date) BETWEEN ? AND ?; SELECT SUM(amount) as cash_in_other FROM cash_ledger WHERE entry_type = 'in' AND DATE(entry_date) BETWEEN ? AND ?; -- EXPENSE SELECT SUM(paid_amount) as staff_payments FROM staff_payments WHERE payment_date BETWEEN ? AND ? AND payment_type != 'deduction'; SELECT SUM(amount) as cash_out_other FROM cash_ledger WHERE entry_type = 'out' AND DATE(entry_date) BETWEEN ? AND ?; ``` --- ## 8. Daily Sales Report — Excel Structure The report Excel file should have these sheets (or sections within one sheet): **Sheet 1: Daily Summary** ``` Row 1: SWISH AUTO CARE — DAILY SALES REPORT Row 2: Date: 24/03/2026 Row 3: (blank) Row 4: Total Income | ₹38,000 Row 5: Total Expense | ₹15,800 Row 6: Net Balance | ₹22,200 ``` **Sheet 2: Job Revenue** Columns: Job No | Reg No | Car | Service | Staff | Amount | Payment Mode | Time **Sheet 3: Parts Sales** Columns: Part | Qty | Unit Price | Total | Customer | Payment | Time **Sheet 4: Cash In** Columns: Category | Description | Amount | Time **Sheet 5: Cash Out** Columns: Category | Description | Amount | Time **Sheet 6: Staff Payments** Columns: Staff | Type | Mode | Amount | Notes --- ## 9. Excel Export — SheetJS Pattern ```js const XLSX = require('xlsx'); function createExcelResponse(res, filename, sheetsData) { const wb = XLSX.utils.book_new(); sheetsData.forEach(({ sheetName, data }) => { const ws = XLSX.utils.json_to_sheet(data); XLSX.utils.book_append_sheet(wb, ws, sheetName); }); const buffer = XLSX.write(wb, { type: 'buffer', bookType: 'xlsx' }); res.setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'); res.setHeader('Content-Disposition', `attachment; filename="${filename}"`); res.send(buffer); } ``` --- ## 10. JWT Auth Pattern ```js // backend/src/middleware/auth.js const jwt = require('jsonwebtoken'); module.exports = (req, res, next) => { const authHeader = req.headers.authorization; if (!authHeader?.startsWith('Bearer ')) { return res.status(401).json({ error: 'No token provided' }); } const token = authHeader.split(' ')[1]; try { const decoded = jwt.verify(token, process.env.JWT_SECRET); req.user = decoded; next(); } catch { return res.status(401).json({ error: 'Invalid or expired token' }); } }; // On login, sign: const token = jwt.sign( { id: user.id, username: user.username }, process.env.JWT_SECRET, { expiresIn: '8h' } ); ``` --- ## 11. Input Validations (Frontend + Backend) | Field | Rule | |-------|------| | Mobile No. | 10 digits, numeric only | | Amount fields | Positive number, max 2 decimal places | | Quantity | Positive integer | | Date fields | Valid date, not future for attendance | | Reg No | Uppercase, alphanumeric, trim whitespace | | Description (cash) | Non-empty after trim, min 3 chars | | Payment mode | Must be one of: cash, upi, card | | Username/password | Non-empty, min 3 chars | --- ## 12. Date & Currency Formatting (Frontend) ```ts // Currency: Always show ₹ with comma formatting function formatCurrency(amount: number): string { return `₹${amount.toLocaleString('en-IN', { minimumFractionDigits: 0 })}`; } // ₹28,500 (not ₹28500) // Date: DD/MM/YYYY function formatDate(dateStr: string): string { const d = new Date(dateStr); return d.toLocaleDateString('en-IN', { day:'2-digit', month:'2-digit', year:'numeric' }); } // 24/03/2026 // DateTime: DD/MM/YYYY HH:MM function formatDateTime(dateStr: string): string { const d = new Date(dateStr); return d.toLocaleDateString('en-IN', { day:'2-digit', month:'2-digit', year:'numeric', hour:'2-digit', minute:'2-digit' }); } ```