A booking that names no tariff is accepted on a resource that has them, uncapped and priced from the resource
Closes the hole by letting a resource declare that it sells tariffs, rather than having the engine infer it from whether a tariff row happens to exist.
The declaration
tariffs_enabled on yoyaku_resource, named after the existing
guarantee_enabled, with a sellsTariffs() accessor. Per resource only, not
resolved through the resource type and a site default: a lot of machinery for one
bit, and a resource type covering both kinds is normal.
The guard
BookingManager::hold() refuses a line naming no tariff when its resource sells
them. In the engine, beside the existing check, so every surface is covered at
once rather than hardening one form and hoping the next caller remembers.
The other half of "uncapped", found while building this
limitFor() fell back to the slot's capacity when a named tariff had no slot
tariff. That reads the room as one tariff's allocation, which would let each
tariff promise the whole house, and when the slot carried no capacity (the normal
state for a tariffed resource, since the generator gives the overall capacity
only to resources defining no tariffs) it left a tariffed line unbounded too.
The same fallback existed in the availability read, so page and hold agreed with each other while both were wrong. Both now fall back to the tariff's own quota. The room still bounds the total separately, against the slot-wide consumption, so the effective limit is the lower of the two.
Vocabulary that caused it
default_quotabecomesquotaonyoyaku_resource_tariff.priceis calledpriceat all three scopes without ever beingdefault_price: the entity name carries the scope, the field carries the meaning. And the field is now the allocation a slot tariff overrides, not merely a seed, so "default" was doubly wrong.assertCapacity()becomesassertFits(), since it asserts a quota and a capacity.assertLimit()'s$capacityparameter becomes$limit, andboundedAvailability()'s$cap/$slot_capbecome$quota/$capacity. All five helpers are private and referenced nowhere else, so this is free.
Refusing to orphan tariffs
A NoOrphanedTariffs constraint refuses turning the mode off while tariffs
exist, naming how many are in the way. Deleting them silently would throw away an
operator's pricing; leaving them silently would keep rows nothing reads. Only the
transition is guarded, so a resource that never sold tariffs is unaffected.
Interface
The Tariffs tab and its add form are gated on the declaration, via
_custom_access alongside the existing _entity_access, and the shortcut
operation is gated too so it cannot link to a 403. A resource selling tariffs but
holding none reads "it sells nothing. Add one." rather than showing an empty
table, because that is a legitimate mid-setup state. The resource's own price
field is hidden by #states when the mode is on, mirroring how
guarantee_enabled reveals its dependents.
Testing
TariffsEnabledTest, seven cases. The two refusal cases were run against
unfixed code and seen to fail before the guard existed; the four "must still
work" cases passed throughout, so the class discriminates rather than ratifying.
It covers the refusal, the refusal holding before any slot tariff exists, a
tariffed line still working, an untariffed resource still booking without one,
and the three constraint states.
Locally: phpcs clean (Drupal,DrupalPractice, --warning-severity=1), phpstan
at the CI ruleset reporting only pre-existing new.static, cspell introducing no
new word, and an eight-class smoke set green. The full suite is left to the
pipeline, since a field rename reaches most of it.
Migration
No update hook: pre-1.0, reinstall-only. An existing site derives the flag once,
on where a resource already has at least one tariff, and renames the
default_quota column in place. Note the limitFor() change binds where nothing
did before: a tariff with no slot tariff is now held to its own quota.
Not in scope
The quota-versus-capacity vocabulary is fixed only inside BookingManager's
private helpers. "rate" as a lingering synonym for tariff, and whether a venue
configuration should hide the capacity fields, are separate.