# SeraNeon Salary Slip Portal

A self-contained PHP web application for generating Indian salary slips
(in INR ₹) for **SERANEON ADVISORY INDIA PRIVATE LIMITED**.

* Upload an `.xlsx` of employee pay data
* Pick a pay-period month
* Get one branded PDF slip per employee, bundled into a single ZIP
* Review every slip and email each one **manually** (one click per
  employee, or a single "Send all unsent" button) — emails are never sent
  automatically

The whole project is plain PHP + Composer, designed to drop straight onto
shared hosting (GoDaddy, cPanel, Hostinger, etc.). No database required.

---

## 1. Folder Structure

```
seraneonsalary/
├── index.php             # Admin dashboard (after login)
├── login.php             # Login page (session auth)
├── logout.php
├── process.php           # Upload + parse + generate pipeline
├── run.php               # Per-run review page: per-employee status + send buttons
├── runs.php              # Slip Archive: full list of past runs
├── send.php              # Send-one / send-all-unsent endpoint (CSRF-checked)
├── delete_run.php        # Delete-from-archive endpoint (CSRF-checked, recursive)
├── test_email.php        # Email diagnostic page (Test email button) - works with Graph or SMTP
├── download.php          # Authenticated download endpoint (zip / per-employee PDF)
├── sample.php            # Streams the sample Excel template
├── config.php            # Edit credentials, branding, SMTP, retention here
├── composer.json
├── .htaccess             # Security headers, deny rules, robots
├── robots.txt            # Disallow: /
├── assets/
│   ├── css/app.css       # Dashboard + login styling
│   ├── js/app.js
│   └── img/logo.png      # Company logo (overwritten via dashboard upload)
├── lib/
│   ├── helpers.php       # Indian number format, masking, CSRF, etc.
│   ├── auth.php          # Session-based auth helpers
│   ├── excel.php         # PhpSpreadsheet parser + sample template writer
│   ├── slip.php          # Dompdf-based slip renderer
│   ├── mailer.php        # PHPMailer wrapper + Test SMTP helper + transport dispatcher
│   ├── graph_mailer.php  # Microsoft Graph (Mail.Send) transport - no SMTP, no App Password
│   └── runs.php          # Run-archive helpers (load_run / save_employees / prune)
├── uploads/              # Last uploaded Excel (deny-all .htaccess)
├── zips/                 # Pointer to latest ZIP (legacy compatibility)
├── history/              # All saved runs (each = self-contained folder, see below)
├── tests/smoke.php       # End-to-end smoke test (run from CLI)
└── vendor/               # Composer dependencies (PhpSpreadsheet, Dompdf, PHPMailer)
```

Each generation lives in its own folder under `history/`:

```
history/20260505_2049_a1b2/
  meta.json              # month, generated_at, source filename, ...
  employees.json         # one record per employee (incl. email_status)
  pdfs/                  # per-employee PDFs
    SalarySlip_<Name>_<Month>.pdf
  archive.zip            # bundled ZIP for download
```

---

## 2. Tech stack

| Concern         | Library                      | Version |
| --------------- | ---------------------------- | ------- |
| Excel parsing   | `phpoffice/phpspreadsheet`   | ^1.29   |
| PDF generation  | `dompdf/dompdf`              | ^2.0    |
| ZIP bundling    | PHP built-in `ZipArchive`    | core    |
| Sessions / auth | PHP sessions + bcrypt hashes | core    |
| PHP runtime     |                              | 7.4+    |

Required PHP extensions (all enabled by default on GoDaddy):
`mbstring`, `gd`, `dom`, `xml`, `zip`, `fileinfo`, `iconv`.

---

## 3. Required Excel Format

Column order **must** match exactly (download the live sample from the
dashboard via "Download sample template"):

| #  | Column                        | Required | Notes |
|----|-------------------------------|----------|-------|
| 1  | Full Name                     | Yes      | Used for slip + filename |
| 2  | Employee ID                   | No       | Optional employee reference shown on slip when provided |
| 3  | Date of Joining               | No       | Optional; supports Excel date cells or text dates, rendered as `dd Mon YYYY` |
| 4  | PAN                           | No       | Optional; expected format `ABCDE1234F`, shown masked on slip (`ABCDE****F`) |
| 5  | Basic + DA                    | Yes      | Monthly salary before tax (Taxable) |
| 6  | Reimbursements                | No       | **Non-taxable**, hidden if blank/0 |
| 7  | Overtime (Current Month)      | No       | Taxable, hidden if blank/0 |
| 8  | Overtime (Previous Months)    | No       | Taxable, hidden if blank/0 |
| 9  | Tax Deducted                  | Yes      | Shown as TDS in deductions |
| 10 | Bonus                         | No       | Taxable, hidden if blank/0 |
| 11 | Bank Name                     | Yes      | Shown on slip |
| 12 | Account Number                | Yes      | Masked to `xxxx<last4>` |
| 13 | Email                         | No       | Required only if you want to email this person's slip |

