PaymentConstructorReports
Generates and stores payout reports for the Payment Constructor cluster — one report row per user per payout, with the per-field values computed from the constructor's field definitions.
Overview
This is the third plugin of the Payment Constructor cluster. PaymentConstructors
defines the constructors, PaymentConstructorFields defines the fields that make
up a payout, and this plugin turns a payout (a constructor + a period) into
concrete numbers: it loads the eligible users, runs the per-user calculation over
every field (general fields, constants, formulas, SQL fields, host-supplied custom
fields), and writes one report row per user together with a run log.
It surfaces in four places on the constructor screen:
- the calculations screen — the runs a constructor has been calculated for, and the Calculate action that starts a new one for a chosen month;
- the reports grid — the saved reports of a payout, with Export (CSV) and Logs actions, and a Regenerate action when the host supplies its URL;
- the preview screen — a live, unsaved single-user calculation used to check a constructor before committing a real payout;
- the logs screen — the outcome (and stack trace) of each generation run.
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 tables have foreign keys to payment_constructors |
PaymentConstructorFields plugin |
@pluginDependency and getDepends(); it supplies the fields of each constructor |
Queues plugin |
A queue worker calculates each payout. The plugin uses the QueuesProvider trait. |
Modules plugin |
The plugin is a module. The install SQL writes to modules. |
Host IPaymentUserProvider |
The host supplies the users. Without it, the calculation and the preview throw an exception. |
System plugin setting db_datetime_format |
The format of cdate and mdate |
Theme package (Festi\Theme) |
DisplayPlugin and StageBuilder |
Tables users, users_types, festi_* |
payment_constructors_reports.id_user has a foreign key to users.id; the preview grid reads users |
The repository has no init.php file.
Installation
The tables have foreign keys to payment_constructors.
Thus, install the cluster in this order:
PaymentConstructorsPaymentConstructorFieldsPaymentConstructorReports(this plugin — last)
Install the plugin with the Festi CLI:
./vendor/bin/festi-plugin --mode install --path ./src/dashboard/ --dump ./dumps/ \
--plugin PaymentConstructorReports \
--repository [email protected]:FestiPlugins/php_festi_plugin_payment_constructor_reports.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 tables
payment_constructor_payouts,payment_constructors_reportsandpayment_constructor_report_logs; - the queue table
queue_paymentconstructorreports; - the plugin row in
festi_plugins; - the module
payment_constructor_reportsinmodules; - eight URL rules in the area
backend; - five sections, with the permission for the user type
admin.
Per-field report columns are not created here — they are added at runtime as fields are activated.
| Section | Use |
|---|---|
payment_constructors_calculations |
The calculations screen |
payment_constructors_reports |
The reports grid of one payout |
payment_constructors_report_export |
The Export action of the reports grid |
payment_constructors_report_preview |
The preview screen |
payment_constructors_report_logs |
The logs screen |
The reports grid also uses the section payment_constructors_report_regenerate for the Regenerate and Logs actions.
The install SQL of the PaymentConstructors plugin makes that section.
Queue worker
The Calculate action only puts the payout in the queue.
A worker of the Queues plugin does the calculation.
Add the worker to the crontab file of the project:
# CRONTAB
*/5 * * * * %PHP% %PROJECT_PATH%/plugins/Queues/queues.php --id 1 --name PaymentConstructorReports
Apply the crontab file with the migration command:
./vendor/bin/festi-migrate --crontab ./dump/crontab.txt --crontab-user user
This crontab example was not run for this page.
Configuration
The plugin has no module options. The host configures the plugin through the providers and listeners in Driving it from a host.
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
Each page of the plugin also answers 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.
| Page | URL |
|---|---|
| Calculations | /payment/constructors/{id}/calculations/ |
| Reports of one payout | /payment/constructors/{id}/calculations/{payout id}/ |
| Preview | /payment/constructors/{id}/reports/preview/ |
| Logs | /payment/constructors/{id}/reports/logs/ |
Usage
- Open the list of payment constructors.
- Click Preview Reports in the row of a constructor.
- Select a user and a month to see the calculation for that user. The plugin does not save it.
- Go back to the list and click Calculations.
- Click Calculate and select a month. The list offers the current month and the 11 months before it.
- Wait until the status of the run changes from "In Queue" to "Completed".
- Click Reports in the row of the run to see the report of each user.
- In the list of payment constructors, click Logs to see the result of each run.
The Calculate action is available only for an active constructor.
How a report is produced
CalculatePayrollAction::open()creates a payout for a constructor and a month (payment_constructor_payouts). The payout has the statusqueued. Theperiod_datecolumn keeps the first day of the month.- The plugin puts the payout id in the queue (
enqueuePayout()). - The queue worker calls
onQueueItemWithTypeCalculatePayout(). The plugin dispatchesPrepareQueuedPayoutEvent, then calculates the payout if its status is stillqueued. - The payout gets the status
in_progress. Generation loads the eligible users from the hostIPaymentUserProviderand computes each field for each user. - Generation writes one
payment_constructors_reportsrow for each user, with the statuscompleted. - The payout gets the status
completed, orerrorif generation fails. - A run appends
payment_constructor_report_logsrows: onein_progressrow at the start, and onecompletedorerrorrow at the end. Anerrorrow has the message and the trace. For a database error, thetracecolumn has the SQL query, not a trace.
Generation stops before the first log row when it does not find the constructor or the constructor is not active. That run has no log row.
A month without eligible users is a completed run with no report rows.
generate() itself neither creates a payout nor sets its status, so anything that
starts a run (the Calculate action, a host's scheduler) goes through
CalculatePayrollAction.
The calculation of one user has this sequence:
generalfieldsconstantfieldscustomfieldssqlfieldsformulafields- the total: the sum of the numeric values of the fields with Is Total
- each
IAfterUserCalculationListenerof the host
Preview is the same calculation with nothing persisted: it runs against a
single user under payout id 0, so a constructor can be validated before any real
payout exists.
Regenerate. The reports grid shows the Regenerate action only when the host gives
the store a regenerateUrl option and no IPayoutLockListener locks the payout.
The plugin has no regenerate route of its own.
Scheduled runs
RunScheduledPayoutsAction opens and queues the payouts of the previous month.
The host calls it on its own schedule:
$plugin = Core::getInstance()->getPluginInstance('PaymentConstructorReports');
$plugin->getRunScheduledPayoutsAction()->run(new DateTime());
This PHP example was not run for this page.
The action reads each active constructor that has a workspace.
For each workspace, it dispatches PrepareScheduledPayoutEvent. The host can skip the workspace there.
PayoutScheduler opens a payout for a constructor in these cases:
- The month has no payout.
- The last payout of the month has the status
error. - The last payout is
queuedorin_progressand is older than six hours.
Driving it from a host (bring-your-own data)
The plugin ships no data sources of its own — it does not know who the users are, what the exchange rate is, or what a "custom" field means. A host application supplies all of that through framework events and a set of optional listeners. The only required one is the user provider; without it, generation throws.
| Provider / listener | Required? | What the host supplies |
|---|---|---|
CreatePaymentUserProviderEvent → IPaymentUserProvider |
yes | the users to calculate — preview user, the users of a constructor and period, excluded ids |
PrepareQueuedPayoutEvent |
no | the workspace context before the worker calculates a queued payout |
PrepareScheduledPayoutEvent |
no | the workspace context for a scheduled run, and the decision to skip the workspace |
IRateProviderListener → IRateProvider |
no | exchange rate (default: pass-through 1.0); a custom field class gets it as the option currency-rate |
ICollectSqlFieldTokensListener |
no | per-payout SQL substitution tokens for sql fields |
IAfterUserCalculationListener |
no | field idents applied after the base total (kept out of the total and out of formulas) |
ICollectCustomPaymentFieldsListener |
no | custom field classes, registered by ident |
ICryptProviderListener → ICryptProvider |
no | decryption of a field value (default: no-op) |
IPayoutLockListener |
no | locks a payout against regeneration (default: unlocked) |
IPrepareReportFieldsListener |
no | last word on the built report columns — the currency money columns are rendered in, above all |
This is how the same report engine serves different products: the host is the only thing that knows its own users, currencies and business fields, and it plugs them in here.
The plugin has two facades for the host and the sibling plugins:
getFacade()returnsIPaymentConstructorReportsFacade: generation, report queries and field validation.getPayoutFacade()returnsIPaymentConstructorPayoutFacade: the rows ofpayment_constructor_payouts.
A process that calculates for more than one workspace calls resetHostContributions() between the workspaces.
Notable behavior
Report columns are created at runtime, not at install time. The base
payment_constructors_reports table ships with only nine columns (id,
id_payment_constructor, id_workspace, id_user, id_payout, status,
total, cdate, mdate). When a workspace activates a payment-constructor field,
the plugin adds a value column with the ident of that field as its name. When the
ident or the data type of the field changes, the plugin renames the column or
changes its type. So the report table's shape follows the activated fields —
the fresh install deliberately carries only the base columns.
| Data type of the field | Column type |
|---|---|
string |
VARCHAR(255) |
integer |
INT |
decimal, price |
DECIMAL(10, 2) |
date |
DATE |
datetime |
DATETIME (TIMESTAMP on PostgreSQL) |
The schema is host-agnostic. The install scripts intentionally omit any host-specific foreign key (e.g. linking a report/payout to a money-transfer record). That linkage is added by the host's own bridge migration, so the same plugin drops cleanly into any product.
Workspace-scoped, with a system-space fallback. The plugin implements both
IWorkSpaceModule and ISystemSpaceModule, so constructors, fields and reports
are isolated per workspace on a host that runs workspaces, and the module still
resolves as active on a host that does not. Each page resolves
its constructor through the workspace-scoped facade lookup, so an id belonging to
another workspace is refused rather than rendered.