Add a POS integration handover
An API reference and rationale for whoever picks this up next. The endpoints are the easy half; what is not obvious from reading the code is why an ack is only published after the commit, why a duplicate counts as a success, and why is_delta false on a filtered catalogue empties a shop's shelf. Those are written down here because each one costs a shop money when someone changes it without knowing. Covers the GET endpoints in full — request parameters, real response shapes, and the cases that surprise people: values coming back as strings from a Redis hash, a quiet till answering 200 rather than 404, and a bill at another outlet returning 404 even though the reference is valid. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
458
POS_API.md
Normal file
458
POS_API.md
Normal file
@@ -0,0 +1,458 @@
|
|||||||
|
# POS integration — handover
|
||||||
|
|
||||||
|
Everything a developer needs to work on, extend or debug the in-store POS
|
||||||
|
integration. Companion to [`POS_TERMINAL_INGEST.md`](POS_TERMINAL_INGEST.md),
|
||||||
|
which covers deployment and broker setup.
|
||||||
|
|
||||||
|
Base path for everything below: **`/live/api/v1/pos`**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. What this is
|
||||||
|
|
||||||
|
Retail tills run a Flutter POS app. Each one holds its own SQLite database and
|
||||||
|
keeps working with no network at all. When a connection is available it
|
||||||
|
publishes completed bills to an MQTT broker; a consumer in this backend commits
|
||||||
|
them to Postgres and acknowledges.
|
||||||
|
|
||||||
|
```
|
||||||
|
Cashier completes sale
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
Till's SQLite ───────────────────── one transaction, before any network
|
||||||
|
sync_status = 0 survives crash, power cut, dead wifi
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
MQTT broker ──────────────────────── transit only, holds nothing you can rely on
|
||||||
|
nearle/pos/{loc}/{terminal}/order
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
fiesta consumer (messaging/posmqtt.go)
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
POSTGRES ─────────────────────────── the permanent record
|
||||||
|
pos_orders, pos_order_items
|
||||||
|
productstocks (stock deducted here)
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
ack → nearle/pos/{loc}/{terminal}/ack
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
Till marks it synced, keeps its copy 7 more days, then purges
|
||||||
|
```
|
||||||
|
|
||||||
|
**Health** takes a separate path: every till publishes a heartbeat every 30
|
||||||
|
seconds, which lands in **Redis** under a 90-second TTL. Never in Postgres.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. The four rules everything rests on
|
||||||
|
|
||||||
|
Break any of these and shops lose money. They are not stylistic.
|
||||||
|
|
||||||
|
**1. Only an application acknowledgement counts.**
|
||||||
|
A broker PUBACK means "I hold these bytes". It is not evidence the database
|
||||||
|
accepted anything. The ack is published *after* the transaction commits, never
|
||||||
|
from a handler that has merely queued the work.
|
||||||
|
|
||||||
|
**2. Silence is not acceptance.**
|
||||||
|
No ack, an empty ack, a 200 with no body — all leave the bill on the till, and
|
||||||
|
it is sent again. This is the correct behaviour when we are struggling.
|
||||||
|
|
||||||
|
**3. A duplicate is a success.**
|
||||||
|
Delivery is at-least-once. A lost ack makes a terminal re-send bills we already
|
||||||
|
hold. Reporting those as failures would strand a day of takings. Deduplication
|
||||||
|
is a unique index on `pos_orders.terminalorderid` — the UUID minted at the till
|
||||||
|
— plus a Postgres advisory lock held for the transaction.
|
||||||
|
|
||||||
|
**4. Store and terminal come from the topic, never the body.**
|
||||||
|
A till that could name its own store in a payload could redirect another
|
||||||
|
counter's acknowledgements.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Endpoints written by a terminal
|
||||||
|
|
||||||
|
These answer with a **bare body**, not the usual `{code, message, status}`
|
||||||
|
envelope — the till reads `accepted` from the top level and marks a bill synced
|
||||||
|
only if its id is there. Wrapping it would leave every terminal queueing for
|
||||||
|
ever.
|
||||||
|
|
||||||
|
### `POST /orders`
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"schema": 1,
|
||||||
|
"batch_id": "9f1c…",
|
||||||
|
"store_id": "1135",
|
||||||
|
"terminal_id": "T4A9",
|
||||||
|
"orders": [{
|
||||||
|
"id": "99999999-8888-4777-8666-555555555555",
|
||||||
|
"invoice_number": "INV-2608-T4A9-00002",
|
||||||
|
"created_at": "2026-08-03T17:29:00Z",
|
||||||
|
"cashier": "Divya",
|
||||||
|
"customer": {"id": "…", "mobile": "9840099999", "name": "Ravi"},
|
||||||
|
"subtotal": 60.0, "discount": 0.0, "tax": 4.44,
|
||||||
|
"tax_breakdown": {"0.08": 4.44},
|
||||||
|
"round_off": 0.0, "total": 60.0,
|
||||||
|
"points_earned": 0, "points_redeemed": 0,
|
||||||
|
"payments": [{"method": "upi", "amount": 60.0, "reference": "TXN123"}],
|
||||||
|
"items": [{
|
||||||
|
"product_id": "6988", "barcode": "6988", "name": "Mysore Banana",
|
||||||
|
"quantity": 1, "unit_price": 60.0, "discount": 0.0,
|
||||||
|
"gst_rate": 0.08, "tax": 4.44, "line_total": 60.0
|
||||||
|
}]
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Response:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "batch_id": "9f1c…", "accepted": ["99999999-…"], "rejected": {} }
|
||||||
|
```
|
||||||
|
|
||||||
|
Status codes carry the other half of the contract:
|
||||||
|
|
||||||
|
| Code | Meaning | Terminal does |
|
||||||
|
|---|---|---|
|
||||||
|
| `200` | batch processed; ack says which bills landed | marks the named ids synced |
|
||||||
|
| `4xx` | the request is wrong — unknown outlet, bad store id | **halts** and shows a person |
|
||||||
|
| `5xx` | outcome unknown | keeps everything, retries with backoff |
|
||||||
|
|
||||||
|
### `POST /customers`
|
||||||
|
|
||||||
|
Same envelope with a `customers` array. **Insert-if-absent on id** — never an
|
||||||
|
update, so a profile corrected at head office is not reverted by a terminal
|
||||||
|
replaying an old capture.
|
||||||
|
|
||||||
|
The id is a **UUIDv5 over the normalised ten-digit mobile**, so two tills
|
||||||
|
registering the same shopper independently produce the same row. Do not
|
||||||
|
reassign it.
|
||||||
|
|
||||||
|
No loyalty figures travel upward — points and spend are derived from the bill
|
||||||
|
stream, which is idempotent and sees every counter.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. GET endpoints — for the web app
|
||||||
|
|
||||||
|
**These use the normal `{code, message, status, details}` envelope.**
|
||||||
|
|
||||||
|
### `GET /sales` — bills for an outlet
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl "$BASE/sales?locationid=1135&fromdate=2026-08-01&todate=2026-08-03"
|
||||||
|
```
|
||||||
|
|
||||||
|
| Parameter | |
|
||||||
|
|---|---|
|
||||||
|
| `locationid` | **required** — the authorisation boundary |
|
||||||
|
| `fromdate`, `todate` | `YYYY-MM-DD`, matched on `businessdate` |
|
||||||
|
| `terminalid` | e.g. `T4A9` |
|
||||||
|
| `cashiername` | exact match |
|
||||||
|
| `paymentmode` | `cash`, `card`, `upi`, `wallet` |
|
||||||
|
| `pageno` | 0-based, default 0 |
|
||||||
|
| `pagesize` | default 50, max 500 |
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 200, "status": true,
|
||||||
|
"details": {
|
||||||
|
"total": 137, "pageno": 0, "pagesize": 50,
|
||||||
|
"bills": [{
|
||||||
|
"posorderid": 7,
|
||||||
|
"terminalorderid": "99999999-8888-4777-8666-555555555555",
|
||||||
|
"invoicenumber": "INV-2608-T4A9-00002",
|
||||||
|
"tenantid": 1087, "locationid": 1135,
|
||||||
|
"terminalid": "T4A9", "cashiername": "Divya",
|
||||||
|
"customerid": 6847, "customermobile": "9840099999", "customername": "Ravi",
|
||||||
|
"billedat": "2026-08-03T17:29:00Z",
|
||||||
|
"businessdate": "2026-08-03",
|
||||||
|
"subtotal": 60, "discount": 0, "taxamount": 4.44,
|
||||||
|
"roundoff": 0, "total": 60,
|
||||||
|
"pointsearned": 0, "pointsredeemed": 0,
|
||||||
|
"itemcount": 1, "paymentmode": "upi",
|
||||||
|
"paymentsjson": "[{\"method\":\"upi\",\"amount\":60,\"reference\":\"TXN123\"}]",
|
||||||
|
"promosjson": "[]",
|
||||||
|
"taxbreakdownjson": "{\"0.08\":4.44}",
|
||||||
|
"batchid": "batch-mqtt-0001",
|
||||||
|
"receivedat": "2026-08-03T17:29:11Z"
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Line items are **not** included — a page of 50 bills would drag hundreds of rows
|
||||||
|
behind it and a list screen shows none of them. Use `/sales/detail`.
|
||||||
|
|
||||||
|
Ordered by `billedat` descending, not by id: a backlog uploaded after an outage
|
||||||
|
arrives out of order, and sorting by arrival would interleave yesterday's bills
|
||||||
|
through today's.
|
||||||
|
|
||||||
|
### `GET /sales/detail` — one bill with its lines
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl "$BASE/sales/detail?locationid=1135&reference=INV-2608-T4A9-00002"
|
||||||
|
```
|
||||||
|
|
||||||
|
`reference` accepts **any of three**: the terminal's order UUID, the invoice
|
||||||
|
number, or the `posorderid`. A support call starts from whichever the caller
|
||||||
|
happens to be looking at.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 200, "status": true,
|
||||||
|
"details": {
|
||||||
|
"posorderid": 7,
|
||||||
|
"invoicenumber": "INV-2608-T4A9-00002",
|
||||||
|
"…": "all the fields above, plus:",
|
||||||
|
"items": [{
|
||||||
|
"posorderitemid": 12, "posorderid": 7,
|
||||||
|
"productid": 6988, "productname": "Mysore Banana",
|
||||||
|
"barcode": "6988", "unitname": "kg",
|
||||||
|
"quantity": 1, "unitprice": 60,
|
||||||
|
"discountamount": 0, "gstrate": 0.08,
|
||||||
|
"taxamount": 4.44, "linetotal": 60
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Returns **404** if the reference does not belong to that `locationid` — even
|
||||||
|
when the reference is a real bill at another outlet.
|
||||||
|
|
||||||
|
### `GET /sales/summary` — totals
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl "$BASE/sales/summary?locationid=1135&fromdate=2026-08-01&todate=2026-08-03"
|
||||||
|
```
|
||||||
|
|
||||||
|
Takes the same filters as `/sales`.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 200, "status": true,
|
||||||
|
"details": {
|
||||||
|
"locationid": 1135,
|
||||||
|
"fromdate": "2026-08-01", "todate": "2026-08-03",
|
||||||
|
"billcount": 137, "itemcount": 402,
|
||||||
|
"grosssales": 18450.50, "taxcollected": 1204.30,
|
||||||
|
"discountgiven": 320.00, "roundoff": -1.50,
|
||||||
|
"averagebill": 134.68,
|
||||||
|
"bypaymentmode": [
|
||||||
|
{"paymentmode": "cash", "billcount": 80, "amount": 9200.00},
|
||||||
|
{"paymentmode": "upi", "billcount": 57, "amount": 9250.50}
|
||||||
|
],
|
||||||
|
"byday": [
|
||||||
|
{"businessdate": "2026-08-01", "billcount": 44, "amount": 5900.00}
|
||||||
|
],
|
||||||
|
"byterminal": [
|
||||||
|
{"terminalid": "T4A9", "billcount": 137, "amount": 18450.50}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Three breakdowns because they answer three different questions: **by tender**
|
||||||
|
for reconciling a drawer, **by day** for a chart, **by till** for an outlet
|
||||||
|
running several counters.
|
||||||
|
|
||||||
|
### `GET /health/terminal` — one till
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl "$BASE/health/terminal?terminal_id=T4A9"
|
||||||
|
```
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 200, "status": true,
|
||||||
|
"details": {
|
||||||
|
"terminal_id": "T4A9", "location_id": "1135",
|
||||||
|
"store_name": "Ragul stores Selvapuram",
|
||||||
|
"app_version": "1.1.0", "status": "online",
|
||||||
|
"pending_bills": "0", "pending_registrations": "0",
|
||||||
|
"oldest_pending_at": "",
|
||||||
|
"today_bills": "2", "today_amount": "170",
|
||||||
|
"last_bill_at": "",
|
||||||
|
"printer_reachable": "0",
|
||||||
|
"reported_at": "2026-08-03T12:04:21Z",
|
||||||
|
"received_at": "2026-08-03T12:04:21Z"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Values are **strings** — it is a Redis hash. A till that has not reported inside
|
||||||
|
its TTL returns `200` with `status: "offline"`, not a 404: it exists, it is
|
||||||
|
simply quiet.
|
||||||
|
|
||||||
|
Fields the till does not collect are **absent, not zero**. A board showing every
|
||||||
|
terminal at 0% battery is worse than one showing nothing.
|
||||||
|
|
||||||
|
`pending_bills` is the number worth watching. A shop quietly accumulating
|
||||||
|
unsynced takings looks completely normal from the floor.
|
||||||
|
|
||||||
|
### `GET /health/location` — the "which counters are dark" board
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl "$BASE/health/location?location_id=1135"
|
||||||
|
```
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 200, "status": true,
|
||||||
|
"details": {
|
||||||
|
"location_id": "1135", "total": 3, "online": 2,
|
||||||
|
"terminals": [
|
||||||
|
{"terminal_id": "T4A9", "status": "online", "today_bills": "37", "…": "…"},
|
||||||
|
{"terminal_id": "T7B2", "status": "offline", "reason": "no heartbeat within 90s"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
A till whose key expired comes back marked **offline rather than omitted** —
|
||||||
|
omitting it would make a dead terminal indistinguishable from one that was never
|
||||||
|
installed, and the dead one is exactly what somebody is looking for.
|
||||||
|
|
||||||
|
### `GET /catalogue` — the till's product pull
|
||||||
|
|
||||||
|
Bare body, no envelope. Used by terminals, not the web app.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl "$BASE/catalogue?store_id=1135&page_size=500"
|
||||||
|
curl "$BASE/catalogue?store_id=1135&since=loc1135-20260803T135407Z"
|
||||||
|
```
|
||||||
|
|
||||||
|
No `since` → **full snapshot**. With a valid `since` → **change set**.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"revision": "loc1135-20260803T135407Z",
|
||||||
|
"is_delta": false,
|
||||||
|
"has_more": false,
|
||||||
|
"products": [{
|
||||||
|
"id": "6988", "name": "Mysore Banana",
|
||||||
|
"barcode": "6988", "sku": "",
|
||||||
|
"category": "grocery", "price": 60, "stock": 750,
|
||||||
|
"unit": "kilogram", "gst_rate": 0.08, "is_active": true
|
||||||
|
}],
|
||||||
|
"customers": [],
|
||||||
|
"retired_product_ids": []
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**`is_delta` is the dangerous field.** `false` means the terminal withdraws
|
||||||
|
every product the response does not mention. A filtered result labelled `false`
|
||||||
|
empties the shelf. In `Catalogue()` the filter and the flag are derived from one
|
||||||
|
value, so no code path can set one without the other.
|
||||||
|
|
||||||
|
Anything ambiguous resolves toward the snapshot: a revision that is malformed,
|
||||||
|
empty, or issued to a different outlet yields a full response.
|
||||||
|
|
||||||
|
The revision **only advances on the final page**, so a terminal that abandons a
|
||||||
|
paginated pull cannot end up holding one claiming it saw pages it never got.
|
||||||
|
|
||||||
|
A delta **cannot withdraw a deleted product** — removing a row from
|
||||||
|
`productlocations` leaves no tombstone. Only a snapshot collects those, so tills
|
||||||
|
should pull without a revision periodically.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. MQTT topics
|
||||||
|
|
||||||
|
| Topic | Direction | Retained |
|
||||||
|
|---|---|---|
|
||||||
|
| `nearle/pos/{loc}/{terminal}/order` | till → us | no |
|
||||||
|
| `nearle/pos/{loc}/{terminal}/customer` | till → us | no |
|
||||||
|
| `nearle/pos/{loc}/{terminal}/health` | till → us, 30s | no |
|
||||||
|
| `nearle/pos/{loc}/{terminal}/ack` | us → till | no |
|
||||||
|
| `nearle/pos/{loc}/{terminal}/status` | till → us | **yes** (Last Will) |
|
||||||
|
| `nearle/pos/{loc}/catalogue` | us → all tills at a shop | **yes** |
|
||||||
|
|
||||||
|
`{loc}` is the numeric `tenantlocations.locationid`. The tenant is resolved from
|
||||||
|
it server-side and never taken from the wire.
|
||||||
|
|
||||||
|
Namespaced under `nearle/` alongside the rider fleet's `nearle/riders/…`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Database
|
||||||
|
|
||||||
|
**`pos_orders`** — one row per counter bill. Separate from `orders` because a
|
||||||
|
bill carries a cashier, terminal, rounding, promos, loyalty and a payment split
|
||||||
|
that `orders` has nowhere to put.
|
||||||
|
|
||||||
|
**`pos_order_items`** — one row per line.
|
||||||
|
|
||||||
|
**`productstocks`** — stock is **not** separate. A counter sale writes the same
|
||||||
|
`out` rows an app order does, through helpers in `repositories/stockLedger.go`.
|
||||||
|
Two stock ledgers would mean the catalogue pull sends a till figures that ignore
|
||||||
|
its own trading.
|
||||||
|
|
||||||
|
**`customers`** — registrations, matched on `contactno`.
|
||||||
|
|
||||||
|
**Redis** — `pos:terminal:{code}` (hash, 90s TTL) and
|
||||||
|
`pos:location:{id}:terminals` (set, no TTL). Namespaced `pos:*` so they cannot
|
||||||
|
collide with express's `delivery:*`, `city:*`, `rider_*`.
|
||||||
|
|
||||||
|
> **Reporting:** counter sales are unioned into `GetRevenueSummary` and
|
||||||
|
> `GetSalesSummary`. **Any new report must do the same**, or it will silently
|
||||||
|
> understate every shop that runs a till. That is the standing cost of the split.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Code map
|
||||||
|
|
||||||
|
| File | |
|
||||||
|
|---|---|
|
||||||
|
| `models/pos.go` | wire types — matches the till's JSON exactly |
|
||||||
|
| `models/posorder.go` | `pos_orders` / `pos_order_items` |
|
||||||
|
| `models/poshealth.go` | heartbeat |
|
||||||
|
| `repositories/posRepository.go` | ingest + catalogue |
|
||||||
|
| `repositories/posSalesRepository.go` | the GET reads |
|
||||||
|
| `repositories/posPresence.go` | Redis presence |
|
||||||
|
| `repositories/stockLedger.go` | **shared** stock helpers |
|
||||||
|
| `messaging/posmqtt.go` | MQTT consumer |
|
||||||
|
| `messaging/posworkers.go` | bounded worker pools |
|
||||||
|
| `controllers/posController.go` | HTTP handlers |
|
||||||
|
| `routes/posroutes.go` | routes |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Operational notes
|
||||||
|
|
||||||
|
**Only `fiesta-0` consumes.** MQTT has no queue groups, so all replicas would
|
||||||
|
receive every message and commit the same bill three times. Ordinal 0 is
|
||||||
|
elected; the others log *"not the elected consumer"*. Override with
|
||||||
|
`POS_MQTT_CONSUMER=always|never`.
|
||||||
|
|
||||||
|
**Worker pools**: `POS_INGEST_WORKERS` (default 8), `POS_HEALTH_WORKERS`
|
||||||
|
(default 2). Heartbeats have their own pool so a backlog of bills cannot make
|
||||||
|
every till look dark at the busiest moment. A full queue **blocks**, pushing
|
||||||
|
backpressure to the broker and the till — slow, never lossy.
|
||||||
|
|
||||||
|
**Startup check**: three `pos: subscribed to nearle/pos/+/+/…` lines on
|
||||||
|
`fiesta-0`. Without them, MQTT ingest is not running and tills queue silently.
|
||||||
|
|
||||||
|
**The broker is not durable storage.** Mosquitto's `max_queued_messages` is 1000
|
||||||
|
and it flushes every 30 minutes. Fine, because a till keeps its copy until we
|
||||||
|
acknowledge — but nobody may ever ack on the broker's behalf.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Known gaps
|
||||||
|
|
||||||
|
- **No product prices.** Every product at loc 1135 is ₹0, so nothing is
|
||||||
|
sellable. Unpriced products come down as `is_active: false` so a till cannot
|
||||||
|
ring up a ₹0 item.
|
||||||
|
- **The Flutter app has never been run.** All testing used a Go program
|
||||||
|
impersonating a till.
|
||||||
|
- **No TLS** on port 1883. Bills carry customer names and mobile numbers.
|
||||||
|
- **No device authentication.** A terminal is trusted with a location id.
|
||||||
|
- **Loyalty does not come back down.** Balances at a till are that till's view.
|
||||||
|
- **`productstocks.quantity` is an integer** but tills sell in kg. POS rounds
|
||||||
|
**up** so it never under-deducts; the app-order path truncates, which was left
|
||||||
|
alone rather than silently changed. Making the column numeric is the real fix.
|
||||||
|
- **Broker credentials in source** — `admin` is in the rider APK and still
|
||||||
|
unrestricted. The two POS accounts are the only ones not in a source tree.
|
||||||
Reference in New Issue
Block a user