2 Commits
Author SHA1 Message Date
Adam Daley 56aed6bec5 Allow custom recurring billing periods (#4102)
* ✨ Allow custom recurring billing periods

Products were limited to a fixed set of 7 hardcoded recurring billing
periods (1W/1M/3M/6M/1Y/2Y/3Y), stored as fixed columns on
product_payment. Admins can now configure any number of arbitrary
periods per product (e.g. 45 days, 18 months, 5 years) within
Box_Period's existing valid ranges, while the original 7 periods
remain as pre-filled defaults and one-click quick-add presets.

- Replace product_payment's fixed w/m/q/b/a/bia/tria columns with a
  new product_payment_period table (one row per configured period),
  migrated via UpdatePatcher::patch98().
- Rework the admin product pricing UI to add/remove billing periods
  by quantity + unit (day/week/month/year), in addition to the
  existing presets.
- Fix Box_Period's constructor, which only ever accepted a
  single-digit quantity (e.g. "9Y" but not "24M" or "45D") despite
  its own unit bounds implying otherwise.
- Fix period labels silently disappearing in the client checkout,
  admin order form, and admin subscription edit screens for any
  period outside Box_Period's 9 presets, by having the API return a
  precomputed title per period instead of relying on a second lookup.
- Fix the Stripe adapter dropping the period quantity entirely when
  creating recurring prices (e.g. quarterly/biannual products were
  silently billed monthly), and reject periods that exceed Stripe's
  own interval_count caps instead of mis-billing them.

Closes #4098

* Address review feedback on custom billing periods

- Make patch98's period migration INSERT idempotent with ON DUPLICATE
  KEY UPDATE, so a rerun after a partial failure doesn't hit the
  (product_payment_id, code) unique constraint.
- Order ProductPayment::$periods by sortOrder so the admin's edit
  order is preserved on reload, matching the pattern already used by
  ServiceDownloadable's ordered collection.
- Wrap the Box_Period parsing failure in
  getEnabledProductPaymentPeriod() so a malformed/missing period
  raises FOSSBilling\InformationException instead of a raw exception,
  matching applyPricingToProductPayment()'s handling.
- Fix the Stripe adapter reusing a wrong-cadence price: Stripe's price
  list filter only supports 'interval', not 'interval_count', so the
  existing-price match now also compares interval_count explicitly.
- Stop the admin pricing form from resurrecting default periods an
  admin had deliberately removed - defaults are now only used when a
  product has no configured periods at all.
- Drop the redundant product_payment_id_idx index, since the unique
  key on (product_payment_id, code) already covers lookups on
  product_payment_id via its leftmost prefix.

* Fix quantity validation and i18n in the add-period JS

- Use Number() + Number.isInteger() instead of parseInt() to validate
  the custom period quantity, so a fractional value like "5.7" is
  rejected instead of silently truncated to 5.
- Replace the English-only '(s)' suffix hack for pluralizing the unit
  label with translated data-singular/data-plural attributes on each
  unit option, selected based on quantity.
2026-08-06 21:39:00 +01:00
Adam Daley dd3dcbfd52 Enhance type safety, authentication checks, and testing framework (#3701)
* Improve type safety, auth checks and PHPStan fixes (#3250)

* Improve type safety, auth checks and PHPStan fixes

* Remove @phpstan-ignore annotations

* Improve type safety, auth checks and PHPStan fixes

* Remove @phpstan-ignore annotations

* Remove redundant @phpstan-ignore comment

* Improve code robustness and reliability (#3248)

* Harden code against null/undefined values

* Update dependency lockfiles

* Sanitize ticket input and use IDs in email list

* Restructure tests and add test utilities

* Move E2E tests to `Library\E2E` namespace

* Update `php-build` action ref in workflows

* Switch tests to Pest and add test scaffolding

* Refactor E2E tests to Pest-style tests

m

Skip E2E tests when env not configured

* Refactor Unit tests and update test config

Add test datasets and refine test helpers

* Add DEBUG guard, fix API JSON decode, update tests

* Refactor tests: add types and Mockery expectations

* Adjust tests, phpstan config, and composer script

* Instantiate test APIs locally, remove beforeEach

* Refactor runtime checks, types & logging

* Apply PHP Code Styling Changes

m

* Add PHPStan ignores, test renames & small fixes

* Enable Pest tests in CI workflow

* Remove legacy tests; add new unit tests/config

* Avoid undefined index in fingerprint comparison

* Ignore properties absent in both fingerprints

* Remove public_ticket_create API method

* Add Fingerprint unit tests

* Remove em dash from fingerprint test name

* Switch to Pest; assorted bug fixes & refactors

* Prefer getDi() and guard DEBUG define

* Refactor tests: DBAL, Antispam rename, helpers

* Refactor email tests to use Doctrine entities

* Update tests, use DBAL mocks & improve Formbuilder admin

* Validate plan_id and update unit tests

* Replace error_log with DI logger and update tests

* Expect db->store in Invoice Service test

* Use DI logger instead of error_log; fix tests

* Fix typo in Hook Service test

* Fix Staff test spelling fixture

* Complete Pest test migration

* Fix Product test spelling fixtures

* Fix Product test spelling fixtures

* Run live tests with Pest and load helpers

* Migrate tests to ApiClient; remove legacy APIHelper

* Fix Email test spelling

* Remove unused PHPUnit bridge

* Organize Cypress end-to-end tests

* Document Pest test directory root

* Remove noisy Pest comments
2026-06-02 07:43:01 +01:00