nawasara / pbb
PBB tax bill lookup for the Nawasara superapp framework — proxies the Ponorogo sipandaunik system, parses its HTML into JSON, and serves it to logged-in residents.
Requires
- php: ^8.1
- illuminate/support: ^10.0|^12.0
- livewire/livewire: ^3.0
Requires (Dev)
None
Suggests
None
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-09-14 02:29:09 UTC
README
PBB (property tax) bill lookup for the Nawasara superapp framework. It sits in front of Ponorogo's tax system, sipandaunik: it calls the NOP search endpoint, parses the HTML response into clean JSON, hides the taxpayer's identity, and serves the result to the citizen app (SuperApps Flutter) behind citizen login.
Status v0.1.0
| Feature | Status |
|---|---|
Look up a bill by NOP (POST /pbb/check) |
ready |
| Parse the year history (paid/unpaid, principal, penalty, total) | ready |
| Mask the name (initial) and drop the address | ready |
Behind citizen login (api.citizen) with throttle |
ready |
| Per-citizen search history | not built yet |
| Panel or menu in Nawasara | intentionally none (see below) |
The package stores nothing. There are no migrations, models, sync jobs, or Livewire pages. Every request goes straight through to sipandaunik. That is why there is no sidebar workspace and no PermissionSeeder: access is gated by the citizen realm JWT, not a Spatie permission.
Setup
No migrations or seeders. Register it in the root composer.json (path repo
plus require), then:
composer update nawasara/pbb
Optional configuration through .env:
NAWASARA_PBB_BASE_URL=https://sipandaunik.ponorogo.go.id
NAWASARA_PBB_HTTP_TIMEOUT=20
There is no @source line in app.css. The package ships no Blade views, so
there are no Tailwind classes to compile.
Endpoint
POST {api-prefix}/pbb/check, behind ['api', 'api.citizen', 'throttle:nawasara-citizen'].
Request body:
{ "nop": "35.02.020.022.004-0206.0" }
Response (200):
{
"data": {
"name_masked": "T••••••",
"bills": [
{ "year": 2026, "status": "LUNAS", "principal": 13133, "penalty": 0, "amount": 13133 }
],
"total_due": 0,
"has_arrears": false
}
}
Other codes: 404 when the NOP is not found, 422 when the NOP is not 18
digits, 502 when sipandaunik cannot be reached.
Design notes
We use POST rather than GET even though the request only reads data. A NOP points to a specific plot of land, and putting it in a query string records it in access logs and proxy history. A POST body does not.
The name is shortened to its first letter and the address is removed
completely. sipandaunik returns the full nama_wp, alamat_wp, and alamat_op
to anyone who knows a NOP, with no session check at all. We tested this on 12
September 2026: the request returns the same full data with no cookie and with
a garbage cookie. Because a neighbour's NOP is easy to guess (they run in
sequence within a block), showing the full name would leak who owns what. An
initial is enough for a citizen to confirm "this is mine". The property address
never leaves the system, because it is the field that points to the physical
location.
Citizen login is required even though the source demands nothing. Since sipandaunik protects nothing itself, we moved the gate here, so that whoever asks about a bill is at least a known citizen rather than an anonymous bulk scraper.
Filtering lives in the Resource, not the client. PbbBillResource is written
as an allow list, so if SipandaunikClient ever returns a new field it is not
forwarded by accident. Masking in the client would leave paths that forget to
filter. In the Resource there is one exit for everything.
The NOP comes from the request body, not the token. A single citizen commonly
manages PBB for several plots (a house, a rice field, an inheritance), and
there is no fixed link between a citizen's sub and any given NOP, so locking
it to the token identity would be wrong.
A 502 is kept separate from a 404. sipandaunik being down and a NOP not existing are two different things, and treating them the same would make a citizen think their tax is paid when the system is actually unreachable.
Roadmap
Per-citizen search history, if it turns out to be useful (for example "NOPs I
have checked before"). It would need one table keyed on keycloak_sub. It is
deliberately left out for now, because storing the NOPs a citizen searches
means storing personal data we do not currently need.
Author
Pringgo J. Saputro, Kominfo Ponorogo. MIT License.