Osy#betaRun it on Osyrin: osyrin.com

Reference / UI

The Qualifications kit — licences, certificates, inductions and signed waivers a person holds

use Osysharp.Qualifications@0; · policy IssuesQualifications => …; · RecordQualification(holder, kindKey) · SignQualification(holder, text, signedName) · SubmitQualification(holder, kindKey, issuedOn) · AttachEvidence(c, file) · VerifyQualification(c) · RevokeQualification(c, reason) · CheckQualification(holder, kindKey) · QualificationGap(holder, kindKeys)

`Osysharp.Qualifications` keeps what a person holds that lets them do something — a licence, a certificate, an induction, a background check, a signed waiver — with a validity in months, who recorded or signed it, the evidence behind it, and one call answering whether somebody holds it on a day and, when not, why not in a sentence. Waivers are signed on the exact text version shown; staff verify what people submit; a notice goes out before each lapses. Not sign-in.

preview1 example compiled by CIuikitqualificationscompliance

Source on GitHub: Osysharp.Privacy.Qualifications — github.com/osysharp/privacy/qualifications ↗ · Osysharp.Qualifications — github.com/osysharp/qualifications ↗ · Osysharp.ShiftScheduling.Qualifications — github.com/osysharp/shiftscheduling/qualifications ↗

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 on

Then 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#

Related

The ShiftScheduling kit — a staff rota: shifts, places, time off, cover and hours

`Osysharp.ShiftScheduling` is a staff rota: shifts with a number of numbered places, built as drafts and published in…

The UserAccounts kit — sign-in done properly, once

`Osysharp.UserAccounts` is the default way into an app: sign-in with a password or a provider (Google, GitHub, …)…

[Attachment] — a file readable wherever its row is

`[Attachment]` on a `FileAsset` reference makes the file readable by exactly the people who may read the row holding it…

deny … except via

Names the ONLY functions whose own code may change something a deny withholds: `deny update UnitsLeft except via…

[Event] — what happens to a record, declared once

An event is a class marked `[Event]`: something that happens to a record, declared once by the code that owns the…

[On] — hear an event, after the commit that raised it

`[On]` on a function makes it a HANDLER of the event its one parameter is. Every handler of an event — in the app and…

settings a package asks its app for (app settings)

A package — or an app — declares a setting by marking a class `[AppSetting("Name")]`. The app that uses it fills it in…

The Privacy kit — erasure that keeps only what the company must or may

`Osysharp.Privacy` answers a person's GDPR request — access, portability, erasure, restriction — per category of what…

Bridges — a package that arrives when an app uses the two it completes

A bridge is a package whose manifest says `completes A@0, B@0;`. An app that uses every package it completes gets it…