# Miliki Portal — Project Notes

Working notes on the current state of the codebase, captured 2026-07-25 as a baseline
for planning restructuring, tweaks, and new features. This is a snapshot of
**what exists today**, not a design document — treat findings as a starting point to
verify against, not permanent fact (the codebase will move).

---

## 1. What this actually is

**Miliki Portal** is a Laravel 9 admin backend for a Tanzanian real-estate plot-sales
business:

- Property owners list projects.
- Projects are subdivided into plots.
- Customers buy plots (full payment / installment / loan).
- Post-purchase, a plot moves through a land-transfer bureaucracy pipeline modeled on
  the real Tanzanian land-title process:
  `Started → District Office → Revenue Authority (TRA) Review → Ministry Of Lands →
  Ready For Signing → Closed`.

**Stack**: Laravel 9, MySQL, Blade + jQuery/DataTables admin UI, a handful of Vue 2
components, `spatie/laravel-permission` for RBAC, `owen-it/laravel-auditing` for
change history.

---

## 2. The single biggest structural fact

**This codebase is a telco / mobile-money agent-banking product that was repurposed
into a real-estate portal**, and the seams show everywhere:

- Migration history literally starts (Aug–Oct 2021) with `customers`, `campaigns`,
  `shops`, `telcos`, `financing_partners`, MSISDN-keyed `transactions` — no
  property/plot concept exists yet. Real-estate tables (`properties`,
  `property_plots`, `property_owners`, `land_uses`) only appear starting April 2022,
  bolted on top of the telco schema.
- `Customer`, `Campaign`, `Telco`, `FinancingPartner`, `Shop`, `OEM` models are
  referenced by Eloquent relations across the code but several of the underlying
  classes **do not exist** — dead code that would fatal-error if actually hit. The
  real buyer-facing flow runs through `User` / `Order` / `PropertyPlot`, not
  `Customer` / `Campaign`.
- Core money tables (`transactions`, `orders`) still carry telco-shaped columns
  (`msisdn`, `a_party_msisdn`, `service_fee`, `termination_fee`,
  `redeem_shop_id`) — mobile money is genuinely still the payment rail, just wearing
  real-estate clothes.
- Two independent audit systems coexist: `owen-it/laravel-auditing` (used by ~40 of
  48 models) and a bespoke `audit_trails` table that was rebuilt three separate times
  by the same vendor's other telco packages (`epmnzava/*`).
- Orphaned reference tables (`account_institutions`, `account_types`,
  `institution_types`) exist in the live DB dump with no migration and no model.

**Implication for restructuring**: a meaningful chunk of the schema/model surface is
legacy weight, not active product. Any cleanup pass should explicitly decide what to
purge vs. what to formalize.

---

## 3. Architecture pattern

**Controller-centric monolith, no service/repository layer in the live path.**

- `PropertyController` alone is ~896 lines: CRUD, plot import, pricing, post-purchase
  status, all inline — direct Eloquent queries, inline `$request->validate()`. Only
  **one** Form Request class exists in the entire app
  (`app/Http/Requests/Auth/LoginRequest.php`).
- Meanwhile there is a **second, fully-built, currently orphaned layer**:
  `FrontendService` (957 lines), `AuthFrontendService` (1306 lines),
  `TransactionService` (1913 lines), plus a `Repositories/*` layer feeding them.
  These are clearly designed as a JSON API for a mobile/USSD front-end, gated by
  Passport token abilities (`tokenCan('create-orders')`), referencing an external
  host (`milikiapi.co.tz`).
- `routes/api.php` contains only unused `use` imports and defines **zero routes**,
  and isn't even registered by `RouteServiceProvider` (the `api` group registration
  is commented out).
- **Conclusion**: there appear to be two apps here — the live Blade admin portal, and
  a dormant customer-facing API that isn't wired to any route in this repo. It may
  live in a separate deployment/repo, or be mid-migration/abandoned. Worth confirming
  with the team before deciding whether to revive, finish, or delete it.

### Request flow example (traced end-to-end): plot post-purchase status update

```
GET/POST /property/updatepostpurchase/{plotid}
  → PropertyController::updatepostpurchase() / storepostpurchase_status()
  → PlotPostPurchase model (append-only status history per plot)
  → CompletedOrderStatus model (master step catalog, managed separately by
    CompletedOrderStatusController, superadmin-only, /plot-purchase/*)
  → resources/views/superadmin/plotstatus/update_status.blade.php
  → redirect to PropertyController::plotshow() (plot detail/timeline view)
```

Notable: the email/notification dispatch line in `storepostpurchase_status()` is
currently **commented out**, so status changes don't actually notify anyone despite
the message being built.

