# Google Drive for job files — setup and local testing

Every job gets its own Google Drive folder, and job files live there instead of
on the server's disk. This is the runbook: what to click in Google, what to
change in the system, how to prove it works locally, and what changes on cPanel.

Nothing here affects a workspace until you switch Drive on in Settings. Until
then the system behaves exactly as it does today.

---

## Going straight to live, without testing locally

This is a reasonable choice here, for one specific reason: **the feature is inert
until somebody ticks the toggle.** `drive_enabled` defaults to off, so the deploy
carries the code and changes nothing. And `billpro:drive-check` runs on the live
server, against the real credentials, filesystem and PHP limits — which is a
better test than a local one, because those are the things that differ.

The order becomes:

1. Deploy the code and run the migration (Part D).
2. Prove the connection **on the server** with `billpro:drive-check --write`
   (Part B6). Nothing user-facing has changed yet at this point.
3. Only then tick the toggle in Settings, and walk the checks in Part C on one
   throwaway job.

If step 2 fails you have changed nothing and can fix it at leisure. Skip Part B
steps B3 and B4 — those are local XAMPP problems.

**Two things in this deploy are not inert**, and are worth knowing before you push:

- **`config/livewire.php` is new.** It raises Livewire's temporary-upload cap
  from its silent 12 MB default to the Drive ceiling, for *every* upload in the
  app, not just job files. This fixes a pre-existing bug — the job page already
  claimed 20 MB and was quietly failing at 12 — but it means PHP's limits are now
  what binds. Run `billpro:drive-check` and read the `php upload` line; it warns
  you when the two disagree and tells you which number to change.
- **The queue is now drained every minute** by the existing cron entry. Nothing
  else in the app has ever queued anything, so this should be a no-op, but if the
  live `jobs` table has ancient rows in it they will now run. Check first:
  `php artisan tinker --execute="echo DB::table('jobs')->count();"` — if that is
  anything but 0, look at what is in there before deploying.

---

## 0. Before you start: do you have Google Workspace?

**Shared Drives require a paid Google Workspace plan** (Business Starter or
above). A free `@gmail.com` account cannot create one, and this integration
depends on it.

