39294-vm/04_BUSINESS_LOGIC.md
2026-03-24 15:28:07 +00:00

338 lines
9.1 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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' });
}
```