* fix(http): don't crash the exception handler on non-int exception codes
Throwable::getCode() is documented as returning int, but PDOException
sets its internal (untyped) $code property to the driver's SQLSTATE
string. ExceptionResponseFactory::getStatusCode() blindly returned
that value from a method typed to return int, so any exception with a
non-int code (e.g. a PDOException raised during session handling)
crashed the exception handler itself with an unrelated TypeError
instead of surfacing the real error.
* fix(update): remove the install directory on default, unconfigured deploys
The install/ directory deletion (in both checkInstaller() and
UpdateFinalization) required an explicit APP_ENV=prod environment
variable. But nothing in the actual shipped deployment path -- the
official Dockerfile, or a typical shared-hosting unzip-and-run install
-- ever sets APP_ENV, so the installer's own completion screen ('The
./install/ folder is normally deleted automatically') was untrue for
the common case: cleanup silently never ran.
Flip the policy to match Environment::isProduction(), which already
treats an unset/unrecognized APP_ENV as production (the ambiguous
default used everywhere else in the app). The install directory is now
removed unless the environment is explicitly dev or test, or DEBUG is
enabled -- so local/CI checkouts keep the installer (and the Pest suite,
which reads install/sql/*.sql directly from disk, keeps working) while
a default production deploy gets cleaned up as documented.
* ✨ Show promo redemption history on the promo detail page
Adds a Redemption History table to the product promotion detail page,
listing the individual clients, orders, invoices, phase, status, and
discount amount for every redemption of a promo code. The admin API
(product_promo_redemption_get_list) and backing data model already
existed; the promo list page even linked to a #promo-redemptions
anchor that didn't exist yet. This wires it up.
Refs #588
* 🔒 Don't auto-delete install/ when APP_ENV is ambiguous
checkInstaller() (src/load.php) runs on every bootstrap and, once the
app is installed, deletes the src/install/ directory automatically.
It gated that irreversible delete on Environment::isProduction(),
which silently treats a missing or unrecognized APP_ENV as production.
APP_ENV=dev is only set inside DDEV's web container, so any process
that touches load.php outside of it (a bare `php console.php`, a
stray `php -S`, etc.) resolves to production by default and wipes
install/ with no warning. This happened in a local dev checkout.
Add Environment::isExplicitlyProduction(), which is only true when
APP_ENV is literally "prod", and require it specifically for this
delete. isProduction()'s existing default-to-production behavior is
left untouched elsewhere, since other checks rely on it (email-sending
gates, DNS validation strictness, admin nudges) in ways that are
either non-destructive or where failing toward production is correct.
Also logs before removing the directory instead of doing so silently.
* 🐛 Prevent request query params from overriding the promo_id
CodeRabbit review: merge(request) ran last in the redemption list query,
so a crafted ?promo_id= query param could override the promo actually
being viewed and show another promotion's redemption data. Force
promo_id last so it always reflects the page's own promo, while still
letting per_page/page follow the pagination controls as intended.