diff --git a/README.md b/README.md index dd05ab66d..2cd6773f6 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ before making a package-level licence statement. [![Latest release](https://img.shields.io/github/v/release/Posnic/POS?include_prereleases&label=latest&color=blue)](https://github.com/Posnic/POS/releases/latest) [![Tests](https://img.shields.io/badge/tests-9%2C000%2B%20passing-brightgreen)](docs/DEVELOPMENT.md#running-the-tests) [![Coverage](https://img.shields.io/badge/coverage-69%25%20statements-yellow)](docs/DEVELOPMENT.md#running-the-tests) -[![API](https://img.shields.io/badge/REST%20API-894%20endpoints-blue)](docs/API.md) +[![API](https://img.shields.io/badge/REST%20API-895%20endpoints-blue)](docs/API.md) [![Source licence](https://img.shields.io/badge/source%20licence-AGPL--3.0-blue)](LICENSE) [![Package notices](https://img.shields.io/badge/package%20notices-component%20licences-informational)](THIRD-PARTY-NOTICES.md) [![Platforms](https://img.shields.io/badge/platforms-Windows%20%7C%20macOS%20%7C%20Linux-lightgrey)](https://github.com/Posnic/POS/releases/latest) @@ -235,7 +235,7 @@ the free edition worse. This is written down in [GOVERNANCE.md](docs/GOVERNANCE. | [Contributor quickstart](docs/CONTRIBUTOR_QUICKSTART.md) | Local setup, test commands, issue map, PR flow and safety rules | | [Developer guide](docs/DEVELOPMENT.md) | Setup, tests, conventions, good first issues | | [Architecture](docs/ARCHITECTURE.md) | How it fits together, and the parts that bite | -| [REST API](docs/API.md) | 894 endpoints, generated from the routes | +| [REST API](docs/API.md) | 895 endpoints, generated from the routes | | [Hardware](docs/HARDWARE_MATRIX.md) | Printers, scanners, drawers, scales — and how far each claim is checked | | [India e-invoicing](docs/INDIA_EINVOICING_DESIGN.md) | Research, readiness inventory and design for GST e-invoicing as an optional feature; no live IRP submission is built | | [India GST return gaps](docs/INDIA_GST_RETURNS_GAPS.md) | What the GST reports compute today, measured against GSTR-1, 3B and 9 | diff --git a/api/src/routes/mobile-pos.routes.js b/api/src/routes/mobile-pos.routes.js index d0dca1901..a3a15ba92 100644 --- a/api/src/routes/mobile-pos.routes.js +++ b/api/src/routes/mobile-pos.routes.js @@ -28,6 +28,29 @@ const limiter = rateLimit({ legacyHeaders: false, }); +// Discovery exposes only public display names, never branch/staff identifiers. +router.get( + '/discovery', + rateLimit({ windowMs: 60000, limit: 60 }), + wrap(async (req) => { + const rows = await req.db + .collection('branches') + .find( + { module_mobile_pos_enable: { $nin: [false, 0, '0', 'false'] } }, + { projection: { _id: 0, branch_name: 1 } } + ) + .limit(10) + .toArray(); + return { + shopName: [ + ...new Set(rows.map((row) => String(row.branch_name || '').trim()).filter(Boolean)), + ] + .join(' · ') + .slice(0, 160), + }; + }) +); + router.post( '/enrolment-proof', rateLimit({ windowMs: 60000, limit: 120, standardHeaders: true, legacyHeaders: false }), diff --git a/api/tests/api/rest-smoke.test.js b/api/tests/api/rest-smoke.test.js index 1d61e2025..5d709a23a 100644 --- a/api/tests/api/rest-smoke.test.js +++ b/api/tests/api/rest-smoke.test.js @@ -168,7 +168,9 @@ describeIfDb('the REST API, against a real database', () => { * branch would not save. It cannot be caught with a mock: the constraint * lives in MongoDB, not in the code. */ - const customers = client.db(DB_NAME).collection('customers'); + // Keep this deliberate legacy-index fixture separate from startup migrations, + // which normalize blank emails in the real customers collection asynchronously. + const customers = client.db(DB_NAME).collection('customers_legacy_index_fixture'); await customers.createIndex({ email: 1 }, { unique: true, sparse: true }); await customers.insertOne({ name: 'First', phone: '' }); diff --git a/api/tests/mobile-pos.integration.cjs b/api/tests/mobile-pos.integration.cjs index ccffa91d7..8364c6a1c 100644 --- a/api/tests/mobile-pos.integration.cjs +++ b/api/tests/mobile-pos.integration.cjs @@ -1304,3 +1304,9 @@ test('mobile snapshot sync carries disabled methods and still accepts a previous await db.collection('branches').replaceOne({ _id: branch._id }, savedBranch); } }); + +test('public mobile discovery exposes a shop name without credentials or internal identifiers', async () => { + const result = await call('/mobile/v1/discovery', undefined, null); + assert.equal(result.status, 200); + assert.deepEqual(result.data, { shopName: branch.branch_name }); +}); diff --git a/docs/API.md b/docs/API.md index 6ff9a51b0..c026ff575 100644 --- a/docs/API.md +++ b/docs/API.md @@ -3,7 +3,7 @@ Generated from `api/src/routes/` by `npm run docs:api`. Do not edit this file by hand; edit the routes and regenerate. -**894 endpoints across 53 route groups.** +**895 endpoints across 53 route groups.** All paths below are relative to the API root. On a desktop install the API runs on a port derived per installation (see `local-ports.js`); in the cloud it sits @@ -15,8 +15,8 @@ behind the tenant hostname. Postman, Insomnia or Bruno, point Swagger UI or Redoc at it, or generate a client from it. It is OpenAPI 3.0.3. -**104 of 894 endpoints carry a request schema** (12%), recovered from -their `express-validator` middleware. The remaining 790 are listed with +**104 of 895 endpoints carry a request schema** (12%), recovered from +their `express-validator` middleware. The remaining 791 are listed with path, method and handler but no body, because inventing a schema is worse than admitting the gap. @@ -730,6 +730,7 @@ Mounted at `/mobile/v1`. Source: `api/src/routes/mobile-pos.routes.js`. | Method | Path | Body documented | Handler | |---|---|---|---| +| GET | `/mobile/v1/discovery` | — | `Boolean` | | POST | `/mobile/v1/enrolment-proof` | — | `hex` | | POST | `/mobile/v1/pair` | — | `handsetLifetimeSeconds` | | GET | `/mobile/v1/inventory/policy` | — | `inventory.policy` | diff --git a/docs/openapi.json b/docs/openapi.json index 780ee3ed1..2d5099212 100644 --- a/docs/openapi.json +++ b/docs/openapi.json @@ -12953,6 +12953,29 @@ "description": "Request body is not yet documented. This endpoint has no validation middleware, so its shape cannot be derived from the source. Adding validation improves both runtime safety and this document." } }, + "/mobile/v1/discovery": { + "get": { + "summary": "Boolean", + "operationId": "get_mobile_v1_discovery", + "tags": [ + "Mobile pos" + ], + "responses": { + "200": { + "description": "Success" + }, + "400": { + "description": "Validation failed" + }, + "401": { + "description": "Authentication required" + }, + "500": { + "description": "Server error" + } + } + } + }, "/mobile/v1/enrolment-proof": { "post": { "summary": "hex", diff --git a/tests/public-routes.test.js b/tests/public-routes.test.js index 64a38d672..57e70be8a 100644 --- a/tests/public-routes.test.js +++ b/tests/public-routes.test.js @@ -77,6 +77,9 @@ const ALLOWED_ANONYMOUS = { '/discovery', ], 'mobile-pos.routes.js': [ + // Rate-limited discovery exposes only bounded public shop display names. + // No identifiers, credentials or business data; pairing still proves access. + '/discovery', // Pairing is a rate-limited authentication entry point with an expiring, // one-use secret; cloud grants also require the approved phone and proof. // Discovery proof returns only a nonce MAC for an unexpired random grant,