**Net Pay** = (Basic + DA + Reimbursements + Overtime + Bonus) − Tax Deducted.

Blank optional cells are treated as `0`. Optional rows/columns with
`0` are hidden from the slip per the spec.

---

## 4. Quick Local Run

```bash
cd seraneonsalary
composer install --no-dev
php -S localhost:8080      # then open http://localhost:8080/login.php
```

Default login (CHANGE BEFORE DEPLOY):

* Username: `admin`
* Password: `ChangeMe@2026`

To run the smoke test:

```bash
php tests/smoke.php
```

---

## 5. Production Deployment to GoDaddy (cPanel)

### 5.1 One-time prep on your machine

1.  Make sure `composer install --no-dev --optimize-autoloader` has been run
    inside `seraneonsalary/`. The `vendor/` directory must exist.
2.  **Generate a fresh password hash** for your admin password and update
    `config.php`:

    ```bash
    php -r "echo password_hash('YourStrongPassword!', PASSWORD_BCRYPT);"
    ```

    Open `config.php` and replace the `ADMIN_PASSWORD_HASH` value with the
    string printed (keep the surrounding single quotes).

    Also change `ADMIN_USERNAME` if you want a non-default account name.

3.  Zip the entire `seraneonsalary/` folder (including `vendor/`) for upload.

### 5.2 Upload to GoDaddy

1.  Log into your GoDaddy cPanel and open **File Manager**.
2.  Navigate to `public_html/` (or whatever directory serves
    `raghavkapoor.org`).
3.  Upload the zip and **Extract** it. You should now have:
    `public_html/seraneonsalary/...`
4.  Open the **MultiPHP Manager** in cPanel and ensure
    `raghavkapoor.org` (and `seraneonsalary` subfolder) is set to
    **PHP 8.1 or higher**.

### 5.3 Set folder permissions

Inside `seraneonsalary/`, the following directories must be writable by
the PHP process (cPanel default is typically already correct):

* `uploads/`
* `zips/`
* `history/` (and every subfolder it creates per run)
* `assets/img/` (so the dashboard can replace the logo)

Recommended permissions: `755` on directories, `644` on files. The
included `.htaccess` files already block direct browser access to all
internal folders even if a malicious link leaks.

### 5.4 Verify

Open: `https://raghavkapoor.org/seraneonsalary/` — you should be
redirected to `login.php` and see a noindex login screen.

Sign in, upload the sample template, click **Generate & Download ZIP**,
and confirm a ZIP downloads containing one PDF per employee.

### 5.5 (Optional) Enforce HTTPS

Add this to the top of `seraneonsalary/.htaccess` if your hosting plan
serves over both HTTP and HTTPS:

```apache
RewriteEngine On
RewriteCond %{HTTPS} !=on
RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [R=301,L]
```

---

## 6. Security Summary

* **Session-based admin login** with bcrypt-hashed password.
* **CSRF token** on every state-changing form.
* **Brute-force throttle** (60 s lockout after 5 failed logins).
* **Strict-mode sessions** with `HttpOnly`, `SameSite=Lax`, and
  `Secure` cookies on HTTPS.
* **Idle timeout**: sessions expire after 1 hour of inactivity.
* **No public indexing**: `robots.txt` (Disallow: /), `<meta noindex>`,
  and `X-Robots-Tag` header.
* **File upload validation**:
  * Extension allow-list (`.xlsx` for data, `.png/.jpg/.jpeg` for logo)
  * 5 MB hard cap
  * `is_uploaded_file()` check
  * `.xlsx` magic-byte sanity check (`PK\x03\x04`)
  * `getimagesize()` validation for logos
* **Path-traversal hardening** on the download endpoint
  (`realpath()` + base-dir comparison + filename regex).
* **All internal folders** (`uploads/`, `generated_pdfs/`, `zips/`,
  `history/`, `lib/`, `vendor/`, `templates/`, `tests/`) ship a deny-all
  `.htaccess` and a placeholder `index.html`.
* **Security headers** set via `.htaccess`:
  `X-Robots-Tag`, `X-Content-Type-Options`, `X-Frame-Options`,
  `Referrer-Policy`, `Permissions-Policy`.

