PaymentConstructorFields

The field catalog of the Payment Constructor cluster: it defines the reusable fields a payout is made of, and attaches them to individual constructors. This is the plugin that decides what gets calculated — the reports plugin then computes it.

Overview

The plugin owns two things:

  • A catalog of field definitions (/payment/constructors/fields/) — a workspace-scoped library where each field has a caption, a unique machine ident, a type, a data_type, a status, and an optional value (a source column, a formula, a SQL expression, or a constant).
  • The constructor-to-field attachments (/payment/constructors/{id}/fields/) — which catalog fields a given constructor uses, in what order, whether each one shows in reports, and which ones the total includes.

You define a field once in the catalog and attach it to as many constructors as you like; the reports plugin materialises a report column for it when it is activated.

Requirements

The three plugins of the cluster work only together. Each plugin class names the other two plugins in @pluginDependency.

Requirement Why the plugin needs it
PaymentConstructors plugin @pluginDependency and getDepends(); the link table has a foreign key to payment_constructors
PaymentConstructorReports plugin @pluginDependency and getDepends(); it validates each field and adds the report column
Modules plugin The plugin is a module. The install SQL writes to modules.
System plugin setting db_datetime_format The format of cdate
Theme package (Festi\Theme) DisplayPlugin and StageBuilder
Tables users_types, festi_* The install SQL gives the sections to the user type admin

The repository has no init.php file.

Installation

The link table has a foreign key to payment_constructors. Thus, install the cluster in this order:

  1. PaymentConstructors
  2. PaymentConstructorFields (this plugin)
  3. PaymentConstructorReports

Install the plugin with the Festi CLI:

./vendor/bin/festi-plugin --mode install --path ./src/dashboard/ --dump ./dumps/ \
    --plugin PaymentConstructorFields \
    --repository [email protected]:FestiPlugins/php_festi_plugin_payment_constructor_fields.git

This CLI example was not run for this page.

Set --path to the directory that contains the project config.php. Set --dump to the dumps directory of the project.

The command adds the repository as a git submodule. Then it copies install/install.<database type>.sql into the dumps directory as a new migration and applies it. Commit the submodule and the new migration together. On each other database of the project, apply the migration with ./vendor/bin/festi-migrate.

The install/ directory contains install.mysql.sql, install.pgsql.sql and install.mssql.sql. The install SQL makes these objects:

  • the payment_constructor_fields table and the link table payment_constructors2payment_constructor_fields;
  • the plugin row in festi_plugins;
  • the module payment_constructor_fields in modules;
  • four URL rules in the area backend;
  • three sections, with the permission for the user type admin.
Section Use
payment_constructors_fields The catalog page
payment_constructors_relating_fields The page with the fields of one constructor
payment_constructors_sql_fields The permission to write a field of the type sql

The schema is engine-clean and host-agnostic — nothing here is tied to a specific host beyond the Festi framework and the two cluster dependencies.

Configuration

The plugin has no module options.

Permissions on a multi-workspace host

The seed grants each section to the user type with ident admin, which is the right default for a host with one global role set. A host that keeps a separate role set per workspace (users_types.id_workspace) has to repeat those grants against its own per-workspace roles — otherwise the pages install correctly but no workspace user can open them. On such a host ident is not unique either, so the follow-up migration must qualify the role lookup with id_workspace.

Write those grants as a migration file of the project. Apply the migration with ./vendor/bin/festi-migrate.

Workspace URLs

Both pages also answer under the workspace prefix (/workspace/{id}/payment/constructors/...), and every link the plugin builds goes through IPaymentConstructorFacade::getWorkSpaceUrl(), which decides between the two. The store hands its tblDef links over as parameters. The prefix segment is configurable on the payment_constructors module — see the PaymentConstructors README.

Usage

  1. Open the list of payment constructors.
  2. Click Fields Configuration to open the catalog.
  3. Add a field. Select the type and the data type, and enter the value.
  4. Set the status of the field to active.
  5. Go back to the list and click Fields in the row of a constructor.
  6. Add the field to the constructor.

The form of an attached field has these controls:

Control Column Function
Field id_payment_constructor_field The catalog field. The list contains only active fields.
Show in Report is_report The reports grid shows the column.
Is Total is_total The total of the report includes the value.
Order Index order_index The position of the field.

The catalog form has the checkbox Is Decrypt (is_decrypt). When it is set, the reports plugin decrypts the value with the ICryptProvider of the host.

Field types

