## 1. Database

- [x] 1.1 Add migration: create `lunch_payment_invoices` (`id`, `from` date, `to` date, `company_id` unsignedBigInteger nullable, `department_id` unsignedBigInteger nullable, `created_by_id` unsignedBigInteger, timestamps).
- [x] 1.2 Add migration: create `lunch_payment_invoice_items` (`id`, `invoice_id` unsignedBigInteger FK on `lunch_payment_invoices` cascade delete, `account_id` unsignedBigInteger, `total_amount` decimal(12,2), `number_orders` unsignedInteger, `is_paid` boolean default false, `paid_by_id` unsignedBigInteger nullable, `paid_at` timestamp nullable, timestamps).

## 2. Models

- [x] 2.1 Add `LunchPaymentInvoice` model (table prefix via `config('lunch_ordering.table_prefix')`, matching existing model style) with `items()` (hasMany `LunchPaymentInvoiceItem`), `created_by()` relations, and a `scopeOverlapping($query, $from, $to, $companyId, $departmentId)` — see task 8.1 for the corrected containment-based (not exact-match) version.
- [x] 2.2 Add `LunchPaymentInvoiceItem` model with `invoice()`, `account()`, `paid_by()` relations.

## 3. Generate invoice endpoint

- [x] 3.1 Add `POST /api/v1/admin/lunch-payment-invoices` on a new `LunchPaymentInvoiceController`: validate `{from, to, company_id?, department_id?}` with `to` required `date_format:Y-m-d|before_or_equal:today` and `from` required `before_or_equal:to`, gated by `lunch_orders_update` + `checkCompanyAccess`.
- [x] 3.2 Reject with a validation error if `LunchPaymentInvoice::overlapping(...)` finds a same-scope invoice whose range overlaps the requested one.
- [x] 3.3 Compute the per-employee snapshot: reuse the filtering shape from `LunchOrderController::ordersReport()` (active orders, `whereHas('booking_menu', ...)` date-range match, `filterByCompanies`, `company_id`/`department_id` scoping) grouped by `account_id`, selecting `count(*) as number_orders` and `sum(price) as total_amount`.
- [x] 3.4 Create the `lunch_payment_invoices` row, then bulk-insert one `lunch_payment_invoice_items` row per employee from the snapshot (`is_paid` defaults false).
- [x] 3.5 Return the created invoice with its items loaded (each item including account info needed for display, e.g. `account:id,username` + profile).
- [x] 3.6 Add OpenAPI doc block, following the existing style in the package's controllers.

## 4. List / view endpoints

- [x] 4.1 Add `GET /api/v1/admin/lunch-payment-invoices` (list, gated by `lunch_orders_index`): return invoices ordered by `from` desc, with a per-invoice summary (`items_count`, `total_amount` sum, `paid_count`).
- [x] 4.2 Add `GET /api/v1/admin/lunch-payment-invoices/{id}` (detail, gated by `lunch_orders_index`): return the invoice with all items (account info + `total_amount` + `is_paid`).
- [x] 4.3 Add OpenAPI doc blocks for both.

## 5. Mark paid / delete endpoints

- [x] 5.1 Add `PUT /api/v1/admin/lunch-payment-invoices/{invoiceId}/items/{itemId}` (gated by `lunch_orders_update`): validate `{is_paid: boolean}`; when true, set `paid_by_id`/`paid_at` on that item; when false, clear both.
- [x] 5.2 Add `DELETE /api/v1/admin/lunch-payment-invoices/{id}` (gated by `lunch_orders_update`): delete the invoice (cascades to items).
- [x] 5.3 Add OpenAPI doc blocks for both.

## 6. Routes

- [x] 6.1 Register all `lunch-payment-invoices` routes under the existing `/admin` group in `packages/w3suga/lunch-ordering/routes/api.php`.

## 7. Verification