Default status steps are kept in sync via `php artisan plotpurchase:sync-statuses`
(idempotent, `firstOrCreate`).

---

## 4. Data model (core vs. legacy)

**Active real-estate domain** (this is the actual product):
`Property`, `PropertyOwner`, `PropertyPlot`, `PropertyPlotPricing`, `LandUse`,
`Location`, `Region`, `District`, `Order`, `ExpiredOrder`, `PlotPostPurchase`,
`CompletedOrderStatus`, `Booking`, `VisitTime`, `VisitPayment`, `Contract`,
`NextOfKin`, `BankList`.

**Legacy telco/agent-banking domain** (dead weight, partially still touched by
payment columns): `Campaign`, `Customer`, `Telco*`, `FinancingPartner*`, `Shop*`,
`TranType`, `TransactionLog`, `PaymentRequest`, `account_institutions`,
`account_types`, `institution_types`, `OEM*` (`*` = broken/non-existent class or
orphaned table).

**Modeling issues to fix eventually**:
- `PropertyPlot::landuse()` relation references `Landuse::class` but the real model
  is `LandUse` (capital U) — case-mismatch, could break on case-sensitive
  filesystems/autoloaders.
- `Status` model has no `$table` override; migration creates table `status`
  (singular) but Eloquent's default inference would look for `statuses` — verify
  against the live schema.
- `Financing` model points at a `financing` table that doesn't exist anywhere
  (migrations or SQL dump) — fully orphaned.
- Nearly every model uses `protected $guarded = []` (mass-assignment wide open).
- `app/Rules/Password.php` — `Rule` implementation with an empty `passes()` body;
  currently unused (real password rules are inline via Laravel's built-in
  `Password::min(10)...`), but a latent bug if anyone wires it up.

**Seed data**: only `RelationSeeder` (family relations) and `RateSeeder` (one
USD→TZS rate) run from `DatabaseSeeder`. No role/permission seeder despite RBAC being
core to the app — roles appear to be created ad hoc /manually in each environment.
`CompletedOrderStatusSeeder` exists but isn't wired into `DatabaseSeeder` (must be run
manually or via the sync command).

**Multi-tenancy**: `config/multitenancy.php` (spatie/laravel-multitenancy) is present
but the package **isn't installed** — unused config stub. Actual "tenancy" is
informal: a `propertyownerid` column used to scope queries in-controller. Single
database, single tenant in practice.

---

## 5. Roles & access

Spatie-based (`laravel-permission`), enforced **ad hoc** — there is no route-level
`auth`/`role` middleware anywhere in `routes/*.php`. Every check is inside controller
constructors/methods, with a private `authorizeSuperadmin()` helper duplicated
verbatim across 5 different controllers instead of centralized.

| Role | Access |
|---|---|
| **superadmin** | Full access — users, roles/permissions, countries, land use, plot-purchase status config, all properties/owners, all transactions |
| **propertyowner** | Scoped to own data via `propertyownerid` column matching; can manage own users (only as `propertyowner` role), own orders |
| **vodacom customer care** | Portal login allowed; sees `customer`-role users where `source='mini'` (Vodacom mini-app/USSD channel); views all orders |
| **customer** | **Cannot log into this portal at all** — explicitly rejected in `AuthenticatedSessionController`; belongs to the orphaned mobile/API surface |
| **system** | Role exists (created by `InitRolePerms`), never assigned or checked anywhere |

**Abandoned second hierarchy**: a `users.management_level` column plus Vue
components (`ManagementLevelComponent.vue`, `RoleFilter.vue`, `UserRoleAssign.vue`)
call `/api/roles/*` and `/api/checkrolemanager/*` endpoints that **don't exist**.
Console command code (commented out) references a `ManagementLevel` model that was
never created, intended values `[Telco, FinancingPartner, Shop Admin, IA Admin,
Campaign]` — apparently a partner-hierarchy tier that was never finished.

**Auth gaps found**: several controllers have no auth check at all and are only
"protected" by the fact nothing else blocks access either:
`Property/PropertyOwnerController`, `Property/LocationController`,
`Property/LogController`, `WebController` (auth middleware commented out),
`GuestController`, `RenameScriptController`, `TestController`/`TestingController`,
`OAuthController`.

Also dead/unregistered: `Kernel.php` imports `AuthApi` and `MilikiScope` middleware
classes that don't exist in the repo; `URLWhitelist.php` middleware exists (gates
`/api/v1` by hostname) but is never registered anywhere.

---

## 6. Payments & integrations

- **Card payments**: `app/Payment/OnlineCard.php` — MiGS/VPC-style gateway
  (Visa/MasterCard), HMAC-signed redirect URL.