The type of a catalog field decides where its value comes from:

  • general — reads a physical table.column source (the mapping is the field's value). The columns on offer are the payment-user source's filterable columns minus enum/foreign-key ones; a host narrows them further by passing an allowlist as the $printableColumns of its PaymentUserSourceConfig. The table must be one of the allowed tables of that configuration. The ident of the field is always the column name.
  • constant — a fixed value. An empty value is not permitted.
  • formula — an expression referencing other fields by %ident%, with numbers, brackets and the operators +, -, *, /. See Formula rules.
  • sql — a value produced by a SQL expression. See SQL fields are gated by their own section.
  • custom — system-managed fields contributed in code. They appear read-only in a "Custom Fields" block and are not part of the editable catalog grid.

Formula rules

The reports plugin (FormulaField) does the checks when you save a formula field:

  • Each %ident% must be an active field. The lookup reads system fields only (id_workspace NULL).
  • An ident with an upper-case letter is not permitted.
  • The check removes each whitespace character from the formula first. Thus %my field% is read as %myfield%.
  • An ident that a host IAfterUserCalculationListener owns is not permitted.
  • A number directly after %ident% is not permitted.

When you attach a formula field to a constructor, each %ident% must be an active field of that constructor.

The calculation reads only idents made of lower-case Latin letters and underscores from a formula. Do not refer to an ident that contains a digit.

Data types and the shared-column rule

Each field also has a data_type — string, integer, decimal, price, date, or datetime — which the reports plugin uses to shape the physical report column.

The important consequence: an ident maps to one physical column, shared across workspaces. The ident column has a unique index, and the report table has one column for each ident. The save is rejected in these cases:

  • A different field that you can see has the same ident.
  • No field that you can see has the ident, but the report table has a column with that name.
  • A system field has the same ident and a different data type.

Choose the ident and data type of a field as if they were permanent, because for that column they are.

Lifecycle: draft → active → deleted

A field is draft, active, or deleted, and only active fields can be attached to a constructor. Activating a field is the moment it reaches into the reports plugin — activation calls the reports facade to add the matching report column. If existing report data is incompatible with the field's data type, the activation is rolled back rather than corrupting the report table.

Attachments are guarded once a constructor has a report row. The plugin then removes the Add and Remove actions and makes the Field control read-only.

Workspace scoping

A catalog field is either system (id_workspace NULL — visible and usable in every workspace) or workspace-owned (visible only in its workspace). Workspace users see system fields plus their own but cannot edit system fields. In single-workspace (officetool) mode everything is system-scoped, so scoping is effectively a no-op.

Without an active workspace, the catalog grid has no workspace filter. The Field control of a constructor then offers system fields only.

SQL fields are gated by their own section

A field of type SQL carries a raw query, so it is not part of what a workspace operator may author. The payment_constructors_sql_fields section (PaymentConstructorFieldsSections::SQL_FIELDS) gates the authoring of one: without that section the SQL option is missing from the type select of the add/edit form, an SQL row loses its edit and remove actions, and a submitted type = sql is rejected on save. The install scripts grant the section to admin only, so a workspace role never receives it unless the host grants it explicitly.

The gate is on writing, not on reading: an SQL row stays listed and its Type column still reads "SQL". That is what RestrictedSelectField is for — the framework's SelectField drops a permission-gated option from its value list altogether, which also blanks the list cell of every row already holding that value. The subclass keeps the full list for display and filtering and narrows only what the form offers.

The Value cell of an SQL row shows the text "SQL expression". Only a user with the section sees the expression, as the tooltip of the cell.

The reports plugin (SqlField) checks the expression on save. It rejects the words DELETE, ALTER, INSERT, DROP, UPDATE, CREATE and BACKUP in any letter case. Then it runs the expression against the payment-user source table to make sure that it is valid.

The expression can contain tokens. The reports plugin replaces each token with a quoted value:

Token Value
%id-payout% The id of the payout
%month-year% The month of the payout, as m-Y
%year-month% The month of the payout, as Y-m
Host tokens The tokens of each ICollectSqlFieldTokensListener

Host integration

The plugin does not add its own top-level menu — it hangs off the base constructors grid. Through IPaymentConstructorsStoreInitListener it injects two actions into that grid (each gated on its own permission): a top-bar Fields Configuration link to the catalog, and a per-row Fields link to a constructor's attachments. It also reacts to constructor duplication by copying the source constructor's field attachments onto the new one.

It exposes IPaymentConstructorFieldsFacade for the reports plugin and the host:

Method Result
getByID() One field by id
getByIdent() One field by ident, from the system fields and the fields of the given workspace
getByPaymentConstructorID() The fields of a constructor in the sequence of order_index, optionally of one status
getByPaymentConstructorIDAndType() The fields of a constructor of one type
updateByIdent() Changes the catalog row of an ident
createRelating() Attaches a field to a constructor

Ident generation — the form has no ident control. The store sets the ident on save:

  • For a general field, the ident is the column name of the value. The store writes it again on each save, so a change of the column changes the ident.
  • For each other type, a new field gets an ident that the store makes from the caption.
  • A field of each other type that has an ident keeps it. A formula refers to the ident, and the report column has its name.

The ident is not cosmetic: it becomes a real column on the report table and an unquoted alias in the calculation query (<expression> AS <ident>). That is why every ident has to match:

^[a-z_][a-z0-9_]*$

Lower-case latin letters, digits and underscores. A digit cannot be the first character.

The built-in DefaultFieldIdentGenerator only lower-cases the caption and turns spaces and hyphens into underscores. The store then normalizes the result:

  • It changes each group of characters that are not a-z or 0-9 to one underscore.
  • It removes the underscores at the two ends and the digits at the start.
  • If nothing remains, the save is rejected.
  • If the ident is in use, the store adds a number: bonus, bonus_2, bonus_3.

Thus, a caption without Latin letters or digits cannot be saved with the default generator. A host with other captions implements IFieldIdentGeneratorListener and returns its own IFieldIdentGenerator:

class SchoolFieldIdentGenerator implements IFieldIdentGenerator
{
    public function getIdentByCaption(string $caption): string
    {
        // transliterate the caption to Latin letters
    }
}

This PHP example was not run for this page.

The generator only has to transliterate. The engine deliberately keeps the validation narrow rather than widening it per host, so an ident is always a safe bare SQL identifier.