# Authorization cheat-sheet

Four layers, each doing one job. The server decides; the UI only reflects it.

| Layer | Where | Answers |
|---|---|---|
| 1. Route | `routes/web.php` | "May this user reach this URL at all?" (stops the request early) |
| 2. Policy | `app/Policies/*` | "May this user do THIS to THIS record?" (role + same shop + record state) |
| 3. Request / service | `FormRequest::authorize()`, `app/Services/*` | Re-checks on write; services enforce business rules (stock, credit limit, return window) |
| 4. UI flags | `Abilities::for(...)` → React | Hide buttons the server would refuse. Never the only check. |

`tests/Feature/AuthorizationTest.php` guards all of this: it **fails if a tenant route has no authorization**, checks a
role × endpoint matrix, the policies, and the flags sent to React.

## 1. Protecting routes

```php
// Record route → policy (role + shop + state). 'product' is the route parameter name.
Route::put('products/{product}', …)->can('update', 'product');
// List / create → class-level policy method
Route::get('products', …)->can('viewAny', Product::class);
// No model involved → plain permission
Route::get('pos', …)->middleware('permission:pos.access');
// Any of several
->middleware('permission:reports.view|stock.view');
// Platform admin only
->middleware('master');
```

Order matters: tenant middleware (`shop.context`, `shop.selected`) runs first, then route-model binding (so another
shop's id is a 404 before anything else), then `can`.

## 2. Policy classes

```php
class ProductPolicy
{
    use ChecksShopAccess;                      // sameShop(): the record belongs to the user's shop

    public function update(User $user, Product $product): bool
    {
        return $user->can('products.update')   // 1. does the ROLE allow it?
            && $this->sameShop($user, $product) // 2. is it MY shop's record? (second lock behind ShopScope)
            && ! $product->trashed();           // 3. record state
    }

    public function setPrice(User $user): bool { return $user->can('products.set_price'); }   // class-level
}
```

- Policies are auto-discovered (`App\Models\Product` → `App\Policies\ProductPolicy`).
- Use them anywhere: `Gate::authorize('update', $product)`, `$request->user()->can('update', $product)`,
  `@can`, `->can()` on routes, `FormRequest::authorize()`.
- **Master Admin** satisfies *permission* checks (`can('products.update')`) via `Gate::before`, but policy abilities
  (`update`, `refund`, …) still apply their rules to everyone.
- Outside an HTTP request (jobs, console, tests) set the shop first, because roles are per shop:
  `app(PermissionRegistrar::class)->setPermissionsTeamId($user->shop_id)`.

## 3. Sending answers to React

```php
// page-level (no record yet)
'abilities' => Abilities::forClass(Product::class, ['create', 'viewCost', 'setPrice']),
// record-level
'can' => Abilities::for($product, ['update', 'delete', 'restore', 'adjustStock']),
```

## 4. Hiding buttons in React

```jsx
// Record-level: `can` came from the server's policy
{product.can.update && <Link href={route('products.edit', product.id)}>Edit</Link>}

// Page-level
{abilities.create && <Link href={route('products.create')}>New product</Link>}

// Same thing as a component, with a fallback
<Can ability="pay" on={supplier} fallback={<span>Owner only</span>}><PayButton /></Can>

// Coarse checks (navigation, headings) can use the user's permissions directly
<Can permission="reports.view"><NavLink href={route('reports.index')}>Reports</NavLink></Can>
const { can, hasRole } = usePermission();   // Hooks/usePermission.js
```

Rule of thumb: **navigation** uses `can('permission')`; **buttons on a record** use `record.can.ability`.

The top-bar menu (groups, items and the permission each needs) is defined once in `resources/js/Layouts/nav.js`.
`tests/Feature/NavigationTest.php` reads that file and checks every item against the server, so a dead link or a
permission that disagrees with the route fails the build.

## Adding a feature: checklist

1. Add the permission to `App\Support\Access` (and to the roles that should have it); re-run `db:seed --class=RolesAndPermissionsSeeder` on existing shops.
2. Add/extend the model's policy.
3. Protect each route with `->can(...)` or `permission:`; the guard test fails until you do.
4. Use the same policy in the FormRequest's `authorize()`.
5. Send `abilities` / `can` to the page and gate buttons with them.
6. Add a row to the matrix in `AuthorizationTest.php`.

## Permission map

| Permission | Owner | Cashier | Stock mgr |
|---|:-:|:-:|:-:|
| products.view / create / update / delete | ✓ | view | ✓ |
| products.set_price (sale price) | ✓ | | |
| categories.manage, stock.view, stock.adjust, purchases.manage | ✓ | | ✓ |
| suppliers.view / manage | ✓ | | ✓ |
| suppliers.pay (money out) | ✓ | | |
| customers.view / manage / collect | ✓ | ✓ | |
| customers.set_credit | ✓ | | |
| pos.access, sales.view / create | ✓ | ✓ | |
| sales.discount, sales.refund | ✓ | | |
| reports.view, shop.settings, staff.manage | ✓ | | |

Single source of truth: `app/Support/Access.php`.
