# Implementation Plan: Inventory Sync Button (046)

**Branch**: `046-inventory-sync-button` | **Date**: 2026-01-08 | **Spec**: `specs/046-inventory-sync-button/spec.md`
**Input**: Feature specification from `specs/046-inventory-sync-button/spec.md`

## Summary

Add a “Run Inventory Sync” action in the Inventory area that dispatches a queued job, records a new `InventorySyncRun` attributed to the initiating user, and provides visibility via:

- Filament database notifications
- Existing bottom-right progress widget (powered by `BulkOperationRun` + `BulkOperationProgress`)

## Technical Context

**Language/Version**: PHP 8.4.15  
**Primary Dependencies**: Laravel 12, Filament v4, Livewire v3  
**Storage**: PostgreSQL (JSONB used for run payload + error context)  
**Testing**: Pest v4 (Laravel test runner)  
**Target Platform**: Containerized (Laravel Sail locally), deployed via Dokploy  
**Project Type**: Web application (Laravel + Filament admin panel)  
**Performance Goals**: UI action should enqueue quickly (<2s perceived) and never block on Graph calls  
**Constraints**: Tenant-isolated, idempotent start semantics, observable background execution, queue workers may be down  
**Scale/Scope**: Single-tenant interactive action; one run per tenant/selection at a time (locks/concurrency)

## Constitution Check

*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.*

- Inventory-first: This feature refreshes Inventory (“last observed”) and does not touch snapshots/backups.
- Read/write separation: No Graph writes. Only reads + local DB writes (run records + inventory items).
- Graph contract path: Inventory sync already uses `GraphClientInterface`; this feature does not add new Graph endpoints.
- Deterministic capabilities: Uses `PolicyTypeResolver` + `config('tenantpilot.supported_policy_types')` to derive default selection.
- Tenant isolation: Uses `Tenant::current()` for all run creation and progress tracking.
- Automation: Uses existing inventory locks/concurrency; runs are observable via `InventorySyncRun` and `BulkOperationRun`.
- Data minimization: Inventory sync continues to store metadata + sanitized meta JSON; no secrets in notifications/logs.

## Project Structure

### Documentation (this feature)

```text
specs/046-inventory-sync-button/
├── plan.md
├── research.md
├── data-model.md
├── quickstart.md
├── contracts/
│   └── internal-actions.md
└── tasks.md            # Phase 2 output (/speckit.tasks) - not created here
```

### Source Code (repository root)

```text
app/
├── Filament/
│   └── Pages/
│       └── InventoryLanding.php              # add header action “Run Inventory Sync”
├── Jobs/
│   └── RunInventorySyncJob.php               # new queued job
├── Models/
│   ├── InventorySyncRun.php                  # add user relationship (nullable)
│   └── BulkOperationRun.php                  # reused by progress widget
├── Services/
│   ├── BulkOperationService.php              # reused for progress + audit
│   ├── Intune/
│   │   └── AuditLogger.php                   # reused for inventory audit trail
│   └── Inventory/
│       └── InventorySyncService.php          # add entrypoint to run sync attributed to user

database/migrations/
└── xxxx_xx_xx_xxxxxx_add_user_id_to_inventory_sync_runs_table.php

resources/views/
└── filament/pages/inventory-landing.blade.php # may need to show action if not already in header

tests/Feature/
└── Inventory/InventorySyncButtonTest.php     # new Pest feature test
```

**Structure Decision**: Web application, implemented in Filament page actions + Laravel queued job.

## Phase 0 Output (Research)

Completed in `specs/046-inventory-sync-button/research.md`.

Key decisions used in this plan:
- Reuse existing bottom-right progress widget by creating a `BulkOperationRun` (`resource=inventory`, `action=sync`).
- Authorize using existing tenant role capability: `User::canSyncTenant(Tenant::current())`.
- Default selection = “full inventory” (all supported policy types, foundations + dependencies enabled).

## Phase 1 Output (Design)

Completed in:
- `specs/046-inventory-sync-button/data-model.md`
- `specs/046-inventory-sync-button/contracts/internal-actions.md`

## Phase 2 Plan (Implementation Outline)

### 1) Data model: store initiator on run record

- Add nullable `user_id` foreign key to `inventory_sync_runs`.
- Add `InventorySyncRun::user()` relationship.
- Backfill is not required (existing rows can remain `null`).

### 2) Job orchestration: queued execution + progress tracking

- Add `RunInventorySyncJob` that:
  - Loads the tenant + user + `BulkOperationRun`.
  - Marks the `BulkOperationRun` as running (`BulkOperationService::start`).
  - Runs inventory sync via `InventorySyncService` in a way that sets `InventorySyncRun.user_id`.
  - Records bulk progress as a single-item operation:
    - success: `recordSuccess` + `complete`
    - failure/skip: `recordFailure`/`recordSkippedWithReason` + `complete` (or `fail` for hard failures)
  - Sends Filament database notifications to the initiating user on completion/failure.

### 3) UI: Inventory “Run Inventory Sync” action

- Add a header action on `InventoryLanding`:
  - Visible/authorized only if `auth()->user()` is a `User` and `canSyncTenant(Tenant::current())`.
  - On click:
    - Compute default selection payload.
    - Pre-flight check: if a matching `InventorySyncRun` is currently `running` (same tenant + selection hash), show an informational notification and do not dispatch.
    - Create a `BulkOperationRun` via `BulkOperationService::createRun(...)` with `total_items=1`.
    - Send a “started” Filament DB notification with the “check bottom right progress bar” copy.
    - Dispatch `RunInventorySyncJob` with identifiers.

### 4) Audit trail

- Use `AuditLogger` to emit at minimum:
  - `inventory.sync.dispatched`
  - terminal event keyed by outcome: `inventory.sync.completed|inventory.sync.failed|inventory.sync.skipped`

### 5) Tests (Pest)

- New feature test covering:
  - authorized user can dispatch a sync from the Inventory page and it creates a `BulkOperationRun`.
  - `RunInventorySyncJob` sets `InventorySyncRun.user_id`.
  - unauthorized user cannot see/execute the action.
  - concurrency case: when a run is already `running`, a second dispatch is prevented.
