784 Commits
Author SHA1 Message Date
Adam Daley 1b3550a6e6 Include homepage and custom pages in XML sitemap, fix product lastmod (#4566)
* Include homepage and custom pages in XML sitemap, fix product lastmod

* Add MySQL migration for custom_pages.updated_at (patch131)
2026-10-22 18:06:42 +01:00
Adam Daleyandopencode 40de3ca924 Fix WHM activation failing when package already exists (#4565)
* 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>
2026-10-22 17:03:35 +01:00
Adam Daley 025ebc0850 Harden guest session-state API calls against cross-site requests (#4546)
* 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.
2026-10-18 03:05:01 +01:00
Adam Daley 8c50583a91 Document UGX exclusion and hardcode Stripe decimal lists (#4542)
* 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.
2026-10-17 23:04:22 +01:00
terminalstate 304b731f4e Send Stripe amounts in Stripe's units for COP, HUF, IDR, ISK, PKR, UGX and other currencies (#4541)
* 🐛 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
2026-10-17 22:47:04 +01:00
Adam Daley db0c3dedf2 Harden Stripe renewal paid-skip for inconclusive billing reasons (#4536)
* 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
2026-10-16 02:11:04 +01:00
terminalstateandAdam Daley 7058313b07 Record Stripe renewal payments after the first invoice is paid (#4523)
🐛 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>
2026-10-16 01:10:59 +01:00
Adam Daley 1a469d09cf Fix cron-sent emails rendering dates in en_US instead of configured locale (#4535)
* 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.
2026-10-16 00:02:51 +01:00
Adam Daley a49cf8fd5f Soften PayPal item_number echo check to a warning (#4534)
🐛 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
2026-10-15 22:48:46 +01:00
Adam Daley 6b18ba6a23 Refresh default branding and migrate legacy asset paths (#4527)
* 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
2026-10-14 20:26:19 +01:00
Adam Daley 5489acb148 Allow custom payment gateway images (#4519)
* Add custom payment gateway images

Allow image uploads and URL imports with AVIF and PNG browser fallback. Preserve sanitized SVG artwork and keep custom images outside packaged assets.

* Fix gateway image CI checks and upgrade coverage

* Limit gateway raster images to four million pixels
2026-10-12 18:23:10 +01:00
Adam Daley bd7e9eab30 Normalize HTTP bootstrap query overrides (#4495) 2026-10-06 23:22:25 +01:00
Adam Daley f5aac2ac36 Serve stale extension directory data on refresh failures (#4493)
* 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
2026-10-06 23:06:05 +01:00
Adam Daley eb95b72f0f Respect Formbuilder permissions on product settings (#4494) 2026-10-06 22:48:48 +01:00
Adam Daley 87200f9bf1 Harden extension directory partial against sparse metadata (#4491)
* 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.
2026-10-06 21:51:22 +01:00
Adam Daley 95243bf71d Keep DirectAdmin POST fields out of request URLs (#4490) 2026-10-06 21:21:53 +01:00
Adam Daley 30faee0c64 Fix CWP suspension error message placeholders (#4487) 2026-10-06 20:38:11 +01:00
Adam Daley c690a0dc5b Handle missing extension directory icons (#4485)
* Handle missing extension directory icons

* Make extension directory test helper readonly
2026-10-06 18:20:31 +01:00
Adam Daley 3d0410bdfe Handle missing Internet.bs registrant fields (#4483) 2026-10-06 15:15:00 +01:00
Adam Daley 3c3e6951b6 Normalize phone numbers before registrar requests (#4482) 2026-10-06 12:25:55 +01:00
Adam Daley cd7b65b04f Harden security boundaries across auth, payments, and extensions (#4474)
* 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.
2026-10-05 00:08:12 +01:00
Adam Daley a7a219e5c9 Preserve order configuration when editing form answers (#4472) 2026-10-04 20:50:43 +01:00
Adam Daley 91028217a8 Report invalid product pricing types as configuration errors (#4471)
* Report invalid product pricing types as configuration errors

* Avoid waiting for external images in admin smoke test

* Recognize Playwright navigation event in spellcheck
2026-10-04 20:06:46 +01:00
Adam Daley cac9e8b383 Validate missing Hestia hostnames before building API URLs (#4470)
Validate Hestia hostname before using the server manager
2026-10-04 19:00:12 +01:00
Adam Daley 2ae683de46 Validate invoice counters and check existing settings during upgrades (#4468)
* Validate invoice counters and warn about invalid upgrade settings

* Remove redundant cast from invoice counter overflow test
2026-10-04 17:58:58 +01:00
Adam Daley 83a676f5d9 Fix Unicode storage in custom pages and MySQL defaults (#4467)
* Support full Unicode in custom pages and MySQL defaults

* Recognize charset migration terms in spellcheck

* Keep charset repair guidance in the pull request
2026-10-04 17:29:40 +01:00
Adam Daley c3b5631e7d Handle invalid CWP responses and malformed PayPal IPNs (#4464)
* Handle invalid CWP responses and malformed PayPal IPNs

Reject incomplete CWP account details before synchronization and explain empty or invalid JSON responses. Reject array-valued PayPal IPN fields before verification.

Add regression coverage for failed responses, unchanged accounts and valid synchronization. Addresses FOSSBILLING-NN9, FOSSBILLING-CMA, FOSSBILLING-NJD, FOSSBILLING-JEW and FOSSBILLING-QMT.

* Report CWP HTTP failures separately from invalid JSON
2026-10-04 16:25:04 +01:00
Adam Daley 71b4d08300 Route offline payments through explicit admin approval (#4453)
* ✨ 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
2026-10-03 06:10:42 +01:00
Adam Daley 5372e6b4ea Fix client group migration on PostgreSQL and SQLite (#4450) 2026-10-03 01:22:37 +01:00
Adam Daley 130046b5d5 Fix stale DDEV autoloader check and patch125 client group FK migration (#4446) 2026-10-02 17:34:52 +01:00
Adam Daley 50c379eeb1 Fix PayPal payments stuck on processing from double claim (#4437) 2026-10-01 13:42:05 +01:00
Adam Daley a83fc69e7a 🐛 Heal promo schema drift behind #4386/#4401 and surface sync failures (#4436)
* 🐛 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.
2026-10-01 12:36:04 +01:00
Adam Daley 668fb8c687 Merge renewal invoices sharing a client, currency, due date, and period (#4432)
* 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
2026-09-29 20:52:14 +01:00
Adam Daley 256751b831 Allow clients to belong to multiple client groups (#4405)
* 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
2026-09-29 18:58:44 +01:00
Adam Daley c76530ee68 Fix spellcheck failures on main (#4427) 2026-09-29 02:56:06 +01:00
Adam Daley 8e47d9c89b Fix Sentry-reported runtime errors and filter third-party adapter noise (#4426) 2026-09-29 01:57:41 +01:00
Adam Daley 3a3ec8bcd2 Heal stuck update finalization after stale-code patch runs (#4425)
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.
2026-09-28 22:53:21 +01:00
Adam Daley 97295484d8 Bind PayPal payments to their invoice with signed callback URLs (#4424)
* 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.
2026-09-28 19:28:47 +01:00
Adam Daley e8f375db3b Invoice immutability, issuing model, and audit journal (#4423)
* 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
2026-09-28 18:51:24 +01:00
Adam Daley 78b1a23d84 Adopt Doctrine DBAL 4.5 and ORM 3.7 APIs, drop deprecated calls (#4422)
* 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.
2026-09-28 05:54:15 +01:00
Adam Daley fac2cbf7fb Reduce duplicate database queries during page rendering (#4415)
* 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
2026-09-26 15:39:14 +01:00
Adam Daley 6415402ec2 Upgrade Tabler to 1.6 and adopt its admin components (#4412)
* 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
2026-09-26 13:08:15 +01:00
Adam Daley 0916670231 Fix invoice reissue schema drift on upgrades and cron (#4407) 2026-09-25 16:57:23 +01:00
Adam Daley 67756a43cd Verify staff basket price overrides (#4403)
* 🔒 Verify staff basket price overrides

* 🐛 Fix update patcher test spelling

* 🐛 Apply staff price overrides to promo discounts

* Update ServiceTest.php

* Fix percent promo invoice discount base

* Base % invoice promos on order line totals

* Clarify invoice promo discount comments
2026-09-24 04:05:17 +01:00
Adam Daley 14e9c49cc2 Remove unused install/sql/structure.sql (#4402)
* Remove unused install/sql/structure.sql

* Deslop retargeted schema tests

* Correct FK comments to distinguish dump from metadata

* Add parameterless to spellcheck dictionary
2026-09-24 01:43:42 +01:00
Adam Daley 161f1328b8 Replace legacy hooks with typed Symfony events (#4400)
* Add typed Symfony events alongside legacy hooks

* Move admin login activity to typed event listener

* Move client login activity to typed event listener

* Add typed events for cron currency and notifications

* Add typed extension lifecycle events for widget cache

* Avoid duplicate typed listener registration

* Add typed events for settings subscriptions and client deletion

* Migrate core cron hooks to typed Symfony listeners

* Migrate support ticket hooks to typed events

* Migrate client and profile lifecycle hooks to typed events

* Migrate authentication hooks to typed events

* Migrate staff checkout and order lifecycle events

* Migrate cart, order batch, invoice, and staff reset hooks

* Migrate extension, invoice, system, and service hooks

* Retire legacy hook runtime and finish reminder events

* Clean up retired hook data during upgrades

* Clarify reminder events and complete gateway cancellation

* Document current typed event development model

* Apply Rector suggestions for typed event changes
2026-09-24 01:19:05 +01:00
Adam Daley 30fa01bdcc Fix schema drift on same-version updates (#4395)
* Fix schema drift on same-version updates

* Address review: lock-free hash check, content hash gate, sync retry cooldown

* Deslop review follow-ups

* Fix spellcheck: avoid unknown word in docblock

* Include entity content hash in Doctrine metadata cache namespace

* Honor sync retry cooldown in the out-of-lock drift check
2026-09-23 18:22:02 +01:00
Adam Daley 87cbebb021 Fix domain list truncation and add server-side sorting across admin lists (#4393)
* Paginate domain TLD and registrar lists

* Disable sorting for domain operations column

* Enable registrar table sorting

* Add allowlisted server-side sorting across admin

* Align sortable headers with default sorting

* Add test headers and trim sorting docs

* Keep tab list params scoped by namespace

* Reset active tab list params on re-click

* Fix tab list param sync

* Address review: stable sort tie-breakers, KB namespace, helpdesk IDs, header direction
2026-09-23 17:03:41 +01:00
Adam Daley 1b558268d9 Partial credit notes and debit notes (#4390)
* 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.
2026-09-22 00:05:06 +01:00
Adam Daley 2fbecd5b99 Harden null handling across core, adapters, and templates (#4384)
* 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
2026-09-21 11:55:52 +01:00