- [ ] 7.1 Manually exercise: generate an invoice for a past month, confirm item totals match `GET /admin/lunch-orders/report` for the same range at that moment; add a new order in that range afterward and confirm the invoice's totals do NOT change; mark/unmark an item paid; attempt to generate an overlapping invoice for the same scope (expect rejection) and for a different scope (expect success); attempt a future-dated `to` (expect rejection); delete an invoice and confirm the range is generatable again. **Not run** - no PHP/artisan available in this environment; needs a real run against a DB before merging.
- [x] 7.2 Run `openspec validate add-lunch-payment-invoices --strict` and fix any reported issues.

## 8. Fix: scope-hierarchy overlap check (backend review finding)

- [x] 8.1 Rewrite `LunchPaymentInvoice::scopeOverlapping()` from an exact `(company_id, department_id)` match to a containment check: `company_id = NULL` (either side) conflicts with everything; same `company_id` conflicts unless both sides have different, specific `department_id`s.
- [x] 8.2 In `LunchPaymentInvoiceController::store()`, when `department_id` is given, resolve/normalize `company_id` from the department itself (source of truth), and reject as invalid if an explicitly-passed `company_id` doesn't match.
- [x] 8.3 Serialize the overlap-check-then-insert critical section behind a MySQL advisory lock (`GET_LOCK('lunch_payment_invoice_generation', 10)` / `RELEASE_LOCK(...)` in a `finally`), so two concurrent requests for a conflicting scope cannot both succeed.
- [x] 8.4 On conflict, return the conflicting invoice's `id`/`from`/`to`/`company_id`/`department_id` in the error response (status 409) instead of a bare message.
- [x] 8.5 Add `company_id`, `department_id`, `from`, `to` filters to `GET /admin/lunch-payment-invoices` (list) - pagination via `limit`/`page` already existed.
- [ ] 8.6 Manually exercise the scenarios from spec.md's "Invoices cannot have overlapping date ranges..." requirement, including the whole-company-vs-department and all-companies-vs-specific-company cases. **Not run** - no PHP/artisan available in this environment.

## 9. Add: refresh an invoice before it's settled (FE request)

- [x] 9.1 Extract the "compute snapshot for an invoice's own from/to/scope and bulk-insert items" logic out of `store()` into a shared private `insertSnapshotItems(LunchPaymentInvoice $invoice)`, used by both `store()` and the new `refresh()`.
- [x] 9.2 Add `PUT /api/v1/admin/lunch-payment-invoices/{id}/refresh` (gated by `lunch_orders_update`): reject with 409 if the invoice has any item with `is_paid = true`; otherwise delete all existing items and re-insert a fresh snapshot via `insertSnapshotItems()`, keeping the invoice's id/from/to/scope unchanged.
- [x] 9.3 Register the route (`PUT /{id}/refresh`, matching the existing `{id}/leave-requests/refresh` convention) - no shadowing risk since no other route on this resource matches that path shape.
- [x] 9.4 Add OpenAPI doc block.
- [ ] 9.5 Manually exercise: refresh an invoice with no paid items and confirm totals reflect newly added/removed orders; mark one item paid then attempt refresh and confirm the paid item's amount is preserved while other items are recomputed. **Not run** - no PHP/artisan available in this environment.

## 10. Simplify scope to company-only; add invoice name and totals

- [x] 10.1 Drop `department_id` from `lunch_payment_invoices` - invoices are scoped by `company_id` only. Update `scopeOverlapping()` to a two-way `company_id = NULL` containment check with no department tier.
- [x] 10.2 Add `name` (required, set at generation via `store()`), `total_amount_sum`, and `number_orders_sum` columns to `lunch_payment_invoices`, kept in sync by `insertSnapshotItems()` after every generate/refresh.
- [x] 10.3 Add `has_unpriced_item` to `lunch_payment_invoice_items`, flagging a line where at least one snapshotted order had no price at generation time.
- [x] 10.4 Update design.md/spec.md to drop department-scoping language and reflect the corrected refresh semantics (preserve paid lines rather than reject-if-any-paid).

## 11. Add: search invoices by name

- [x] 11.1 Add a `name` query filter to `GET /admin/lunch-payment-invoices` (list): case-insensitive substring match against the invoice's `name`, wildcard-escaped.
- [x] 11.2 Add OpenAPI doc for the new `name` parameter.
