Yoyaku already has a policy system: configurable rules attached to a resource or its type through a Policies tab, run by ConstraintPolicyManager, three of them shipped. The first version of this issue added a fourth rule of the same family entirely outside it, as two base fields on the tier and the tariff, a bespoke check inside holdGroup() and an exception class of its own. That was the wrong shape, so this rebuilds it as the rule the system was already for, and moves the system to where its subjects live.

The rule

TierPerTransactionLimit caps how many places of a tariff a single transaction may hold. A selling rule, not an allocation: the quota bounds what everyone together may buy, this bounds what one buyer may take out of it, which is what stops one order clearing the reduced seats before anyone else reaches them. Its number is the attachment own max setting, edited in the policy form like every other policy, so no field is added to any entity.

Where policies live

The plugin type, its attribute, base class, manager, resolver, contexts and phases move from yoyaku_order into the engine. Every host a policy attaches to is an engine entity, and the newest rule needs no booker at all, so a subsystem attaching to engine entities and refusing engine operations does not belong in the module whose subject is the customer. The screens move to yoyaku_ui rather than core, because that is where every other admin screen already is and because core ships no routing file.

The rules that do need a booker stay in yoyaku_order and extend a new BookerPolicyBase, which carries the count by dimensions. The skip constraints waiver follows the policies down onto the transaction, since what it waives is now the engine business.

Where policies attach

Four hosts, each with a Policies tab: the resource type, the resource, the tariff, and the tier. They merge rather than override. Every attachment reaching a booking is asked and any of them may refuse, so two ceilings need no precedence rule: the tighter one refuses first. The engine already settles overlapping availability bounds the same way, asking every provider and taking the lowest.

Attached wider than what it counts, a policy applies to each narrower thing rather than to their sum. A per-tariff limit attached to a resource caps each of that resource tiers, it does not cap the resource as a whole. That matches how SlotPerBookerLimit already reads at that width.

When policies run

A policy declares its phases, and the phase belongs to the plugin rather than to the site: offering the choice would let somebody select a moment at which the rule cannot be answered at all. Hold runs before any capacity is taken, so a rule about the basket alone refuses the click rather than the checkout. Transaction runs wherever the assembled transaction is validated, which is not one moment but four: the registration form, a workflow review step, an operator moving the order, and confirmation. Only that phase can answer a rule that needs the booker, because at hold time nothing names one yet.

Two defects fixed on the way

  • The group check ran only above the slot locks, so two concurrent requests reading a transaction that holds four of a tier capped at five would each see room for one more and both commit. It now runs again under the locks, before any line of the group is saved. This was present in the version that was queued to merge.
  • A refusal reached the seat map as taken, greying out a seat that is in fact free and that the next click would prove bookable. It now travels as PolicyRefusedException, distinct from a full tier or an occupied place, and the map states the reason and leaves the seat alone.

Telling a surface the number

A policy that caps a count implements CeilingProviderInterface, and ConstraintPolicyManager::ceilingFor() returns the lowest ceiling any attachment puts on a tier. A quantity control sets its maximum from it and says so, because a control that silently refuses to go further reads as broken. The figure is advisory: the refusal on hold is what decides, and it re-checks under the lock.

Out of scope

A per-resource total, meaning at most N places of this hall per transaction whatever the tariff, counts a different thing and is not built here. Gating payment on policy violations is a separate change spanning orchestra_payment and is filed on its own.

Issue fork yoyaku-3614349

Command icon Show commands

Start within a Git clone of the project using the version control instructions.

Or, if you do not have SSH keys set up on git.drupalcode.org:

Comments

mably created an issue. See original summary.

mably’s picture

mably’s picture

Status: Active » Needs review
mably’s picture

Title: Limit how many places of one tariff a single cart may hold » Cap what one transaction may take of a tariff, and move the policy system to the engine
Issue summary: View changes

  • mably committed fe5788a0 on 1.x
    feat: #3614349 Cap what one transaction may take of a tariff, and move...
mably’s picture

Status: Needs review » Fixed

Now that this issue is closed, review the contribution record.

As a contributor, attribute any organization that helped you, or if you volunteered your own time.

Maintainers, credit people who helped resolve this issue.

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.