PaymentConstructors

The base plugin of the Payment Constructor cluster: it manages the payment constructors themselves and owns the extension points that the fields and reports plugins — and the host application — build on.

Overview

A payment constructor is a named, workspace-scoped configuration of a payment run. It carries a caption, an owner, a status, an SQL filter fragment and — through the sibling plugins — a set of fields and generated reports.

This plugin provides the managed list of constructors and, more importantly, the cluster-wide contracts the rest of the cluster and the host plug into. It is the piece that must exist first; fields and reports layer onto a constructor.

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
PaymentConstructorFields plugin @pluginDependency, and the module dependency in getDepends()
PaymentConstructorReports plugin @pluginDependency
ModelFilters plugin @pluginDependency; the Filters page and the Duplicate action use it
Modules plugin The plugin is a module. The install SQL writes to modules and module_options.
Jimbo system plugin The settings db_datetime_format and users_table
Theme package (Festi\Theme) DisplayPlugin and StageBuilder
Tables users, users_types, festi_* See the list below

The plugin uses the tables of the project as follows:

  • payment_constructors.id_user has a foreign key to users.id.
  • The list shows the owner from the column users.full_name.
  • The install SQL gives the sections to the user type with the ident admin.

The repository has no init.php file.

Installation

This is the base plugin. Install it first. The other two plugins have foreign keys to the payment_constructors table.

Cluster install order:

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

Install the plugin with the Festi CLI:

./vendor/bin/festi-plugin --mode install --path ./src/dashboard/ --dump ./dumps/ \
    --plugin PaymentConstructors \
    --repository [email protected]:FestiPlugins/php_festi_plugin_payment_constructors.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 does these steps:

  1. It adds the repository to the plugins directory as a git submodule.
  2. It copies install/install.<database type>.sql into the dumps directory as a new migration.
  3. It applies the migrations that the database does not have.

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_constructors table;
  • the plugin row in festi_plugins;
  • the module payment_constructors in modules;
  • the module option workspace_url_segment in module_options;
  • six URL rules in the area backend;
  • four sections, with the permission for the user type admin.
Section Use
payment_constructors_list The list of constructors
payment_constructors_filters The Filters page and the filters field
payment_constructors_duplicate The Duplicate action
payment_constructors_report_regenerate The PaymentConstructorReports plugin uses it for the Regenerate and Logs actions of the reports grid

Configuration

Workspace URLs

Every cluster page answers on two routes: the bare one (/payment/constructors/...) and a workspace-scoped one that carries the workspace id (/workspace/{id}/payment/constructors/...). Which one a link points at is decided at render time by IPaymentConstructorFacade::getWorkSpaceUrl(): with no active workspace it builds the bare URL, with one it prepends the prefix.

Nothing else in the cluster hardcodes a URL. Plugins call the facade directly; stores build their tblDef links through it and hand them to the definition as parameters (filtersUrl, duplicateUrl, constructorsUrl, previewReportsUrl, reportLogsUrl, fieldsUrl), so a tblDef never resolves a URL itself.

Renaming the segment. workspace is only the default. A host that mounts its workspaces elsewhere sets the workspace_url_segment module option on the payment_constructors module and the whole cluster follows; Prosvita, for example, sets it to company so cluster pages sit under /company/{id}/... alongside the rest of that application.

The option is deliberately is_workspace = FALSE, is_user = FALSE: the segment has to match the route patterns in festi_url_rules, so it is a deployment-level setting and never appears in a workspace administrator's module options.

A change of the segment has two parts. Both parts are necessary:

  1. Set the default value of the option to the new segment.
  2. Change the pattern of the nine workspace routes of the cluster.

The nine routes are three routes of this plugin, two of PaymentConstructorFields and four of PaymentConstructorReports.

Do not change the rows by hand in a database client. Make a migration file in the dumps directory of the project:

./vendor/bin/festi-make-migration --dump ./dumps/

Put the two statements in the new file. This example changes the segment to company:

UPDATE module_options
SET default_value = 'company'
WHERE name = 'workspace_url_segment'
  AND id_module = (SELECT id FROM modules WHERE ident = 'payment_constructors');

UPDATE festi_url_rules
SET pattern = REPLACE(pattern, '~^/workspace/(?:', '~^/company/(?:')
WHERE plugin IN ('PaymentConstructors', 'PaymentConstructorFields', 'PaymentConstructorReports')
  AND pattern LIKE '~^/workspace/(?:%';

Apply the migration:

./vendor/bin/festi-migrate --path ./src/dashboard/ --dump ./dumps/

This migration example was not run for this page.

The workspace id in those patterns sits in a non-capturing group — ~^/workspace/(?:[0-9]+)/payment/constructors/([0-9]+)/filters/$~ — so it never enters the router's match list and the handlers keep receiving the constructor id as their only argument. Copying the pattern with a capturing group instead would hand every handler the workspace id where it expects the constructor id.

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.

