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:

  1. PaymentConstructors
  2. PaymentConstructorFields
  3. PaymentConstructorReports (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_reports and payment_constructor_report_logs;
  • the queue table queue_paymentconstructorreports;
  • the plugin row in festi_plugins;
  • the module payment_constructor_reports in modules;
  • 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

  1. Open the list of payment constructors.
  2. Click Preview Reports in the row of a constructor.
  3. Select a user and a month to see the calculation for that user. The plugin does not save it.
  4. Go back to the list and click Calculations.
  5. Click Calculate and select a month. The list offers the current month and the 11 months before it.
  6. Wait until the status of the run changes from "In Queue" to "Completed".
  7. Click Reports in the row of the run to see the report of each user.
  8. 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

  1. CalculatePayrollAction::open() creates a payout for a constructor and a month (payment_constructor_payouts). The payout has the status queued. The period_date column keeps the first day of the month.
  2. The plugin puts the payout id in the queue (enqueuePayout()).
  3. The queue worker calls onQueueItemWithTypeCalculatePayout(). The plugin dispatches PrepareQueuedPayoutEvent, then calculates the payout if its status is still queued.
  4. The payout gets the status in_progress. Generation loads the eligible users from the host IPaymentUserProvider and computes each field for each user.
  5. Generation writes one payment_constructors_reports row for each user, with the status completed.
  6. The payout gets the status completed, or error if generation fails.
  7. A run appends payment_constructor_report_logs rows: one in_progress row at the start, and one completed or error row at the end. An error row has the message and the trace. For a database error, the trace column 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:

  1. general fields
  2. constant fields
  3. custom fields
  4. sql fields
  5. formula fields
  6. the total: the sum of the numeric values of the fields with Is Total
  7. each IAfterUserCalculationListener of 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 queued or in_progress and 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() returns IPaymentConstructorReportsFacade: generation, report queries and field validation.
  • getPayoutFacade() returns IPaymentConstructorPayoutFacade: the rows of payment_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.