---

## 7. Features

* Upload `.xlsx` with employee pay data (max 5 MB)
* Optional logo replacement at upload time
* "Reuse last upload" checkbox to skip re-uploading the same file
* "Preview first slip" button — opens the first slip in a new tab using
  the browser's PDF viewer, so you can sanity-check the layout before
  generating the full set
* Bulk-generate one PDF per employee:
  `SalarySlip_<Name>_<Month>.pdf`
* Auto-bundle into one ZIP: `SalarySlips_<Month>_<timestamp>.zip`
* **Manual review-and-send workflow**: after generation you land on a
  per-run page that lists every employee with their net pay, email
  status, a **View PDF** link, and an individual **Send** / **Resend**
  button. A **Send all unsent** button dispatches the whole remaining
  batch in one click. Emails are never sent automatically.
* **Slip Archive**: every generation is preserved (default: last 100
  runs). Re-open any past month, re-send a slip to one person who lost
  the email, or download the ZIP again — without re-uploading the Excel.
  Each archived run can be **Deleted** from either the run review page
  or the archive list (with confirmation).
* **Two email transports** — pick whichever your environment allows:
  * **Microsoft Graph (Mail.Send / app-only OAuth)** — recommended for
    Microsoft 365. No SMTP AUTH, no App Password, no MFA workarounds.
    The admin grants one-time consent for an Azure AD app; the app sends
    via the Graph REST API. Can be scoped to a single mailbox via
    `New-ApplicationAccessPolicy`. See SETUP-GUIDE.md "Cheat Sheet C".
  * **SMTP via PHPMailer** — fallback for non-M365 providers (Gmail,
    SendGrid, Brevo, GoDaddy, etc.) and existing M365 setups that already
    work with App Passwords.
  If both are configured, Graph wins.
* **Test email** button: instantly diagnose your `config.php` email block
  by sending yourself a one-line test email. Works for Graph and SMTP.
  Failures show the exact transport-level error plus a one-line "what this
  likely means" hint covering the common Microsoft 365 mistakes (Graph:
  `AADSTS70011`, `AADSTS7000215`, `ApplicationAccessPolicy`,
  `MailboxNotEnabledForRESTAPI`; SMTP: Authenticated SMTP off, Send-as
  permission missing, App Password needed, etc.).
* Indian number formatting (`₹1,50,000`) and amount-in-words
  (`One Lakh Fifty Thousand Rupees Only`)
* Slip layout follows Indian payslip norms:
  * Pay Date = 5th of the next month (e.g. April → 5 May)
  * Earnings split into TAXABLE and NON-TAXABLE groups
  * Taxable Earnings subtotal + Gross Earnings total
  * Tax Deducted shown as `Tax Deducted (TDS)`
  * Net Pay highlighted, both in figures and words
  * Reimbursements explicitly labelled "Non-taxable"
  * Statutory strip (CIN / GSTIN / PAN) at the bottom of each slip
* Slip hides optional fields whose value is 0 / blank
* Account number is always masked to `xxxx<last4>`

---

## 8. Customising

### 8.1 Branding & company details

All of these live in `config.php`. Each individual line is hidden if you
leave the value blank, so you can show as much or as little as you want.

* **`COMPANY_NAME`** — appears at the top of every slip
* **`PRIMARY_COLOR`** — primary accent colour for slips and the dashboard
* **`COMPANY_FOOTER`** — single-line footer at the bottom of each slip
* **`COMPANY_ADDRESS_LINES`** — array of address lines shown next to the logo
* **`COMPANY_PHONE` / `COMPANY_EMAIL` / `COMPANY_WEBSITE`** — contact line
  rendered just below the address
* **`COMPANY_CIN` / `COMPANY_GSTIN` / `COMPANY_PAN`** — statutory strip
  rendered above the footer (CIN + GSTIN are the most common on Indian
  payslips for KYC, loan, audit purposes)

### 8.2 Email transport (Microsoft Graph or SMTP)

For a non-developer-friendly walkthrough — including the **recommended
Microsoft Graph path** that works without SMTP AUTH or App Passwords, plus
the special case of Microsoft 365 **shared mailboxes** — see
[`SETUP-GUIDE.md`](../SETUP-GUIDE.md). The short version is below.

#### Option 1 — Microsoft Graph (recommended for M365)

Fill in the four required `GRAPH_*` constants in `config.php`:

