Summary#
use Osysharp.Qualifications@0; keeps the qualifications people hold. The app says who records them:
policy IssuesQualifications => IsStaff; // record, verify and revoke qualifications, read everybody's
policy ManagesQualificationKinds => IsAdmin; // what kinds exist, and the text a waiver is signed onThen one call answers anywhere a qualification matters — a door, a booking, a shift: QualificationGap(person, ["site-induction", "hire-waiver"]) is "" when they hold both, or the sentence saying what is missing.
Signature#
virtual policy IssuesQualifications => false; virtual policy ManagesQualificationKinds => IssuesQualifications;
virtual policy ChecksQualifications => IssuesQualifications; virtual policy ActsForHolder(Principal holder) => holder == user;
app.Qualifications = new QualificationsSetup { RenewalNoticeDays = 30, HoldersSubmit = true };
public entity QualificationKind { string Key; string Name; QualificationCategory Category; int? ValidForMonths; int? NoticeDays;
bool SignedByHolder; bool NeedsEvidence; bool Active; }
Qualification RecordQualification(Principal holder, string kindKey, DateOnly? issuedOn = null, string reference = "", string issuedBy = "", string note = "");
Qualification SignQualification(Principal holder, QualificationText text, string signedName);
Qualification SubmitQualification(Principal holder, string kindKey, DateOnly issuedOn, string reference = "", string issuedBy = "");
FileAsset KeepEvidence(UploadedFile upload); QualificationEvidence AttachEvidence(Qualification c, FileAsset file);
void VerifyQualification(Qualification c, string note = ""); void RejectQualification(Qualification c, string reason);
void RevokeQualification(Qualification c, string reason); QualificationText PublishQualificationText(QualificationKind kind, string body);
QualificationCheck CheckQualification(Principal holder, string kindKey, DateOnly? on = null); // State, IsValid, ExpiresSoon, ValidUntil, Reason
bool HasValidQualification(Principal holder, string kindKey, DateOnly? on = null);
List<QualificationCheck> MissingQualifications(Principal holder, List<string> kindKeys, DateOnly? on = null);
string QualificationGap(Principal holder, List<string> kindKeys, DateOnly? on = null);
List<Qualification> LapsingWithin(int days); List<Qualification> LapsedWithin(int days); List<Qualification> AwaitingVerification();Description#
What is a qualification?#
One person's qualification of one QualificationKind, from IssuedOn through ValidUntil — the LAST day it holds, the
kind's months after issue less a day (3 May + 12 months holds through 2 May; month ends clamp), or none for a kind that
does not lapse. It carries the number on it, who issued it outside the app, who recorded it, and — for a signed one —
the text version, who signed, the name they typed and when. It is never edited into something else: a renewal is a new
qualification, and revoking or turning one down keeps the row with the reason. Its status changes only through the kit's
functions (deny update Status except via …).
How is one put on record?#
Three ways. Staff record it (RecordQualification) — an induction they gave, a licence they saw — in force at once.
The person signs it (SignQualification) for a kind marked SignedByHolder: a waiver, a declaration. The person
submits it (SubmitQualification) with evidence — a scan or photo kept with KeepEvidence and attached with
AttachEvidence, readable wherever the qualification is — and it counts once staff VerifyQualification it (a kind with
NeedsEvidence refuses verification with no file) or says why with RejectQualification.
How does a waiver keep what was signed?#
Its kind's text is a QualificationText with a version, never changed: PublishQualificationText makes the next version.
SignQualification takes the version the person was shown and refuses if it is no longer the current one, so nobody signs a
wording they did not read; the signature keeps that version. ActsForHolder(holder) says who may sign for whom — a
parent for a child with a managed account of Osysharp.UserAccounts.
How is it checked?#
CheckQualification(holder, kindKey, on) answers the state — Valid, Expired, Missing, Revoked or Waiting — with
ExpiresSoon, the last day, and a sentence: Safety briefing is valid until 28 February 2027., Skipper's licence
expired on 30 September 2026., Hire waiver has not been signed., First aid certificate was revoked: number did not
check out. Of several, the most favourable decides. It reads as the caller: a person about themself or somebody they
act for, ChecksQualifications about anybody. QualificationGap and MissingQualifications ask several at once — the call to
block anything that needs them.
What happens before and at a lapse?#
Each qualification's own run (QualificationLife) raises QualificationExpiringSoon its kind's NoticeDays (else the app's
RenewalNoticeDays) before it lapses, and QualificationExpired when it does — both only when nothing newer of its kind
replaces it, so a person who renewed in time hears nothing. An old qualification entered already lapsed is not news.
LapsingWithin(days) and LapsedWithin(days) are the lists a desk works from.
Which pages does it serve?#
/qualifications — what the signed-in person holds, until when, the waivers waiting for their signature (the full text,
an "I have read it and agree" tick and their typed name), and adding one of their own with its file.
/staff/qualifications — what lapses within 30 days, what lapsed, what waits for verification (with the files), and any
person's record: record one, revoke one. /staff/qualifications/kinds — the kinds and each waiver's text versions. Each
stands in the app's working shell with theme tokens only; take one over by declaring a component of the same name, or
place QualificationsOfPerson(person) — a guardian's page signing for a child — QualificationsDesk or QualificationKinds on
a page of your own.
Whose data is kept, and how does it go?#
The number on a qualification, the typed signature and why one was revoked are Personal.Content. With Osysharp.Privacy,
Osysharp.Privacy.Qualifications arrives by itself (Bridges — a package that arrives when an app uses the two it completes) and counts, copies (with the exact text signed)
and erases a person's qualifications and their files.
Examples#
A warehouse that checks a licence before a shift on the trucks#
app Warehouse {
use Osysharp.Identity@0;
use Osysharp.Qualifications@0;
use Osysharp.Storage;
use Osysharp.Ui;
use Osysharp.Workflow;
model "model/**/*.osy";
}
using Osysharp.Identity;
using Osysharp.Qualifications;
[Role] enum DepotRole { Authenticator, Picker, Supervisor }
policy IsSupervisor => RoleGrant.Any(g => g.Member == user && g.Role == DepotRole.Supervisor);
policy IssuesQualifications => IsSupervisor;
app.Qualifications = new QualificationsSetup { RenewalNoticeDays = 45, HoldersSubmit = false };
/// Before a picker takes a truck: their licence and their induction, or the sentence saying which is missing.
[Server, Authorize(IsSupervisor)]
public string MayDrive(Principal picker) {
var why = QualificationGap(picker, ["forklift-licence", "site-induction"]);
return why == "" ? "Cleared to drive." : why;
}
/// A note for the supervisor when a licence is about to lapse.
entity Reminder {
[Required("Whose licence?")] Principal Driver;
DateOnly Lapses;
security { allow read when IsSupervisor; }
}
[On]
void NoteTheLapse(QualificationExpiringSoon e) {
new Reminder { Driver = e.Qualification.Holder, Lapses = e.Qualification.ValidUntil!.Value };
}See also#
- The ShiftScheduling kit — a staff rota: shifts, places, time off, cover and hours — a position on the rota requiring a qualification
- The UserAccounts kit — sign-in done properly, once — an account for somebody who never signs in, whose waiver a guardian signs
- [Attachment] — a file readable wherever its row is — the evidence, readable wherever its qualification is
- deny … except via — how a qualification's status changes only through the kit's functions
- The Privacy kit — erasure that keeps only what the company must or may — a person's qualifications in their privacy requests
kits/qualifications/tests/— every behaviour above as a test, the screens under--pixels