## Context

- **App**: Laravel 5.6, PHP 7.4. `App\Console\Kernel::schedule` defines one task, `Order::summaryLogging()` → `daily()`. App timezone is `Asia/Ho_Chi_Minh`, so the task is due at 00:00 local = 17:00 UTC; Laravel converts, the host timezone (UTC) needs no change.
- **Dev host** (`web-dev-linux`, Ubuntu): Apache2 + mod_php, workers run as `www-data`. No nginx, no php-fpm. `cron.service` is active and enabled. PHP CLI at `/usr/bin/php` (7.4.33).
- **Deploy**: `.gitlab-ci.yml` job `pos-dev` (runner tag `web_dev`, runner user `gitlab-runner`, already uses `sudo rm -rf`) copies the tree to `/var/www/html/tnx_pos_2026/dev/pos/src` on every tagged deploy after deleting the previous `src`. The path is stable across deploys. `storage` is a symlink to `…/dev/pos/shared/storage` (`gitlab-runner:www-data`, `2775` setgid).
- **Gap**: nothing invokes `php artisan schedule:run`, so scheduled tasks never execute on dev.
- **Constraint**: Laravel 5.6 has no `schedule:work`; cron is the documented and only mechanism. The dev machine is Windows for local work, so line-ending safety matters for files consumed by Linux daemons.

## Goals / Non-Goals

**Goals:**
- Run `php artisan schedule:run` every minute on the dev server, from the project root, as the same OS user as the web server.
- Keep the cron definition in the repo and have CI install it, so a rebuilt server or a changed path never silently loses the scheduler.
- Grant `gitlab-runner` only the minimum extra privilege required.

**Non-Goals:**
- Scheduler for the local Docker Compose stack.
- Hardening existing tasks (`withoutOverlapping()`, `onOneServer()`, output logging).
- Staging/production environments (the layout allows one cron file per env later; not done now).
- Replacing the `sudo rm -rf` already in CI.

## Decisions

### D1 — cron + `schedule:run` every minute (the Laravel-documented approach)
`* * * * *` calling `schedule:run` is the standard: cron is only the heartbeat; which task runs when is decided in `Kernel::schedule`. `schedule:run` is idempotent per minute — non-due tasks exit in milliseconds.
*Alternatives*: `schedule:work` (Laravel ≥ 8, unavailable); systemd timer (works, but non-standard for Laravel and adds nothing on a dev box).

### D2 — the cron job runs as `www-data`
`storage/logs` and `storage/framework/cache` are written by both Apache (per request) and the scheduler. With the `daily` log channel, whichever process first writes on a given day creates `laravel-YYYY-MM-DD.log` and fixes its owner and mode. If cron ran as `root`, `gitlab-runner` or `lent`, the file would be owned by that user with mode 644 (cron umask 022); the setgid bit only guarantees the group, not group write, so Apache would get "Permission denied" → HTTP 500 for the rest of the day. Running as `www-data` makes both writers the same user and removes the conflict at the root. `www-data`'s `nologin` shell is irrelevant: cron executes jobs with its own `SHELL=/bin/sh`.
*Alternatives*: run as another user plus `umask 002`/`chmod` fix-ups — fragile, and this repo has already shipped one deploy fix for exactly this class of permission bug.

### D3 — `/etc/cron.d/tnx-pos-dev`, not a user crontab
A per-environment file in `/etc/cron.d` has a user column, is a plain file CI can install, is trivially diffable against the repo, and stays out of any human's personal crontab. Constraints of `cron.d` that the implementation must honour: owner `root:root`, mode `644`, file name without dots, trailing newline, LF line endings. Violations are silent (cron logs to syslog and skips the file).
*Alternatives*: `crontab -u www-data` (manual, invisible to the repo, lost on server rebuild); `gitlab-runner`'s own crontab (no sudo needed but violates D2).

### D4 — CI installs the file with a single `sudo install`
`sudo install -m 644 -o root -g root deploy/cron.d/tnx-pos-dev /etc/cron.d/tnx-pos-dev` performs copy + owner + mode atomically in one command, is idempotent, and overwrites on every deploy so the server never drifts from the repo.
Granting `gitlab-runner` write access to `/etc/cron.d` via group/ACL was considered and rejected: Debian cron requires files in `/etc/cron.d` to be **owned by root**; a file created by `gitlab-runner` is skipped with `WRONG FILE OWNER` in syslog. Root is therefore mandatory, and `sudo` is the only route.
*Alternatives*: `sudo cp` + `sudo chown` + `sudo chmod` (three commands, three sudoers entries, non-atomic window with wrong mode); `sudo tee` (needs `chmod` afterwards).