- **Airtel Money**: `app/Payment/AirtelMoney.php` — raw cURL to
  `serengetiapi.co.tz`; reads `env('airtel_money_endpoint')` directly rather than via
  `config/miliki.php` where the value actually lives — likely a bug.
- **M-Pesa**: no API integration; treated as a manually-confirmed, out-of-band
  payment method (string flag only) in `VisitPaymentService`.
- **Tigo Pesa**: not integrated at all.
- Orchestration lives in `TransactionService` (`request_payment`, `payment` as the
  de facto inbound confirmation/webhook handler, `processInstallmentPayment`, etc.)
  — but this entire service is part of the **orphaned API layer** (§3), not reachable
  from any live route.
- **Latent bug**: `TransactionService`'s `getResponse()` helper calls `log_audit()`,
  which is commented out in `app/Helper/functions.php` — any live invocation of the
  payment flow would currently throw `Call to undefined function log_audit()`.
- **SMS/Email notifications**: central hub is `add_notification()` in
  `app/Helper/functions.php` → SMS via Nextsms (`epmnzava/nextsms`, sender id
  `MILIKI`), email via `SendMailNotification` mailable. Triggered on order
  created/paid/expired/completed.
- **File imports**: `PlotImport` (bulk plot upload via Excel, template at
  `public/plot_format.xlsx`) and `UserImport` (bulk user invite with role assignment)
  — both live, reachable from admin routes.
- **Real-time/broadcasting**: configured (`config/broadcasting.php`,
  `routes/channels.php`) but no Pusher/Ably package installed and no Echo client
  code — unused scaffolding, not a real feature.
- **CORS**: fully open (`allowed_origins => ['*']`) on `api/*` — acceptable only
  because `supports_credentials => false`, but worth revisiting given payment/order
  endpoints are meant to live under that prefix.

---

## 7. Error handling, logging, testing — overall: minimal, no safety net

- **No custom exception classes anywhere.** Domain errors (e.g. "plot not
  available") are plain arrays with string status codes, not typed exceptions —
  nothing can be caught/handled polymorphically.
- **`Log::error` / `Log::warning` are used nowhere in `app/`.** Every log call,
  including inside real `catch` blocks, uses `Log::info` — makes log-based alerting
  or severity triage impossible. At least one fully silent empty catch block exists
  (`TransactionService.php`).
- Only 2 of 30 controllers contain any try/catch at all; the rest of what error
  handling exists lives in the Services/Actions layer.
- **Sentry** is the only real monitoring integration, wired through a single line in
  `Handler::register()` — depends on an env var not verifiable in this checkout (no
  `.env`/`.env.example` committed).
- **Telescope and Debugbar are fully configured but not installed** — misleading
  dead config that could make a new engineer think dev-debugging tooling exists when
  it doesn't.
- **Queueing**: default connection is `sync` unless overridden; only 2 Jobs exist
  (`ProcessExpiryCheck`, `ProcessNotification`), neither defines retries/backoff/
  `failed()`. Most "async-shaped" work (notifications, payment handling) actually
  runs synchronously with manual try/catch instead of going through the queue.
- **Tests are placeholder-level**: `tests/Unit` doesn't exist despite being declared
  in `phpunit.xml`; most Feature test bodies are commented out; **zero coverage of
  orders, plots, or payments** — the app's core financial logic. CI
  (`.github/workflows/deploy.yml`) deploys straight to the server on every push to
  `main`/`test` with **no test step at all**, and does a `chmod -R 777` on deploy.

---

## 8. Open questions to resolve before restructuring

1. Is the orphaned `FrontendService`/`AuthFrontendService`/`TransactionService` API
   layer meant to be revived (finish wiring `routes/api.php`, fix `log_audit()`,
   connect a mobile app) or deleted? It represents a huge chunk of code (~4,000+
   lines) currently doing nothing.
2. Is the telco/agent-banking legacy domain (`Customer`, `Campaign`, `Telco*`,
   `Shop*`, `FinancingPartner*`, `OEM*`) safe to remove, or does anything in
   production still depend on it (e.g. the Vodacom mini-app/USSD channel referenced
   via `customers.source = 'mini'`)?
3. Was the `management_level` / partner-hierarchy concept intentionally abandoned,
   or does it need to be finished for a specific use case (Telco/FinancingPartner/
   Shop-Admin/IA-Admin tiers)?
4. What is the actual `QUEUE_CONNECTION` and `SENTRY_DSN` in production? (Not
   verifiable from this repo — no `.env` committed.)
5. Priority order for the gaps in §7 — given there's no test safety net, any
   refactor of `TransactionService`/`OrderAction`/payment controllers should budget
   for adding both proper exception types + `Log::error` usage and real Feature test
   coverage before or alongside the change, since nothing currently catches
   regressions there.
