# Customers Statistics — Contracts

> Produced by the Reversa **Writer** (phase: generation) · doc_level: `complete`
> Generated on 2026-09-21

**Confidence scale:** 🟢 CONFIRMED · 🟡 INFERRED · 🔴 GAP

External HTTP contract exposed by the `customers-statistics` unit — the single route `GET /customers/{customer}/statistic` (`CustomerController::statis`, route name `customers.statis`) under the admin group (`['web','admin']`, empty admin prefix). It is a read-only **HTML** reporting page (not a JSON endpoint) reached from the customer back office; it requires an authenticated admin session; an unauthenticated request gets `302 → auth/login`. 🟢 (`routes/web.php:57`, `CustomerController.php:215-261`)

> This route is declared **before** `resource('/customers', 'CustomerController')` (`routes/web.php:62`), so `/customers/{customer}/statistic` is not captured by the resource routes. The resource CRUD belongs to `customers-crud`; the sibling `/customers/{customer}/*` screens (`orders`, `debt`, `check-gift`, …) belong to `customers-purchase-history`, `customers-debt-actions`, and `customers-loyalty`. 🟢 (`routes/web.php:53-62`)

## Exposed contract

### `GET /customers/{customer}/statistic` 🟢

| Aspect | Value |
|--------|-------|
| Route name | `customers.statis` |
| Middleware | `['web','admin']` (authentication only; no per-record authorization) |
| Path param | `customer` — the customer id (integer) |
| Query param | `page` — optional, paginates the annotated-orders panel (10/page) |
| Success | `200 OK`, `Content-Type: text/html` — the rendered `pages.customer-statis` |
| Not found | `404` when the id is unknown or soft-deleted (`Customer::findOrFail`) |
| Unauthenticated | `302 → auth/login` |
| Side effects | **None** — pure read; triggers no summary rebuild |

**Request example**

```
GET /customers/42/statistic?page=2
Cookie: <admin session>
```

**Response:** an HTML page. It is not a machine contract; the *data* rendered into it is the contract of interest, enumerated below.

## Rendered data contract (view model)

The page binds these values (see `design.md` → Interface for provenance). A reimplementation exposing the same screen must supply the same fields with the same semantics. 🟢

| Field | Meaning | Source | Freshness |
|-------|---------|--------|-----------|
| `item.orders_count` | Lifetime number of orders | summary `orders_count ?? 0` | nightly (stale ≤ ~24 h) |
| `amountTotal` | Lifetime amount spent (Σ order `total`) | summary `amount_total ?? 0` | nightly |
| `pointsTotal` | Lifetime points earned (Σ `earned_point`) | summary `points_total ?? 0` | nightly |
| `item.points` | **Current** spendable reward-points balance | live customer | live |
| `debtTotal` | **Current** debt balance | live `customer.debt_total ?? 0` | live |
| `statisticsUpdatedAt` | When the summary was last rebuilt (or `null`) | summary `updated_at` | — |
| `attrStatistic` | Milk (category id 1) per-weight items: `{attr_weight, qty_total, amount_total}` | `categories_statistic[1].items` | nightly |
| `medicineStatis` | Medicine (category id 8) per-weight items: `{attr_weight, qty_total, amount_total}` | `categories_statistic[8].items` | nightly |
| `goodsStatis` | Other categories aggregated: `{name, qty_total, amount_total}`, name-sorted | remaining `categories_statistic` entries | nightly |
| `orders` | Paginator of this customer's **annotated** orders (`notes` not null), newest first, 10/page | live `Order` query | live |

**Degradation contract:** if the summary row is absent → all summary-sourced fields are `0` / empty and `statisticsUpdatedAt` is `null`. If `categories_statistic` is malformed → `attrStatistic`, `medicineStatis`, `goodsStatis` are all empty while the headline totals still render. In both cases the response is still `200`. 🟢 (`CustomerController.php:226-256`)

## Consumed contracts (owned by other units)

This screen renders links/actions that call endpoints owned elsewhere; they are **not** part of this unit's contract and are documented here only as dependencies: 🟢 (`customer-statis.blade.php:28-31,56-57,183,215,319-386`)

| Consumed endpoint | Purpose on this page | Owning unit |
|-------------------|----------------------|-------------|
| `GET /customers/{id}` (edit) — `customers.edit` | "Basic info" tab link | `customers-crud` |
| `GET /customers/{id}/orders` — `customers.orders` | "Purchased orders" tab link | `customers-purchase-history` |
| `GET /customers/{id}/debt` — `customers.debt` | "Debt" tab link | `customers-debt-actions` |
| `GET /customers/{id}/gift-received` — `customers.gift-received` | "Gifts received" link | `customers-loyalty` |
| `POST /customers/{id}/redeem-points` — `customers.redeem-points` | Redeem-points modal submit | `customers-loyalty` |
| `GET /customers/{id}/check-gift` — `customers.check-gift` | Gift-availability check in modal | `customers-loyalty` |
| `GET /settings/gifts/scan?active=1` | Gift autocomplete in redeem modal | `gifts-scan` |
| `PUT /orders/{id}/note` — `orders.update_note` | Inline note edit on annotated orders | `orders-note` |

## Upstream data dependency

The entire headline + category contract is populated by `Order::summaryLogging()` — a nightly `->daily()` scheduled rebuild and the on-demand `php artisan customer-summary:logging` command — writing to `customer_order_summary`. This screen is a **pure consumer** of that table and never writes it. If the rebuild has never run for a customer, this endpoint returns a valid but all-zero page. 🟢 (`Order.php:69-181`, `app/Console/Kernel.php:30-32`, `CustomerOrderSummaryLogging.php:34-38`)

## Error & status summary

| Condition | Status | Body |
|-----------|--------|------|
| Valid id, authenticated | `200` | HTML page (data possibly all-zero if uncomputed) |
| Unknown / soft-deleted id | `404` | Laravel not-found page |
| Unauthenticated | `302` | redirect to `auth/login` |
| Malformed `categories_statistic` | `200` | HTML page, category tables empty, totals intact |

## Contract gaps

- 🔴 No response signal distinguishes "summary not yet computed" from "genuinely zero" — a consumer/observer cannot tell a broken nightly job from a new customer.
- 🟢 The medicine category id (`8`) is a code-marked `placeholder` (`Order.php:25`) — already documented as a known, deferred-to-business limitation in `openspec/changes/archive/2026-08-27-customer-statis-and-order-filter/design.md`, not a new gap requiring confirmation here.
- 🟡 No per-record authorization on the route — any authenticated admin can read any customer's statistics (ADR-0009).
