Rentiva v4.35.0 — Vendor Report & Appeal System
A shared infrastructure for vendor → admin reports lands in v4.35.0. Five contexts, one custom table, one shared modal, one admin "Bayi Raporları" page. Plus the withdrawal/pause reason capture flow: when a vendor withdraws a vehicle and explains why, the penalty is suspended until an admin reviews the appeal — accept, the vendor keeps their score; reject, the penalty applies retroactively.
Why
Until v4.35.0 vendors had no structured way to talk back to the admin. Booking issues (no-show customer, damage), vehicle actions that triggered automatic penalties, score deductions they thought were unfair — all of these went straight to email or Slack, off the platform. The marketplace had a one-way feedback loop.
v4.35.0 closes the loop with a single piece of shared infrastructure. The same modal, table, and admin page handle all five contexts. The only thing that varies is the trigger location and the side-effect on resolution.
The five contexts
| Context | When | Trigger | Resolution side-effect |
|---|---|---|---|
booking | Customer issue (no-show, damage, dispute) | "Sorun Bildir" on the vendor booking card (button existed since v4.26.5, JS now wired) | None — informational |
vehicle | Appeal a paused/withdrawn vehicle action | "İtiraz Et" on listings page (paused/withdrawn states only) | None — admin guidance |
vehicle_action | Reason capture during withdraw/pause | Modal automatically opens when a vendor clicks Withdraw or Pause | Reject → deferred penalty applied; resolve → vendor keeps score |
penalty | Appeal an already-applied penalty | "Appeal" button on each penalty row in score history | (v4.36.0+ — ledger compensating entry helper) |
general | "Yöneticiye Yaz" — direct line to admin | Footer link on every vendor panel page | None |
The Not 2 augment — withdrawal reason capture
Before v4.35.0, withdrawing a vehicle meant a confirm() dialog ("Are you sure? A penalty may apply.") and an immediate score deduction + ledger debit. There was no way to tell the platform why the vehicle had to leave.
Now the lifecycle JS captures the reason in a proper modal and POSTs it alongside the mhm_vehicle_lifecycle_withdraw AJAX call. The AJAX handler creates a vehicle_action report before calling the Manager. Inside the Manager, a new filter — mhm_rentiva_before_apply_penalty — fires:
$apply_penalty = (bool) apply_filters(
'mhm_rentiva_before_apply_penalty',
true,
$vehicle_id,
$vendor_id,
'withdrawal',
$penalty
);
if ($apply_penalty) {
ReliabilityScoreCalculator::update($vendor_id, 'withdraw', $vehicle_id);
}
The same filter wraps PenaltyRecorder::record_penalty() so the ledger debit stays in lockstep with the score. PenaltySuspensionHook::maybe_suspend() returns false when an open vehicle_action report exists for the vendor + vehicle pair.
State and side-effects are decoupled: the vehicle still transitions to withdrawn (post status, lifecycle meta, cooldown date — all set), but the score and ledger are paused. When the admin resolves the report:
- Resolved (vendor wins) — no-op. Score and ledger were never touched. The vendor lost zero points and zero TL.
- Rejected —
VendorReportService::apply_deferred_penalty()callsReliabilityScoreCalculator::update()to recompute (the withdrawal is in state, so the score now drops) and re-runsPenaltyRecorder::record_penalty()to write the ledger debit. The penalty applies retroactively.
Architecture in one paragraph
VendorReportRepository is the DB layer (CRUD on the new wp_mhm_rentiva_vendor_reports custom table; per-request has_open_report_for() cache so the dual filter hook costs one DB query). VendorReportService is the business layer (validation, FSM, side-effects, hook firing for emails). VendorReportAjaxHandler is the transport. PenaltySuspensionHook is the bridge to the existing penalty pipeline. VendorReportsAdminPage is the admin UI. VendorReportAssets enqueues the shared modal and JS on every vendor-facing page. The only thing that touches multiple layers is the filter hook itself, and that's deliberate — the hook is the seam between the vendor-report subsystem and the lifecycle subsystem.
Plan-vs-codebase reconciliation
The original plan was written 2026-04-15 and assumed nine things about the codebase that turned out to be false. A comprehensive pre-implementation audit caught all of them:
src/Admin/Penalty/directory — does not exist; penalty code lives insrc/Admin/Vehicle/.PenaltyService::cancel()/apply_pending()— no such class. Removed from design; service layer callsReliabilityScoreCalculatorandPenaltyRecorderdirectly.mhm_rentiva_before_apply_penaltyfilter — did not exist. Introduced in this release.penalty_record_idis BIGINT — penalties are anonymous ledger entries with UUIDs. Schema changed toVARCHAR(64) context_id.- Score deduction is hook-suspendable — actually synchronous before any hook fires. Filter must wrap the score update inline.
- Migration file
0042_create_vendor_reports.php— no sequential numbering convention. RenamedVendorReportsMigration, registered viaDatabaseMigrator::run_migrations()afterCURRENT_VERSIONbump. - "Sorun Bildir" button needs adding to vendor-bookings.php — already exists since v4.26.5. Build sequence simplified.
- "Report Issue" string needs adding to .po — already translated. List trimmed.
- No Mode gating mentioned —
Mode::canUseVendorMarketplace()required on all new admin pages, AJAX handlers, email registrations, and asset enqueues.
The user explicitly requested "Vendor ile ilgili tüm dosyaları detaylı incele. Eksik, gözden kaçan bir şey olmasın" before implementation began. That nudge is what surfaced these. Plan files written months earlier are not authoritative; the codebase is.
Tests
- PHPUnit: 840 → 864 (+24 — Repository 10, Service 8, PenaltySuspensionHook 6)
- PHPCS: 0 errors
- Runtime smoke test: admin "Bayi Raporları" page renders, all status + context filters in TR, empty state correctly shown, full migration applied via
DatabaseMigratoron plugin activate.
Compatibility
- No license-server upgrade required.
- Backwards compatible: lifecycle AJAX submissions without a
reasonPOST parameter behave exactly as in v4.34.x (no report created, normal penalty applied). Older vendors don't break. - Dev sites need
define('MHM_RENTIVA_DEV_PRO', true);inwp-config.phpto bypass the Pro gate while testing.
What's next
The roadmap continues with the transfer wave: v4.36.0 adds context-aware add-ons (extending vehicle_addon CPT with an addon_context taxonomy), v4.37.0 adds a transfer-locations map view. After that the vendor wave picks up — directory page (v4.38.0) and the long-deferred Phase 5 vendor public profile (v4.39.0).
