# Backend changes the front end needs to act on

**For:** whoever maintains the Annkut 2026 front end
**Backend version:** migration `20260915110000`
**Full reference:** [`API_CONTRACT.md`](API_CONTRACT.md) · helper client: [`annkut-api-client.js`](annkut-api-client.js)

This is the short list of what changed and what you have to do about it. Most
of it is new capability you can take at your own pace. Only **item 3** asks you
to remove something, and it is one button.

---

## At a glance

| # | Change | Do you have to act? |
|---|---|---|
| 1 | New endpoint: find a receipt book by its cover number | Only if you build the admin book screen |
| 2 | New filter: Yuva Pravrutti | Only if you add the switch |
| 3 | **Leadership accounts are read-only** | **Yes — hide the receipt-book management button** |
| 4 | `login/me` is fixed on production | Yes, if you wrote a workaround |
| 5 | CORS is config-driven now | Only when you add a new front end URL |

---

## 1. New — find a book by its cover number

`POST receiptbooks/lookup`

This backs the admin screen where someone types a book number, presses search,
and reads back everything written in that book.

```json
{ "book_no": "341" }
```

```json
{
  "status": true,
  "book": {
    "id": 37, "book_no": "341", "status": "AVAILABLE",
    "mandal_name": "Pritam Nagar", "area_code": "BH02",
    "parivar_code": null,
    "start_no": 1, "end_no": 25,
    "last_used_no": 25, "next_receipt_no": 26, "remaining": 0
  },
  "receipts": [
    {
      "id": 54, "receipt_no": 1,
      "sahyogi_surname": "patel", "sahyogi_first_name": "kavy",
      "sahyogi_middle_name": "A", "sahyogi_number": "9925494690",
      "seva_amount": "500.00", "prasad_type": "annkut_sevak",
      "payment_method": "cash",
      "collected_by_code": "RKPT001", "collected_by_name": "Patel Amitkumar Ashwinbhai",
      "collected_at": "2026-09-12 08:32:29"
    }
  ],
  "summary": {
    "pages": 25, "filled": 25, "remaining": 0,
    "collected_amount": 15300, "unused_numbers": []
  },
  "can_edit": true
}
```

Three things worth knowing:

- **`receipts` is in page order** (receipt_no ascending), not date order. People
  write receipts up out of sequence, so date order would not match the physical
  book the person is holding.
- **`unused_numbers`** are pages below the high-water mark with nothing recorded
  — torn out, spoiled or voided. That is what someone reconciling a returned
  book is hunting for. Show them.
- **`can_edit` is per book, not per user.** It already accounts for the book's
  mandal being in the caller's scope, which you cannot work out from the login
  profile. Drive the row's Edit button off this flag and nothing else.

**Errors:** `404` = no book anywhere carries that number (a typo).
`403` = the book exists but belongs to a mandal outside your scope. They mean
different things to the person searching — show different messages.

Works on `CLOSED` books: a finished book still has to be readable.

### The Edit action needs no new endpoint

Use the existing `POST seva/edit_seva`:

```json
{ "seva_id": 54,
  "sahyogi_surname": "Patel", "sahyogi_first_name": "Kavy",
  "sahyogi_middle_name": "A", "sahyogi_number": "9925494690" }
```

It rebuilds the composed display name from the parts and re-links the sahyogi
record when the number changes. It also accepts `seva_amount`, `prasad_type`,
`payment_method`, `notes` and `receipt_no` if you want more of the row editable
later. **ADMIN only** — everyone else gets 403.

---

## 2. New — the Yuva Pravrutti filter

One key, the same on both endpoints:

```js
const f = yuvaOnly ? { pravrutti: "YUVA" } : {};

api.summary({ ...f });                   // mandals page: totals AND cards
api.sevaks({ mandal_id: 7, ...f });      // drill-in: the sevak list
```

**Send it to both calls.** On `seva/get_seva_count` it narrows the headline
figures *and* every mandal row, so the page cannot end up showing yuva totals
above whole-mandal cards. On `sevak/get_sevak` it narrows the list and `total`,
so paging stays correct.