Usage

The list lives at /payment/constructors/, or under the workspace prefix on a host that runs workspaces — see Workspace URLs (a "Payment Constructors" item under the Finance sidebar). Each constructor has:

  • a caption and a status (active / disabled). The list highlights a disabled constructor with the CSS class m-row-red and removes its Preview Reports action;
  • an owner, set to the current user on create;
  • a filter: an SQL WHERE fragment in the filters field of the form. The field needs the payment_constructors_filters section;
  • a Filters action that opens the ModelFilters editor for the constructor. The editor offers the filterable columns of the payment-user source table;
  • a Duplicate action that opens the Add form with the caption, status and filter of the source constructor. After the save, the plugin copies the ModelFilters filters, and the listeners copy the related data of the fields and reports plugins.

The plugins of the cluster do not apply the filter to the users. The host selects the users in its IPaymentUserProvider (see PaymentConstructorReports).

The sibling plugins add more actions to each row. The row actions have this sequence: Calculations, Preview Reports, Fields, Filters, Logs, Edit, Duplicate.

Workspace scope. With an active workspace, the list shows only the constructors of that workspace, and a new constructor gets the workspace id. Without an active workspace, the list has no workspace filter, and a new constructor gets no workspace id. The Filters and Duplicate pages are strict: getByIDInWorkSpace() refuses a constructor whose workspace is not the current one.

Filter safety. Before a filter is saved it is checked two ways: a blacklist rejects mutating keywords (DELETE, ALTER, INSERT, DROP, UPDATE, CREATE, BACKUP), and the fragment is test-run as a WHERE clause to confirm it is valid SQL. The test-run reads the table that the system plugin setting users_table names. The blacklist check is case-sensitive: it finds a keyword only when the keyword is in upper case. The store does not save a fragment that fails one of the two checks.

Host integration

Like the rest of the cluster, this plugin stays host-agnostic and lets the host fill in what it alone knows, through framework events and observer hooks:

  • Who the users are — CreatePaymentUserSourceProviderEvent lets a host return an IPaymentUserSourceProvider describing the source table, the user key, an optional workspace key, an allowed-tables whitelist and an optional list of printable columns. With no host, the default targets the core users table keyed on id. The Filters page, the field types general and sql, and the calculation read this configuration.
  • Page chrome — CreatePaymentNavigationProviderEvent lets a host apply its own active-menu and breadcrumbs to cluster pages. With no host it is a no-op.
  • Extra store actions and fields — IPaymentConstructorsStoreInitListener lets satellite plugins add their own actions and fields to the constructors grid (this is how the reports plugin injects Preview/Logs, and how a host adds a constructor column of its own). A contributed action arrives after the model has loaded, so it never passes through the attribute defaults the model fills in for its own tblDef actions, while the grid reads every attribute unconditionally. Hand it to ContributedStoreAction::getPrepared($model, $action) — it runs the array through the model's own action-attribute spec — instead of restating the attribute list at each call site.
  • Duplicate defaults — ICollectPaymentConstructorDuplicateFieldsListener lets a plugin that contributed its own field pre-fill it on the duplicate form, the way the engine pre-fills caption, status and filter.
  • Duplicate follow-up — IPaymentConstructorDuplicateListener fires after a constructor is duplicated, so dependent plugins can copy their own related data onto the new constructor.

The host-facing facade IPaymentConstructorFacade has these methods:

Method Result
getPaymentUserSourceConfig() The configuration of the payment-user source table
getPaymentUserSourceColumns() The columns that a general field can print, as table.column => label
isColumnExistInTable(), isCorrectSQL() Checks that the sibling plugins use for validation
getByID(), getByIDInWorkSpace() One constructor; the second method refuses a constructor of a different workspace
create() A new constructor; the method sets cdate and mdate
getByWorkSpaceID(), getActiveInWorkSpaces() Lists of constructors for a host that runs a schedule
getCurrentWorkSpaceID(), getWorkSpaceUrl() The current workspace id and a cluster URL with the workspace prefix

Notable behavior

  • Active in both scopes, and dependency-aware — implements IWorkSpaceModule and ISystemSpaceModule, so the module resolves as active on a host that runs workspaces and on one that does not. The two dependency lists are not the same list: @pluginDependency names all three siblings (PaymentConstructorFields, PaymentConstructorReports, ModelFilters) so they are reachable as $this->plugin->…, while the module-level getDepends() names only PaymentConstructorFields — the reports module declares the dependency in the other direction, and ModelFilters is a library with no module at all.
  • A constructor is a ModelFilters model — its value object implements IModelFilter (model type payment_constructor), which is what lets the Filters action reuse the generic ModelFilters editor instead of a bespoke one.
  • Engine-clean schema — the install owns only the payment_constructors table. The table has one foreign key, from id_user to users.id, and no host-specific columns. The cross-plugin foreign keys are added by the fields and reports plugins, which is exactly why this plugin installs first.