Internal processing
Paid add-on. A submission received is not a submission dealt with. This module adds to every request a state, a person responsible, a deadline, a priority and a history — without touching what the core already knows about it.
This document describes what the module guarantees and what it refuses. The user guide gives the short version, under “Following a request through”.
Being assigned never grants the right to read
That is the decision governing everything else.
The natural slope of an assignment module is: “the assigned person must be able to open the case, so the assignment gives them access”. That shortcut turns a dropdown into a rights-granting tool — assigning a case to any account on the site would be enough to open to them a submission they should never have seen: address, phone number, attachments included.
The direction is therefore reversed. The right comes first, the assignment only designates, among those who already have it, the one whose task it is:
- the list offered is computed from the accounts that can already read this form’s submissions;
- the check is repeated on write, because a dropdown is markup and markup can be rewritten;
- an account that loses its right stays named on the case — the screen says so — but receives no further reminder and no longer sees the case in its queue.
The processing capability
It is called solis_forms_process_entries and is used with the form’s id:
current_user_can( 'solis_forms_process_entries', $form_id ).
It is a meta capability, translated by map_meta_cap into two conditions the
site already grants: seeing the submissions, and having a say over that
particular form. It therefore opens no new right and is granted to nobody.
An extra primitive capability would have had the opposite defect: absent from every site already installed, it would have deprived everyone of processing on the day of the update, administrator included, until somebody worked out that it had to be granted.
A site wanting a distinct “handler” role can re-wire the translation through a
higher-priority map_meta_cap filter, without touching the code.
On a multisite network, capabilities are per site and so are the tables: a network account without the right on this site is not offered, and one site’s queue never shows another’s cases.
The states belong to the form
A quote request does not go through the same stages as a job application. States are therefore configured form by form: a label, an order, and whether or not it closes. Eight at most.
The default setting offers four: Received, In progress, Waiting, Resolved — the last one closing.
The key never changes
Each state has a key (in_progress, under_review) made once, at creation, from
the label. Renaming the state does not change it. That is what allows a typo
to be corrected or a word translated without every existing case ending up in a
state the configuration no longer names.
The server refuses an invalid key instead of repairing it: repairing would mean renaming, and a renamed key no longer designates the same cases.
What is refused
One refusal, and it is the one that protects: you only move to a state this form declares. A key forged in a request, a state since removed from the settings, another form’s state — all are refused by the server, whether the screen offered them or not.
There is no transition diagram. A matrix saying “from this state, you may only go to those” configures badly, is understood even less well, and almost always ends up wide open because the first unforeseen case blocks somebody on a Friday evening.
Reopening a closed case is allowed. A tool that forbids it is worked around by a second submission, which loses the very history you wanted to keep.
A state removed from the settings
The cases that were in it keep it: rewriting them automatically would decide in somebody’s place, and the history would lie. They are displayed with their key, flagged as undeclared, and can move to any declared state. The way out is still there.
Two people do not overwrite each other
Every case carries a revision. The screen carries it in the form, and the
write applies only to that revision: UPDATE … WHERE entry_id = … AND revision = ….
Two people validating the same case in the same second therefore do not overwrite each other. The second write touches no row, nothing is recorded, and the screen says so: “somebody else modified this submission in the meantime. Nothing was written: here is the current state.”
Re-reading the row before writing would have left exactly the interval we are trying to close. The condition travels with the write.
The state, the person, the deadline and the priority go out together, in a single write: four buttons would have produced four revisions, and therefore three further chances of hitting a conflict halfway through.
A comment, for its part, has no revision: adding to a history competes with nothing, and requiring a revision would have lost a text written while somebody else was changing the state.
The history is not rewritten
Transitions, assignments, deadline and priority changes, reminders and internal comments live in a single table, in the order they arrived. The repository offers neither modification nor deletion of an event: a history that can be rewritten is worth nothing, since its whole interest is that it cannot be made to say something else afterwards.
It disappears with what it describes. Deleting a submission or a form takes the cases and their log with it — an internal comment talks about somebody, and there is no right to erasure that would spare the comment while erasing the answer.
The two emails
They are configured separately. Telling the person you assign is useful from day one; chasing an overdue case assumes deadlines are taken seriously, which happens only after a few weeks of use.
Neither carries a submission’s contents. No answer, no sender’s name, no internal comment: the form’s name, the case number, the deadline, a link.
The reason is not prudishness. The right to read is verified when the link is opened, in the admin, and nowhere else. A mailbox is shared, forwarded, synchronised onto a lost phone: putting the answer in the message would amount to delivering the data where no control applies any more, and delivering it for good — one does not revoke an email that has gone.
The overdue reminder
- One per case, and not one more. The condition
reminded_at IS NULLtravels with the write: two concurrent runs of the task send only one. - Only to the person designated. An overdue case with nobody assigned sends nothing: telling every administrator about what is nobody’s task produces an email everyone learns to filter — and the day the message matters, it is in a folder nobody opens any more. That kind of delay shows in the queue, which is the right place.
- Nothing goes out for a submission trashed or marked as spam, even if its processing stayed open.
- Nothing goes out to somebody who has lost the right to open the case.
The task runs every hour. A deadline is set to the hour, and a daily task would report a delay up to twenty-three hours after it happened. It costs nothing when no form asks for reminders: it then makes no query at all.
The deadline
It is set in hours from receipt, not as a fixed date: a fixed date would already be past for every submission of the following month, and the whole queue would show as overdue.
Zero hours means “no deadline”. That is a legitimate choice: many processes have no deadline, and inventing one so that the column is filled teaches people to ignore the colour red.
A case’s detail allows its deadline to be moved. The date entered is understood in the site’s time zone and kept in universal time; an unreadable date is refused rather than reinterpreted, because a guessed deadline would show as overdue without anyone having set it.
Lateness is written, not merely coloured: a colour is not read aloud, does not survive a black-and-white printout, and blurs together for a proportion of the people looking at the screen.
The three screens
A submission’s detail carries the processing block: state, person, deadline, priority, then the history and the comment field. That is where decisions are made.
A form’s submissions list gains a column — state, person, mention of lateness — and a row action: Move to: …. It offers only the next stage in the declared order, not the full list. With four states, offering every state would mean three extra links on every row, next to “View”, “Spam”, “Trash” and “Delete”: nobody reads an action row that long any more, and the dangerous link ends up lost among the harmless ones.
The link does not name the target state: it asks to move forward, and it is the server that reads the declared order to know where to.
The processing queue (menu SolisForms → Processing) answers the question you ask yourself on arriving in the morning, and which is not asked form by form: what is to be dealt with, what is assigned to me, what is overdue. It is sorted by descending priority then by deadline — priority is what somebody decided, the deadline what the calendar imposes, and letting the calendar win would make the priority setting decorative.
Somebody who manages only their own forms sees only their queue there: the restriction is placed in the query, not applied to an already paginated list.
The timing measurements
The queue displays, above the table and according to the current filters: the number of open cases, the number overdue, and the processing time.
The median first, the mean afterwards. One case forgotten for six months doubles a whole quarter’s mean: you read “average time: eleven days” where forty-nine cases out of fifty were handled the same day, and you conclude the team is slow. The median says what usually happens, the mean says what the exceptions cost.
The gap between the two is the information. When the mean exceeds twice the median, the screen says so plainly and gives the longest time in the sample, so that you can go and look at the case concerned.
Only closed cases are measured. A case still open has no duration: it has an age. Mixing them would pull towards zero the measurement of a team that has just opened twenty cases, and pull it away from zero for one with none in progress. Open cases are therefore counted, not averaged — and it is lateness, not age, that says which to look at. Reopening a case removes it from the measurement.
The sample covers the last five hundred closures, and the screen says how many cases it rests on. The bound answers the question you actually ask: how long cases are taking right now — an average drawn over three years of data would answer a different one.
The measurement follows the screen’s filters. A time across all forms would mix a quote request with a job application.
What the module does not touch
The submission’s status — active, spam, trash, draft — stays the core’s. Resolving a case trashes nothing, and trashing a submission resolves nothing.
Spam, trash and drafts open no case: the queue would otherwise stay full of what nobody intends to look at, and the overdue counter would lose all meaning.
Switching processing on for a form already in service leaves the submissions already received without a case. The detail screen offers to open them one by one. Opening them automatically would have meant walking a whole table when a setting is saved — and inventing a retroactive deadline, and therefore a queue entirely overdue from its first minute.
For developers
An action is emitted after every state change:
add_action(
'solis_forms_pro_workflow_transitioned',
function ( int $entry_id, string $from, string $to ): void {
// …
},
10,
3
);
The tables are {prefix}slf_entry_workflows (current state, one row per
submission, entry_id unique) and {prefix}slf_workflow_events (the log,
append-only). The scheduled task’s hook is solis_forms_workflow_reminders.
What V1 does not do
- No free-form transition diagram.
- No multi-level approval.
- No external automation: the module calls no third-party service.
- No collaborative editing: the revision reports the conflict, it does not merge two sets of input.
- The emails are internal. Nothing is written to the person who filled in the form; that is the role of the core’s notifications.