`get_seva_count` echoes back what the filter resolved to:

```json
{ "pankh_filter": ["YK", "YT"], "total_filled_form": 1, ... }
```

Label the screen from that, not from your own copy of the rule.

> **Use `pravrutti: "YUVA"`, not `pankh: ["YK","YT"]`.** The grouping is stored
> on the pankh row in the database. If the Yuva Pravrutti ever covers a
> different set of pankh, the server follows and your app needs no redeploy.
> `pankh` still works and takes one code (`"M"`) or a list, for other slices.

### ⚠️ One trap: Target changes meaning under the filter

`filled_forms`, `collected_amount`, `sevaks` and `parivars` are simply
narrowed. **`target_forms` changes source.** Unfiltered it is the mandal's own
target; filtered it is the sum of the matching sevaks' individual targets,
because a mandal target is a single number with no breakdown by pankh.

The two will not add up to each other, and that is correct — they answer
different questions. Don't show them side by side as if they were comparable.

---

## 3. Leadership accounts are read-only — hide the receipt-book button

**What changed:** `add_seva` used to authorise a receipt on *scope* alone.
Anyone who could **see** a mandal could **write** in it. That gave all 14 Sant
and Agresar karyakars write access across their whole xetra, including booking
receipts against other people's accounts. Seeing a mandal and writing in it are
different rights, and the backend now separates them.

**Who is affected — 14 accounts:**

| Post | Accounts |
|---|---|
| Kothari | `SNBH001` |
| Sant Nirdeshak | `SNBH002` |
| Nirdeshak | `AGSP001` `AGSP002` `AGSP003` |
| Sah Nirdeshak | `AGSP004`–`AGSP011` |
| Yuva Nirdeshak | `AGYP011` |

`AGSP011` is the "TBC Placeholder" account the import created for a leadership
row that listed mandals (`UM, KR, KJ, OS`) but no name — it is a real account
with real scope, so treat it like the rest until someone replaces it.

These people hold the **widest read scope in the organisation** — the Kothari
sees all 42 mandals — and **zero write permissions**. `SNBH001` also lost the
ADMIN post; `ADMIN26` is now the only administrator.

**What you actually need to change: hide the receipt-book management
control for these accounts.** They do not need it.

**What does *not* change, despite the above.** These posts have never held any
permission, so Edit Sevak, Reset Password, Void Seva and every book action were
*already* returning 403 for them. The only behaviour that changed is writing a
seva entry into a book belonging to a mandal they merely oversee — and no such
book is reachable from the UI, because all 14 of these accounts currently hold
**zero receipt books** (7 have no parivar at all; the other 7 have a family with
no book issued). Their Add Seva dropdown is empty either way.

So in practice: one button to hide, nothing that silently starts failing.

**Do it by permission, not by a hard-coded list of Sevak IDs.**
The login profile already carries everything you need:

```js
import { can, PERM } from "./api/annkut";

// The receipt-book management screen / button - the one to hide.
// Gate the whole entry point on this:
can(sevak, PERM.BOOK_ASSIGN) || can(sevak, PERM.BOOK_MANAGE)

// Individual actions inside it, if you show them separately:
can(sevak, PERM.BOOK_ASSIGN)   // Assign / Deassign        ADMIN + SANCHALAK
can(sevak, PERM.BOOK_MANAGE)   // Add / Submit / Transfer / Delete   ADMIN

// Elsewhere:
can(sevak, PERM.SEVAK_EDIT)    // Edit a sevak row         ADMIN + SANCHALAK
can(sevak, PERM.SEVA_MANAGE)   // Edit / Void a seva entry ADMIN only
can(sevak, PERM.SEVA_CREATE)   // Add Seva for a mandal you oversee
```

A hard-coded list of Sevak IDs would go stale the moment someone is appointed
or steps down. `can()` reads the permissions the server sent with the login
profile, so it follows the database on its own — and returns true for admins
automatically.

### If the button is still showing, check this first