### D5 — scoped sudoers rule
Rather than relying on whatever `sudo` grant the runner already has, the required grant is documented as an exact-command rule so it cannot be used to write any other file. It lives in `/etc/sudoers.d/gitlab-runner-tnxpos` next to the existing `rm -rf` rule:

```
gitlab-runner ALL=(root) NOPASSWD: /usr/bin/install -m 644 -o root -g root /var/www/html/tnx_pos_2026/dev/pos/src/deploy/cron.d/tnx-pos-dev /etc/cron.d/tnx-pos-dev
```

If `sudo -l -U gitlab-runner` already shows `NOPASSWD: ALL`, the rule is redundant but harmless; the README still records it as the minimum requirement.

### D6 — one file per environment, hard-coded path, no templating
The dev path is written literally into `deploy/cron.d/tnx-pos-dev`. When staging/prod arrive, each gets its own file (`tnx-pos-staging`, …) and its own CI line. This keeps the cron file byte-identical to what lands on the server (diffable, no `sed`/`envsubst` in CI) and keeps the sudoers rule exact.

### D7 — output redirected to `/dev/null`, per Laravel docs
`schedule:run` prints "No scheduled commands are ready to run." every minute; capturing it would only produce noise. Per-task output, when needed, belongs to `->appendOutputTo()` in `Kernel::schedule`, not to the cron line.

### D8 — pin LF line endings
`.gitattributes` gets `deploy/cron.d/* text eol=lf`. The runner checks out on Linux (LF by default), but a stray CRLF would make cron log `bad minute` and skip the entry — invisible to CI. The rule makes the failure impossible rather than unlikely.

## Risks / Trade-offs

- [Runner sudo grant is a whitelist that does not include `install`] → CI step fails loudly on the first deploy; the README documents the exact sudoers rule (D5) as a one-time manual server step.
- [Deploy window: `rm -rf src` runs before the new copy exists] → a cron tick during those seconds fails once (`cd` fails, nothing runs). Harmless; next minute recovers. No mitigation needed.
- [`.env` on dev not readable by `www-data`] → Apache already reads it under the same user, so if the site works, cron works. Verification step in tasks confirms with a manual `sudo -u www-data … schedule:run`.
- [Someone edits `/etc/cron.d/tnx-pos-dev` by hand on the server] → overwritten on next deploy. This is intended; the repo is the source of truth.
- [`summaryLogging()` is a heavy `INSERT … SELECT`] → runs once nightly at 17:00 UTC; no overlap concern at `daily()`. Revisit with `withoutOverlapping()` if the cadence changes.
- [Deploy pipeline holds root via sudo — provisioning and deployment are not separated, unlike Ansible/PaaS-managed setups] → Accepted deliberately (2026-09-15): no provisioning layer exists, the pipeline already holds a scoped `rm -rf` sudo grant, and the new `install` grant is exact-command and cannot write any other path. Exit path: when config management or a PaaS arrives, move the single CI `install` line there; `deploy/cron.d/` stays the source of truth. A no-root alternative (CI only `diff`s the server file against the repo and fails on drift; humans install) was considered and declined for now.
- [Reversa policy] → `.gitlab-ci.yml`, `.gitattributes`, `README.md` are pre-existing project files. `.reversa/reversa-config.json` currently sets `allowLegacyEdits: true` with empty `allowedPaths` (unrestricted). Re-read it at apply time; do not modify it.

## Migration Plan

1. Merge the change; tag `dev-<10 digits>` to trigger `pos-dev`.
2. If the CI `install` step fails with a sudo error, add the D5 sudoers rule on the server (manual, one-time) and re-run the job.
3. Verify on the server: `cat /etc/cron.d/tnx-pos-dev`, `journalctl -u cron -n 5` shows `(www-data) CMD (…)`, and `sudo -u www-data /usr/bin/php artisan schedule:run` in `src` prints "No scheduled commands are ready to run."
4. Rollback: `sudo rm /etc/cron.d/tnx-pos-dev` on the server and revert the CI line; no application state is touched.

## Open Questions

- ~~Exact current sudoers grant for `gitlab-runner`~~ — **Resolved 2026-09-15**: `sudo -l -U gitlab-runner` shows only `(root) NOPASSWD: /usr/bin/rm -rf /var/www/html/tnx_pos_2026/*`. The `install` grant (D5) is **not** present, so migration step 2 is required before the first deploy succeeds. The new rule follows the same exact-command style as the existing one.
