* Fix WHM activation failing when package already exists (#4560)
- Request listpkgs with want=all and parse both legacy package and modern data.pkg shapes
- On addpkg already-exists, sync via editpkg and continue; only block activation on other errors
- Include WHM reason in Server_Exception and fix placeholder keys
* Add WHM API terms to spellcheck dictionary
* Address review: use typed exception for WHM already-exists, keep reasons out of API errors
- Add Server_PackageExistsException; request() now throws generic messages and translates already-exists at the WHM boundary
- checkPackageExists() catches the typed exception instead of matching getMessage()
* Fix Rector findings: remove unused catch variable and setAccessible call
* Handle modern WHM metadata.result failures in Whm::request
---------
Co-authored-by: opencode <opencode@localhost>
* Harden guest session-state API calls against cross-site requests
Extend the guest authentication guard to the session-bound cart mutations
(cart add/remove item, promo apply/remove, reset, currency change),
requiring the pre-login session nonce and a same-origin context like
client login, signup, and staff login already do. Cart changes accept GET
with the nonce in the query string, matching _checkCSRFToken()'s token
sources, so shipped fb_api_link usage keeps working.
Also pin an explicit SameSite=Lax on the session cookie when
security.mode is not strict: an unset attribute leaves cookie behavior to
each browser's default, and some browsers send cookies on cross-site
requests when it is missing. The installer session already uses Lax.
* Default queued cookies to SameSite Lax
CookieQueue now defaults to Lax so locale and timezone preference
cookies carry an explicit policy instead of leaving the behavior to
each browser's unset-attribute default. The CSRF cookie keeps its
explicit Strict, and callers can still pass null to omit the
attribute.
* Sign guest E2E requests with the pre-login nonce
The Live Tests failures on the cart guard came from the test helper
keeping its own copy of the guarded-endpoint list (login and signup
only), so guest cart calls went out unsigned and were rejected with
403. Attach the nonce to every guest request instead - the dispatcher
strips the parameter where it is not used - and let guest GET requests
carry it in the query string, which _checkCSRFToken() already accepts.
* Set the cURL URL after appending guest GET parameters
CURLOPT_URL copies the value at set time, so mutating $url after
curl_setopt_array() left the handle pointing at the query-less URL and
guest GET requests went out without the nonce or parameters. Re-set
CURLOPT_URL once the query string is appended.
* Address review comments on #4541: document UGX exclusion, hardcode Stripe decimal lists
- Rename STRIPE_ZERO_DECIMAL_CURRENCIES to STRIPE_ZERO_DECIMAL_CHARGE_CURRENCIES
and document why UGX is excluded (zero-decimal per Stripe's list, but its
special-case row requires charge amounts x100), plus ISK/HUF/TWD notes.
- Replace the CLDR-based three-decimal check with a hardcoded
STRIPE_THREE_DECIMAL_CURRENCIES list (BHD, JOD, KWD, OMR, TND); drop the
Symfony Intl dependency from this helper.
- Reword the StripeTest comment to state the UGX exception explicitly.
* Extend Stripe currency test coverage in #4541 follow-up
- getAmountInMinorUnits: add BHD (second three-decimal code), AFN/YER
(more CLDR-zero codes Stripe charges x100), TWD (payout-zero/charge-two
special case), unknown-code fallback (XXX) and lowercase inputs.
- Add getAmountFromMinorUnits round-trip test via invokePrivateMethod so the
read-back path is covered too.
* 🐛 Send Stripe amounts in Stripe's units, not CLDR's
The adapter scaled amounts by CLDR's fraction digits, which are 0 for
COP, HUF, IDR, ISK, PKR, UGX and several other currencies that Stripe
expects with two decimals, so it sent 1/100 of the invoice total and
read Stripe's amounts back with the same factor. Use Stripe's rules:
its zero-decimal currencies as-is, three-decimal ones with three
digits, everything else (ISK and UGX included) x100.
* ✏️ Avoid an unknown word in the Stripe currency test comment
* Harden Stripe renewal paid-skip for inconclusive billing reasons
Only skip the already-paid original invoice for the initial payment or
when billing_reason is missing (ambiguous: duplicate initial webhook vs
renewal — skipping avoids double-crediting). Adds regression tests that
a duplicate initial webhook is still skipped and documents the double
DB fetch as an identity-map hit.
* Rename test identifiers to satisfy spellcheck
🐛 Record Stripe renewal payments after the first invoice is paid
The subscription metadata always points at the first invoice of the
subscription, so the "already paid" skip in invoice.paid handling
returned early for every renewal: no funds were added and the renewal
invoice stayed unpaid. Apply the skip to the initial payment only.
Co-authored-by: Adam Daley <admdly@users.noreply.github.com>
* Fix cron-sent emails rendering dates in en_US instead of configured locale
In CLI/cron context there is no Accept-Language header, so
Locale::acceptFromHttp() fails and Locale::lookup() with an empty
detected locale returns the first enabled locale (en_US) instead of
null, overriding the configured i18n.locale default. Guard
getBrowserLocale() against empty/unparsable headers so it falls back
to the configured default, and stop queueing empty locale cookies.
Also plumb an explicit per-recipient locale through email rendering:
TwigFactory::createEmailEnvironment(), System render helpers, and
Email::sendTemplate() now honor the recipient's language
(client lang, per staff member on broadcasts), outranking browser
auto-detection, with Massmailer rendering per-client too.
Fixes#4524
* Fix tests: simulate header-less requests, update subject render expectation
Symfony's Request::create() seeds a default Accept-Language header,
so strip it to reproduce the CLI/cron condition in the new
getActiveLocale fallback tests. Update the existing
renderEmailSubjectString test for the new locale parameter.
🐛 Soften PayPal item_number echo check to a warning
PayPal freezes item_number at subscription signup while the invoice
number can change (renumbering, issuing), so the strict check added in
#4424 failed legitimate renewal payments for older subscriptions.
Downgrade the item_number mismatch to a warning log and keep
processing; the stable internal invoice id reference remains a hard
error. The signed callback binding, subscription linkage, and amount
checks remain the payment binding.
Fixes#4533
* Refresh default branding assets
Add updated light and dark logos plus an SVG favicon, and point installer and default company branding settings to the new assets. Update shared theme tests to use the renamed light logo.
* Complete default branding asset migration
* Preserve branding assets still referenced by settings
* Harden extension directory partial against sparse metadata
Remote directory entries may omit icon_url, author, author URL/id,
releases, or description. Under strict_variables any missing key
crashes the whole Extensions page (and the registrar/payment-gateway
tabs that reuse this partial). Default every optional field and fall
back to a generic icon/author label.
Adds sparse-metadata cases to ExtensionDirectoryTest covering
omitted/null icons, registrar author shapes without URL, missing
authors, and empty/missing releases.
Fixes#4486
* Use page-level type fallback for extension install links
Addresses review: entries without extension.type should stay in the
currently selected directory category instead of falling back to a
hard-coded mod.
* Serve stale extension directory data on refresh failures
The Extensions page blocks on a live directory fetch. Until now a
failed refresh meant every admin page load paid another HTTP timeout
(up to 5s each), and the cache key ignored the installed FOSSBilling
version, so an upgrade could briefly serve the previous version's
filtered list.
Keep a week-long last-known-good copy alongside the hourly entry:
failed refreshes fall back to it (with a warning logged), and
further attempts are suppressed for 5 minutes. The installed
version is now part of the cache key. Non-app exceptions from the
HTTP layer are normalized to FOSSBilling exceptions so callers can
rely on a single exception contract.
* Treat corrupted fresh directory cache entries as misses
* Validate directory results before writing either cache slot
* Validate stale directory results before serving them
* Harden extension directory partial against sparse metadata
Remote directory entries may omit icon_url, author, author URL/id,
releases, or description. Under strict_variables any missing key
crashes the whole Extensions page (and the registrar/payment-gateway
tabs that reuse this partial). Default every optional field and fall
back to a generic icon/author label.
Adds sparse-metadata cases to ExtensionDirectoryTest covering
omitted/null icons, registrar author shapes without URL, missing
authors, and empty/missing releases.
Fixes#4486
* Use page-level type fallback for extension install links
Addresses review: entries without extension.type should stay in the
currently selected directory category instead of falling back to a
hard-coded mod.
* Harden theme settings auth and CSRF checks
* Secure JSON webhook callback validation
Adds pre-persistence validation for JSON payment callbacks before any transaction lifecycle work or storage. This change bounds callback input in the IPN entrypoint, introduces a JsonWebhookAuthenticatorInterface, and validates Stripe webhook signatures through a reusable method before processing. It rejects unsupported/invalid gateways and oversized or malformed payloads, and adds unit coverage for missing, invalid, expired, malformed, oversized, and valid signed callbacks.
* Harden payment return flow
Route gateway return and cancel URLs through a dedicated `/invoice/payment-return` page that only redirects to fixed invoice destinations. This removes the legacy session restore token flow, avoids exposing session capability in payment handoffs, switches PayPal returns to browser redirects (`rm=1`), and adds unit coverage for the new payment return and gateway URL behavior.
* Harden admin transaction error rendering
Replaced inline `onclick` handling in the invoice transactions template with escaped `data-transaction-error*` attributes, and added a delegated click listener in `fossbilling.ts` to show messages safely. This removes executable inline attributes while preserving diagnostic text.
Added unit coverage for both Twig and admin JS to ensure hostile payloads stay as plain text, no script/image injection is introduced, and dynamically inserted error links still behave correctly.
* Harden email confirmation token lifecycle
Refactors client email confirmation to be transactional and replay-safe by binding tokens to the current email hash, enforcing expiry, and atomically consuming/revoking tokens during approval. Email changes from both admin and profile flows now revoke outstanding confirmation links and reset `email_approved` consistently, including concurrent update scenarios.
Adds a dedicated `EmailConfirmationTest` suite covering replay prevention, stale/legacy/expired token rejection, resend behavior, rollback safety, and profile-change edge cases, while removing outdated unit tests that no longer match the new confirmation flow.
* Harden invoice PDF logo fetching
Invoice PDF generation no longer allows Dompdf to reach remote network sources. Logos are now fetched through a no-private-network client, validated for MIME, size, and safe SVG content, and embedded as data URIs when allowed. Malformed, private, redirecting, or unsafe logo URLs are ignored without interrupting invoice rendering, and the new unit tests cover the security and compatibility cases.
* Harden guest login/signup CSRF checks
Adds a dedicated guard for guest `client/login` and `client/create` API calls to require POST, validate the pre-login session CSRF nonce, and block cross-site origins/fetch contexts before dispatch. Client-facing login/signup templates now include `CSRFToken` hidden inputs, and test helpers (PHP + Playwright) fetch `/login` first to reuse the anonymous session token. Unit tests were expanded to cover accepted/rejected authentication request paths and token propagation in shipped forms.
* Avoid persisting extension config on reads
Stop `Extension` config lookups from creating empty `ExtensionMeta` rows, and gate guest settings to core or active modules before reading config. Tests were updated to cover the non-writing read path and to ensure authorized config writes still preserve public settings.
* Harden extension archive extraction
Improve `downloadAndExtract()` security and cleanup behavior by staging extraction in a private temp directory, using cryptographically secure temp names, validating all ZIP entries before extraction (path, null-byte/colon checks, and allowed file types), and removing staged artifacts on failure. After successful validation/extraction, permissions are restored before moving to the final extension path.
Adds unit tests covering rejection of unsafe archive entries before any write occurs and confirming valid package files/directories still extract correctly.
* Harden extension directory data handling
Adds strict validation for extension IDs and catalog metadata in `ExtensionManager`, including cached responses, and rejects unsafe author URLs/schemes. Updates the admin extensions template to remove inline `onclick` usage and load README content via modal event data attributes, preventing injected names from becoming executable script. Includes new unit tests covering malformed directory payloads, cached validation behavior, safe rendering of quoted names, identifier pre-validation, and valid metadata edge cases.
* Harden client password reset revocation
Move client password changes and reset issuance into `ClientPasswordResetRepository` so password updates and reset-token revocation happen atomically. Admin, guest, and profile password changes now reuse the same transactional path, preventing stale recovery links from overwriting newer passwords and replacing outstanding reset tokens safely across supported drivers. Added unit coverage for revocation, rollback, stale-token rejection, and driver-specific locking behavior.
* Harden staff password reset transactions
Refactors admin password changes to go through `AdminPasswordResetRepository` so password updates and reset-token revocation happen atomically in one transaction. Profile and staff password-change flows, plus guest reset redemption/request flows, now reuse repository methods (`changePassword`/`replaceReset`) instead of manually persisting/removing entities.
Adds row-lock based eligibility rechecks (including SQLite-specific lock handling) to prevent stale or preloaded reset tokens from being reused across concurrent updates. Unit tests were updated and expanded with a new repository test suite covering revocation behavior, expiry/eligibility checks, rollback safety, side-effect failures, and concurrency races.
* Prevent public installer reinstall when debug is enabled
* Fix strict-variables signup test context for CSRFToken
* Fix PHP 8.5 SVG logo detection and Rector findings
Route image/svg+xml reported by getimagesizefromstring to the self-contained SVG validator instead of the raster path. Apply ReturnNeverTypeRector and ReadOnlyAnonymousClassRector suggestions in tests.
* Satisfy PHPStan for SVG mime check in PDF logo fetch
* Add nosniff and pjpeg to spellcheck dictionary
* Reuse CSRF cookie for pre-login API tests
Update the test API helper to read the pre-login CSRF token from the cookie jar before falling back to parsing the login page. This keeps signup and other back-to-back guest requests on the same session, even when `/login` redirects or the session id rotates.
* Send CSRF token in Playwright stale-login check
The password-change test asserts stale credentials are rejected with an invalid-credentials message. Since guest logins now require the pre-login session nonce, submit the double-submit cookie value like the real login form does; otherwise the request is rejected before credentials are checked. This also avoids the retry that pushed the suite over the client_signup rate limit.
* Address review findings: rate-limit ordering, staging cleanup, README race
- Run guest CSRF validation after the rate limiter so invalid-token floods still consume api_login/api_guest budgets.\n- Remove extension staging dir and archive when destination resolution, the already-installed check, or the final move fails.\n- Ignore stale extension README responses that arrive after another extension was selected.
* Check stale passwords from a fresh session in Playwright
Changing a password invalidates every session including the current one, so the old session can no longer pass CSRF validation. Run the stale-password login from a fresh anonymous context with its own pre-login nonce, like a real login attempt.
* Support full Unicode in custom pages and MySQL defaults
* Recognize charset migration terms in spellcheck
* Keep charset repair guidance in the pull request
* ✨ Route offline payments through explicit admin approval
Replace the fragile stored-IPN check for manual gateways with a
capability split: automated adapters verify callbacks, offline gateways
settle through a permission-checked approve action and state machine.
Centralize IPN decoding so typed adapter signatures never see null, give
adapters explicit error codes, and remove the dead validate_ipn flag.
* Satisfy Rector: drop redundant casts, readonly test double
* Make approval E2E immune to IPN-hash dedupe collisions
* Reword E2E variable flagged by spellcheck
* 🐛 Heal promo schema drift behind #4386/#4401 and surface sync failures
Add patch127 with the promo columns (requires_products, auto_apply,
priority, stackable) plus the auto_apply index that #4386 and #4401
shipped without a migration, crashing promo loads with "Unknown
column 't0.requires_products'" on installs the portable sync didn't
heal. Surface the drift state and last failed sync in the
finalization status (with a finalize-screen warning) so a repeatedly
failing sync is visible instead of living only in the update log.
Fixes#4433
* 📝 Allow LONGTEXT in spellcheck dictionary
Required by patch127's promo migration DDL.
* 🐛 Report schema drift independently of the sync retry cooldown
isSchemaOutOfSync() honors the cooldown, which hid the drift status
(and the finalize-screen warning) for an hour after each failed
sync. Status now uses a cooldown-ignoring hash check, and the warning
stays visible once finalized with drift remaining.
* Merge renewal invoices sharing a client, currency, due date, and period
Adds an opt-in renewal merge for the batch generator (#4118): orders
sharing a client, currency, due date, and billing period go onto one
invoice instead of one invoice per order. Precedence is per-order meta
override, then the client's tri-state preference, then the global
invoice_merge_renewals setting (off by default). Orders paid through
an active gateway subscription keep their one-invoice shape.
Grouping happens upstream of generation, so every order is invoiced
exactly once and stamped with the shared invoice id (no #1494/#1969
repeat). Each merged invoice is a fresh draft, so invoice immutability
is never violated.
* Deslop the renewal-merge diff
* Address review: boolean normalization, atomic merged invoices, settings off-switch, E2E flag restore
* Retry bucket orders singly when merged generation fails, issuance outside generation
* Reset the entity manager and stop the batch when merged generation closes it
* Allow clients to belong to multiple client groups (#4387)
* Use spellcheck-clean identifiers in multi-group E2E
* Renumber multi-group migration to patch125 after main merge
* Halve guest logins in multi-group E2E for the shared api_login budget
* Only migrate group assignments that reference existing groups
Reset the PHP opcode cache after extracting the update archive, so the next request's automatic finalization cannot run against pre-update classes and stamp the state finalized without applying patches.
Also heal a finalized state that still reports pending patches on the next request, keep offering finalization on the finalize screen until the patch count reaches zero, and report the database/code patch levels in the completion guard's error.
* Bind PayPal payments to their invoice with signed callback URLs
The PayPal adapter decided which invoice to credit from the invoice_id
query parameter inside notify_url. That URL is rendered into the buyer's
payment form as an ordinary editable hidden field, and neither the IPN
authenticity check nor the amount validation ever confirmed which
invoice the payment was for, so an edited callback URL could credit a
genuine payment to a different invoice.
Sign the gateway/invoice pair into notify_url when the payment form is
built (HMAC-SHA256 with the install salt) and verify the signature
before the transaction is bound or credited. Callbacks issued before
signing existed are still honored for notifications tied to an
already-stored subscription on the same invoice, so existing recurring
profiles keep working. PayPal's echoed item_number/invoice references
are cross-checked against the credited invoice as defense in depth.
* Honor unsigned PayPal callbacks continuing pre-upgrade payments
Strict signature verification rejected delayed notifications for
payments made before callback signing existed: completed eChecks and
refunds arriving at the original unsigned notify_url. Accept an
unsigned callback when it names a PayPal transaction id already
recorded for the same invoice, which a fresh forged binding cannot
produce since notification contents are PayPal-grounded.
* Track main's invoice issuing rename in new PayPal binding tests
Main replaced isApproved/approveInvoice with isIssued/issueInvoice;
the two tests added for the callback binding still stubbed the old
name.
* Add invoice cancel and immutability flow
Introduce admin invoice cancel/void support with new before/after events, UI actions, and API/service handling for approved unpaid invoices. Consolidate legacy invoice edit/delete toggles into a single `invoice_immutability` setting, tighten issued-invoice edit rules, update expired unpaid invoice cleanup to void issued invoices instead of deleting them.
* Rename invoice approval flow to issuing
* Refine invoice draft and void workflows
* Add invoice journal history tracking
Add an invoice event journal to capture lifecycle changes with trimmed snapshots, expose it through the admin API, and replace the invoice billing-parties tab with a history view and JSON download.
This also adds upgrade patches and a backfill so existing installs get a baseline journal entry for invoices that predate the feature, plus tests and schema snapshot updates for the new entity.
* Fix client page rendering and JSON encoding
Move client related-list queries to the tab scope so shared footer blocks can access them under Twig strict_variables, and add a regression test for the manage page render path.
Also harden JSON output in invoice/update patch flows by throwing on journal encoding failures and substituting invalid UTF-8 in legacy invoice snapshots so one bad row does not block updates.
* Harden invoice journal follow-ups: consumed-keys changes, activity log lines, translatable labels, E2E coverage
* Fix datepicker clear button pushing calendar icon over the field value
* Use DBAL 4.5 introspectTableByUnquotedName in invoice patch helpers
* Harden invoice issuing and payment recording
Remove the orphaned tax_setup_eu endpoint left behind by #1826, whitelist
draft statuses, guard first issuance to unpaid drafts, snapshot auto-issued
funds invoices, and record the paid journal after the admin paid_at override
* Harden invoice money paths
Manual refunds no longer log, journal, or dispatch as performed: the skip
is recorded distinctly. payInvoiceWithCredits returns the real outcome and
the batch variant counts covered/skipped/failed with per-invoice warnings.
Processed transactions freeze their history fields, refuse deletion, and
clean up orphaned balance rows on unprocessed deletes; transaction linkage
refuses canceled/refunded/replaced invoices. processInvoice rejects paid
invoices, and the expiry cron rechecks deletability under the row lock.
rmByClient is documented as intentional erasure and pinned by test.
* Remove dead legacy IPN pipeline, serialize transaction processing
The legacy process() pipeline (_parseIpnAndApprove, _debit/_refund/_subscribe/
_unsubscribe, amount/currency validation, debitTransaction) had no production
callers: live processing runs through gateway adapters via preProcess. Delete
~350 lines of unreachable payment code plus its tests and the now-unused
findOneProcessedByTxnId finder, and wire the atomic claimForProcessing into
preProcess, createAndProcess, and processAndCatchErrors so concurrent
deliveries converge on a single processor.
* Expand invoice coverage and finish consistency sweep
Number-series fallback paths, non-default currency payment and missing-rate
refusal, tax-rate effect on totals, batch activation isolation and batch
delete fail-fast, client pay/PDF controller auth matrix, expiry-cron
disabled and isolation paths, zero-total and already-paid email skips, and
the reissue replacement email variable. Billing-channel debug lines and the
prepare instant-issue warning now carry structured context.
* Pin no-gap guarantee for rolled-back number reservations
* Document journal retention, log base-currency rewrites on issued invoices
* Record offline refund documents in manual refund mode
Manual mode no longer returns null while the UI reports success: refunding a
paid invoice now generates a credit-note-series document marked as an offline
refund (note plus journal flag), so money moved outside FOSSBilling leaves a
paper trail. Unknown refund-logic values fail loudly instead of silently
skipping.
* Complete GDPR erasure: anonymize transactions, cascade support cleanup, refuse deletion with active services
Detached transactions are anonymized in place on client erasure (IP, raw
IPN payloads, notes scrubbed; financial record kept). Support erasure now
removes notes, messages, and edit history instead of orphaning them.
Client deletion is refused while provisioned service rows exist, forcing
explicit cancellation first. Also quotes the reserved table
name portably: invoice deletion and the transaction counter/pickup were
syntax errors on SQLite.
* Add live client-erasure walkthrough test
* Bypass rate limiter in development, fix antispam E2E state pollution
* Remove stale Invoice draft comments
* Fix spellcheck findings: rename UPDATEABLE_FIELDS, reword coinages
* Tolerate missing service tables in client-deletion gate
* Exercise promo add-remove on drafts in E2E
* Apply Rector suggestions in invoice tests
* Address PR review: single-claim transactions, frozen seller snapshot, issuance guard
* Trim repeated no-reclaim comments in Stripe adapter
* Run schema sync before journal backfill on upgrade
* Address second review round: claim quoting, patch recovery, bounded journal healing
* Serialize journal baseline writes with a per-invoice claim
* Surface instant-issue failures instead of returning a silent draft
* Adopt Doctrine DBAL 4.5 and ORM 3.7 APIs, drop deprecated calls
Migrates off every DBAL 4.4/4.5 and ORM 3.7 deprecated API in use: introspectTableByUnquotedName() replaces introspectTable()/listTableColumns()/listTableIndexes(), SortOptions:: and all ORM orderBy()/addOrderBy()/OrderBy directions move to the native SortDirection enum. Also pins doctrine/dbal ^4.5 explicitly in composer.json.
* Use hasColumn()/hasIndex() for the transaction IPN-hash support check
Compares quoted string representations could miss on platforms where introspected names are marked quoted. Table::hasColumn()/hasIndex() normalize identifiers, so the check is correct on every driver.
* Reduce duplicate database queries during page rendering
* Fix Rector and spellcheck failures in query optimizations
* Limit lookup caching around cron tasks and direct metadata writes
* Use the real System service class in cron test mocks
* Upgrade Tabler to 1.6 and adopt its admin components
* Fix spellcheck for Tabler integration notes and tests
* Build admin widget assets before Playwright tests
* Fix date input synchronization and clipboard fallback
* Refresh clipboard instances after mapping legacy targets
* Support Tabler clipboard attribute alias in fallback
* Partial credit notes and debit notes
Partial refunds via line/qty selection with amount tracking on the original (paid until fully refunded), transactional issuance, modal UI, corrected income/refund stats.
Debit notes as separate payable documents with own DN series, inert lines, cross-references, labels, and notification email.
Drops the one-CN-per-invoice unique constraint (partial refunds need many CNs per original) with a legacy cleanup patch; double-issuance stays guarded by the invoice lock plus paid-status check.
* Deslop partial/debit notes
Remove an unused service instance in the partial-validation test, drop an unused client token in the partial E2E, and fix the missing separator before the appended E2E case.
* Address review: per-line remainders, debit transactions, input validation
Track credited quantities per source line (refunded_item_id) and validate against remainders in both refund paths; full refunds credit exactly what remains.
debitInvoice validates lines before any write and runs creation, numbering, and notes in a transaction with a locked status recheck.
Admin refund rejects malformed items input; refund modal split into full-refund action plus capped partial inputs fed by per-line remainders.
Client detail resolves related invoice numbers server-side in one query instead of per-note API calls.
* Harden null handling across core, adapters, and templates
Guard the currency fraction-digit lookup when zlib is unavailable, map unknown GeoIP country codes to incomplete records, tolerate object app options and controllers without a register method, default missing ResellerClub and PayPal IPN fields, and hide server rows when a hosting order has no server.
* Guard non-string GeoIP country codes before enum lookup