Skip to content

Resource types

Say a city runs three swimming pools and a theatre. The pools take bookings the same way: no payment, confirmed at once, one workflow. The theatre sells seats through a payment workflow. A resource type is what the three pools share: make a "Pool" type and a "Theatre" type, and each resource names its type.

A type answers the questions that belong to an order rather than to one resource. An order can hold bookings on several resources, paid once and driven by one workflow, so which workflow drives it cannot be a property of each resource: two resources could disagree. It is a property of their type.

What a type is

yoyaku_resource_type is a config entity with an id, a label, an optional tenant and its field rules. Without a tenant, the type is shared and any tenant's resources may use it. With one, it is that tenant's own. A resource references its type and keeps everything about itself: its label, capacity, slots, managers and the rest.

Types and tenants have admin forms of their own, and a type's field rules, policies and managers are tabs of it. Managing a type takes administer yoyaku: a resource's managers cannot change its type's settings, so how an order is driven and paid stays out of their hands.

What a type carries

The type knows nothing about workflows or payment. A setting that is not the type's own is kept in its third_party_settings, under the name of the module that owns it:

  • yoyaku_orchestra: the workflow profile, which names the workflow and whether orders confirm themselves.
  • yoyaku: the constraint policies attached to the type.

Settings that may differ between the resources of one order stay on the resource: booking cutoff, lead time, capacity, prices and guarantees, the calendar's selection rules, whether availability counts are shown. A type, a tenant or the site can still decide them for every resource below it, with a field rule. A resource holding inherit follows the rule, and FieldRuleResolver::getEffectiveValue() is the one read path every getter goes through.

Which workflow drives an order

WorkflowProfileResolver reads the workflow profile in this order, and the first one set wins:

the order's resource type   (third_party_settings of yoyaku_orchestra)
  -> the type's tenant      (third_party_settings of yoyaku_orchestra)
    -> the site             (yoyaku_orchestra.settings:workflow_profile)

The order's type is the type of its first booking still in play: a booking edited out of the basket never decides it. An order holds bookings of one workflow profile only. The SingleWorkflowProfilePerOrder constraint on the order refuses a second, and "no profile" counts as a profile of its own. So a cart may mix resources, and even types, as long as they are driven alike.

The workflow does not start on its own. A checkout surface, such as the calendar "Book" button or the cart's checkout, or an operator calls WorkflowStarterInterface::start($order), which starts it once and does nothing when it is already running. Whether the order is free or paid is a branch inside the workflow.

Which account takes the payment

PaymentAccountResolver::getAccountUuidForOrder() resolves the payment account from the order's tenant, then from the site:

the order's tenant   (its default_account_uuid)
  -> the site        (yoyaku_payment.settings:default_account_uuid)

An order belongs to one tenant, so the answer is always defined. An account is a merchant concern, which is why it hangs on the tenant and not on a shared type. A payment step in a workflow may still name an account of its own, which overrides the resolver for that step.