The backend already sends what you need. Log in as one of the 14 and run this
in the browser console:

```js
const me = (await api.me());          // or whatever you store the profile in
console.log(me.access);
// expected for all of them except SNBH001:
//   { is_admin: false, permissions: [], mandal_count: 7, ... }
```

If `permissions` is `[]` and the button is still on screen, the component is
not reading it — it is rendering unconditionally, or gating on something like
"is the user logged in". That is the bug. The check is:

```js
{(can(sevak, PERM.BOOK_ASSIGN) || can(sevak, PERM.BOOK_MANAGE)) && (
   <ManageReceiptsButton />
)}
```

**`SNBH001` is the one real exception.** Until the backend is deployed he still
holds the ADMIN post on production, so `is_admin` is `true` and the button
appears correctly. Test with any of the other 13 to see the true picture.

There is one **new** permission code, `seva.create` (ADMIN + SANCHALAK). Pull
the latest [`annkut-api-client.js`](annkut-api-client.js) — it is already in
`PERM`, along with `YUVA_PRAVRUTTI` and `findBook()` for items 1 and 2.

**Not affected:** a sevak writing in their **own family's** book. That is the
ordinary collection flow, it needs no permission, and it is untouched.

> Permissions drive **button visibility only**. The server re-checks every
> request and also verifies the mandal is in scope, so a hidden button is a
> courtesy, not the security boundary.

---

## 4. `login/me` is fixed — remove any workaround

`login/me` was returning **401 "Authentication required"** on production while
`sevak/get_mandal_list` and `seva/get_seva_count` worked with the same token.

It was a backend bug, not yours. `login/me` had its own private copy of the
"read the Bearer token" logic that only checked one of the places Apache can
put the header. The three endpoints affected were `login/me`, `login/logout`
and `login/change_password`.

Two consequences:

- **Session restore on page reload works now.** If you built a workaround —
  caching the profile, or skipping the `me` call — you can drop it.
- **`login/logout` actually revokes the token now.** It previously returned
  `{"status": true, "message": "Signed out."}` while leaving the token valid.
  Anyone who signed out on the live site was not signed out.

---

## 5. CORS is configuration now

Allowed origins live in `application/config/cors.php` on the server, not in
code. Currently allowed:

- `https://parivarid-changes.dtbkfrslkiavw.amplifyapp.com`
- `https://master.d3lfyncl45ukvr.amplifyapp.com`
- **any branch** of those two Amplify apps — `https://<branch>.<appid>.amplifyapp.com`
  is matched by suffix, so a new branch works without a backend change
- in local development only, `http://localhost:<any port>` and
  `http://127.0.0.1:<any port>`

If you deploy the front end to a **new domain**, tell the backend maintainer to
add it. Nothing else is needed for new Amplify branches.

Two notes: a browser sends `Origin` with no trailing slash, so an entry written
`https://host/` can never match. And localhost is allowed **only** outside
production — the live server refuses it deliberately, because the API sends
`Access-Control-Allow-Credentials` and a page loaded from a developer's own
machine could otherwise make signed-in calls against live data.

---

## Still true, still the most common bug

**MySQL returns numbers as strings.** `target_forms` is `"0"`, `id` is `"359"`.
Only values the PHP computes itself (`total`, `total_target`, `year`, the
`seva_*` counters, and everything inside `parivar.members[]`) are real numbers.

```js
Number(s.target_forms) - Number(s.filled_forms)   // correct
s.target_forms - s.filled_forms                   // works by luck
s.id === 359                                      // false — it is "359"
```

Use the `num()` helper in [`annkut-api-client.js`](annkut-api-client.js).

---

## Suggested order

1. **Item 3** — hide the receipt-book button and move any other write controls
   onto `can()`. Small, and it stops these users seeing a screen that would
   only refuse them.
2. **Item 4** — delete the `login/me` workaround, confirm session restore.
3. **Item 1** — the admin book screen.
4. **Item 2** — the Yuva Pravrutti switch.
5. **Item 5** — nothing to do until you move domain.