```php
define('GRAPH_TENANT_ID',     '<directory-tenant-id>');
define('GRAPH_CLIENT_ID',     '<application-client-id>');
define('GRAPH_CLIENT_SECRET', '<client-secret-Value>');
define('GRAPH_FROM_USER',     'hr.communication@yourcompany.com');
define('GRAPH_FROM_NAME',     'YourCompany HR');
define('GRAPH_REPLY_TO',      '');
define('GRAPH_SAVE_TO_SENT',  true);
```

The Azure AD admin grants a one-time `Mail.Send` (Application) consent and
generates the secret. The full step-by-step is **Cheat Sheet C** in
SETUP-GUIDE.md. **No mailbox password, no App Password, no MFA toggle.**

#### Option 2 — SMTP via PHPMailer (fallback)

Set the values below in `config.php`. Leave `SMTP_HOST` blank to disable
this path. If you've already filled in the Graph block above, the SMTP
block is ignored.

```php
define('SMTP_HOST',       'smtpout.secureserver.net');  // or smtp.gmail.com, etc.
define('SMTP_PORT',       465);
define('SMTP_USERNAME',   'payroll@yourcompany.com');
define('SMTP_PASSWORD',   '<smtp-password-or-app-password>');
define('SMTP_ENCRYPTION', 'ssl');                       // 'tls', 'ssl', or '' (none)
define('SMTP_FROM_EMAIL', 'payroll@yourcompany.com');
define('SMTP_FROM_NAME',  'YourCompany Payroll');
```

Common provider settings:

| Provider                    | Host                                     | Port | Encryption |
|-----------------------------|------------------------------------------|------|------------|
| GoDaddy / Workspace Email   | `smtpout.secureserver.net`               | 465  | `ssl`      |
| Gmail / Google Workspace    | `smtp.gmail.com`                         | 587  | `tls`      |
| Microsoft 365               | `smtp.office365.com`                     | 587  | `tls`      |
| Amazon SES                  | `email-smtp.<region>.amazonaws.com`      | 587  | `tls`      |
| Zoho Mail                   | `smtp.zoho.in`                           | 465  | `ssl`      |

Once either Graph or SMTP is configured:

1. Add an `Email` column to the Excel (column 13 in the current template), one row per employee.
   Rows with a blank Email show up on the run page with a **No email on
   file** badge and no Send button — their PDFs are still generated and
   downloadable.
2. From the dashboard, click **Test email** in the top-right and send a
   test to your own address. Green banner = ready. Red banner = follow
   the on-page hint and try again.
3. On the dashboard, click **Generate slips**. You land on the run
   review page where every Send button uses whichever transport is
   active (Graph wins over SMTP if both are set).

### 8.3 Other knobs

* **Available months in dropdown**: by default it lists the previous 12
  months. Change `month_options()` in `lib/helpers.php`.
* **Required-field rules**: `REQUIRED_FIELDS` in `config.php`.
* **History retention**: `HISTORY_KEEP` in `config.php` (default 100).
  Older runs are pruned automatically each time a new run is generated.
* **Pay-date convention**: 5th of the month following the pay period
  (April → 5 May). Change in `lib/slip.php` (search for `Pay date convention`).

---

## 9. Troubleshooting

| Symptom | Likely cause / fix |
|---------|-------------------|
| 500 error on first load | Run `composer install --no-dev` so `vendor/` exists. |
| "Could not save the uploaded file" | Make `uploads/` writable (`chmod 755`). |
| ZIP generates but is empty | Check `history/` is writable and that `php-gd` is enabled (Dompdf needs it for embedded images). |
| Test SMTP returns a red error | Read the on-page error message and hint, then see the troubleshooting table in [`SETUP-GUIDE.md`](../SETUP-GUIDE.md) for the one-line fix. |
| "Email is currently disabled" banner on a run page | `SMTP_HOST` is blank in `config.php`. Fill in the SMTP block and reload — no need to regenerate the run. |
| Session keeps logging out | Make sure your browser accepts cookies for the host and that the system clock isn't badly out of sync. |
| `mb_string` errors on shared host | Enable the `mbstring` PHP extension via cPanel's "Select PHP Version → Extensions". |
| Indexed by Google despite robots.txt | Confirm `https://raghavkapoor.org/seraneonsalary/robots.txt` returns `Disallow: /`. The app also sets the `X-Robots-Tag` header and a `<meta noindex>` tag. |

---

## 10. Changing the Admin Password

```bash
php -r "echo password_hash('NewPassword!', PASSWORD_BCRYPT);"
```

Copy the printed hash into `config.php`:

```php
define('ADMIN_PASSWORD_HASH', '$2y$12$....');
```

Save and re-upload `config.php` only — no other change needed.