Check: open [drive.google.com](https://drive.google.com) and look at the left
sidebar. If you see **"Shared drives"**, you are fine. If you only see "My
Drive", stop here.

Why it has to be a Shared Drive, and not just a folder someone shares:

- A **service account has no storage quota of its own.** It physically cannot
  own files in anybody's "My Drive". Uploads fail with `storageQuotaExceeded`
  no matter how small the file is. In a Shared Drive the storage belongs to the
  organisation, and it simply works.
- Files created by a **person** belong to that person. When they leave and their
  Workspace seat is deleted, the job archive goes with them.

If you do not have Workspace, the alternative is to authenticate as a human user
with OAuth instead of a service account. That is a different build; say so and
it can be added.

---

## Part A — Google side (about 10 minutes)

### A1. Create a project

1. Go to [console.cloud.google.com](https://console.cloud.google.com).
2. Project dropdown (top left) → **New Project**.
3. Name it `BillPro Files` → **Create**. Wait for it to become the active project.

### A2. Turn the Drive API on

1. Left menu → **APIs & Services → Library**.
2. Search `Google Drive API` → open it → **Enable**.

Skipping this is the `accessNotConfigured` error later.

### A3. Create the service account

1. Left menu → **IAM & Admin → Service Accounts** → **Create service account**.
2. Name it `billpro-drive`. Click **Create and continue**.
3. **Skip** the "Grant this service account access to project" step — it needs
   no project roles. Click **Continue**, then **Done**.

### A4. Download its key

1. Click the service account you just made → **Keys** tab.
2. **Add key → Create new key → JSON → Create**. A `.json` file downloads.
3. Open it in a text editor and copy the **`client_email`** value. It looks like
   `billpro-drive@billpro-files-123456.iam.gserviceaccount.com`.

Treat this file like a password. It is the only credential needed to read and
write everything in the Shared Drive.

### A5. Create the Shared Drive

Go to [drive.google.com](https://drive.google.com) → **Shared drives** → **New**
→ name it `BillPro Job Files` → **Create**.

### A6. Let the service account in

1. Open the new Shared Drive → click its name at the top → **Manage members**.
2. Paste the `client_email` from step A4.
3. Set the role to **Content manager**. Contributor is not enough — it cannot
   create folders.
4. Untick "Notify people" (there is no inbox behind that address) → **Send**.

**This is the step people miss.** Without it Google answers 404 for a drive it
will not admit exists, and `billpro:drive-check` will say so.

### A7. Copy the Shared Drive ID

Open the Shared Drive and read it out of the address bar:

```
https://drive.google.com/drive/folders/0AH7xK9pQrStUk9PVA
                                       ^^^^^^^^^^^^^^^^^^ this is the ID
```

---

## Part B — Local machine

### B1. Put the key somewhere safe

Move the downloaded JSON **outside the project folder**. Anything inside the
repo risks being committed or, worse, served over HTTP.

```
C:\Users\<you>\.secrets\billpro-drive.json
```

### B2. Add the settings to `.env`

```dotenv
GOOGLE_DRIVE_CREDENTIALS="C:\Users\<you>\.secrets\billpro-drive.json"
GOOGLE_DRIVE_SHARED_DRIVE_ID=0AH7xK9pQrStUk9PVA
GOOGLE_DRIVE_ROOT_FOLDER_ID=
GOOGLE_DRIVE_MAX_UPLOAD_KB=512000
```

Leave `ROOT_FOLDER_ID` blank to file jobs at the top of the Shared Drive.

### B3. Fix cURL's certificate store — required on this machine

XAMPP ships no CA bundle, so **every HTTPS call to Google fails locally** with
`cURL error 60: SSL certificate problem`. This is a local-only problem; cPanel
hosts normally have it configured already.

1. Download [`cacert.pem`](https://curl.se/ca/cacert.pem).
2. Save it as `C:\xampp252\php\extras\ssl\cacert.pem`.
3. Edit `C:\xampp252\php\php.ini` and set **both** of these:

```ini
curl.cainfo = "C:\xampp252\php\extras\ssl\cacert.pem"
openssl.cafile = "C:\xampp252\php\extras\ssl\cacert.pem"
```

4. Restart Apache, or your `php artisan serve` process.

### B4. Raise PHP's upload limits — only if you want to test big files

The system now accepts 500 MB, but PHP does not. Local values are currently
`upload_max_filesize = 40M` and `post_max_size = 40M`, so a 30 MB test file
already proves the point. To go bigger, in the same `php.ini`:

```ini
upload_max_filesize = 512M
post_max_size = 512M
```

### B5. Apply the database change

```bash
php artisan migrate
php artisan config:clear
```

### B6. Prove the connection before touching the app

```bash
php artisan billpro:drive-check
```

You want credentials found, `token : obtained`, and your workspaces listed. The
command names the exact problem when there is one — a missing file, a revoked
key, a Shared Drive that was never shared.

Then prove it can actually write, using a real job number:

```bash
php artisan billpro:drive-check --job=JOB-2026-08-0123 --write
```

This creates the folder, lists it, uploads a small probe file and bins the
probe. If it passes, the integration works.

---

## Part C — Test it in the app

Start everything, **including the queue worker**. Folders are created in the
background, so without a worker a new job gets no folder:

```bash
composer dev
```

That runs the server, the queue listener, logs and Vite together.

### C1. Switch it on

**Settings → Google Drive** → tick *Store job files in Google Drive* → **Save**.

If the panel shows an amber "Not connected yet" box, Part B is not finished. The
toggle does nothing until that clears.

### C2. Walk the six checks

| # | Do this | Expect |
|---|---|---|
| 1 | Create a new job | The folder is there immediately: `2026 / August / SPJ-260807005071` |
| 2 | Open the job → **Files** tab | "Open folder in Drive" and "Check Drive for new files" are there |
| 3 | Upload a 30 MB file on the Files tab | It appears on the job with an **In Drive** badge, and in the Drive folder |
| 4 | In Drive, drag a file into that folder yourself | Back on the job, press **Check Drive for new files** → it is listed. Left alone, the scheduled sweep picks it up within ten minutes |
| 5 | Open the customer's job link, upload artwork as the customer | It lands in the Drive folder and opens from the job page. The customer is never shown a Drive link |
| 6 | Delete a file from the job | Gone from the job; found in the Drive **bin**, not destroyed |

### C3. Test the failure path — worth doing once

Temporarily break the Shared Drive ID in `.env`, run `php artisan config:clear`,
then upload a file on a job.

The upload should still **succeed**, landing on local disk instead. That is
deliberate: nobody should lose a file because Google is unreachable. The reason
is in `storage/logs/laravel.log`. Then put the ID back.

---

## Part D — cPanel

Everything above applies, with four differences.

1. **Upload the JSON key outside `public_html`** — for example
   `/home/<cpaneluser>/.secrets/billpro-drive.json` — then set
   `GOOGLE_DRIVE_CREDENTIALS=/home/<cpaneluser>/.secrets/billpro-drive.json`
   and `chmod 600` it.

2. **The queue is drained by the existing cron entry.** The scheduler now runs
   `queue:work --stop-when-empty` every minute, so the single
   `* * * * * … artisan schedule:run` line from DEPLOY_CPANEL.md is all that is
   needed. Confirm that cron job exists and is running, or job folders will
   never be created.

3. **Rebuild the caches after changing `.env`:**

   ```bash
   php artisan migrate --force
   php artisan config:cache
   php artisan billpro:drive-check
   ```

4. **Check PHP's upload limits** in cPanel → *MultiPHP INI Editor*.
   `upload_max_filesize`, `post_max_size` and `max_execution_time` all cap what
   can be uploaded regardless of what the system allows.

Then repeat the Part C checks once on the live site with a throwaway job.

---

## When something goes wrong

Run `php artisan billpro:drive-check` first. It prints Google's own words, which
the app deliberately hides from users.

| Symptom | Cause |
|---|---|
| `cURL error 60: SSL certificate problem` | Step B3 not done |
| `404 … File not found: <drive id>` | Step A6 — the Shared Drive was never shared with the service account |
| `storageQuotaExceeded` | Writing outside a Shared Drive, or the role is Contributor rather than Content manager |
| `accessNotConfigured` | Step A2 — Drive API not enabled on the project |
| `invalid_grant` | Key revoked, wrong project, or the server clock is more than a minute fast |
| Uploads work but land on disk, not Drive | Drive is failing and falling back. Reason is in `storage/logs/laravel.log` |
| New jobs get no folder | No queue worker running (local), or the cron entry is missing (cPanel) |
| Large upload fails with no message | PHP's `upload_max_filesize` / `post_max_size`, not the app |
