Osy#betaRun it on Osyrin: osyrin.com

Reference / UI

The Memberships kit — what lets a person in: passes, memberships and visit cards

use Osysharp.Memberships@0; · policy SellsMemberships => …; policy AdmitsMembers => …; · SellMembership(person, plan) · CheckIn(person, place) · EntryFor(person) · PauseMembership(m, until) · RenewMembership(m) · TransferMembership(m, to) · AddToMembership(m, person) · UnusedValueOf(m)

`Osysharp.Memberships` sells what lets people in — a day ticket, a monthly membership, a card of ten visits, a family year — and lets them in: a check-in uses a visit, never goes below zero, and when somebody may not enter it says why in a sentence. Memberships pause (moving their end), renew (by hand or by the app's payment), transfer, carry family members and say what is left of them in money. A visit card's visits are a pack of Osysharp.PrepaidCredits.

preview1 example compiled by CIuikitmembershipsadmission

Source on GitHub: Osysharp.Memberships — github.com/osysharp/memberships ↗ · Osysharp.Privacy.Memberships — github.com/osysharp/privacy/memberships ↗

Summary#

use Osysharp.Memberships@0; sells memberships and lets their holders in. The app says who sells and who checks people in:

policy SellsMemberships => IsDesk;    // plans, selling, pausing, ending, renewing, transferring
policy AdmitsMembers => IsGate;       // checking people in, the door log

Then SellMembership(person, plan) at the counter or after a payment, and CheckIn(person, "Main entrance") at the door — which answers Admitted, or Refused with the reason to read out.

Signature#

virtual policy SellsMemberships => false;     virtual policy AdmitsMembers => false;
virtual policy ActsForMember(Principal holder) => holder == user;
app.Memberships = new MembershipsSetup { NoticeDays = 7, MembersMayPause = false, HoldersMayTransfer = false, MembersCheckIn = false };

public entity MembershipPlan { string Name; decimal Price; string Currency; int Periods; PlanPeriod Period;   // Day | Week | Month | Year
                               int Visits; bool StartsOnFirstVisit; int MaxPauseDays; int People; bool Renewable; bool OnSale; }
Membership SellMembership(Principal holder, MembershipPlan plan, string? reference = null, DateTime? startsOn = null,
                          bool autoRenew = false, decimal? price = null, Membership? renewalOf = null);
Membership RenewMembership(Membership m, string? reference = null);      void SetAutoRenew(Membership m, bool on);
Admission CheckIn(Principal person, string place = "");                 MembershipEntry EntryFor(Principal person);
MembershipChange PauseMembership(Membership m, DateTime? until = null, string reason = "");
MembershipChange ResumeMembership(Membership m, string reason = "");
MembershipChange ExtendMembership(Membership m, DateTime until, string reason);
MembershipChange EndMembership(Membership m, string reason);
MembershipChange TransferMembership(Membership m, Principal to, string reason = "");
MembershipChange UndoAdmission(Admission a, string reason = "");
MembershipMember AddToMembership(Membership m, Principal person);       bool RemoveFromMembership(Membership m, Principal person);
List<Membership> MembershipsOf(Principal person);    int VisitsLeftOn(Membership m);    decimal UnusedValueOf(Membership m);

Description#

What is a plan?#

What is on sale: a length (Periods × Period), a number of Visits (0: as often as you like), whether it starts on the first visit rather than when sold, how many days it may be paused in all, how many People it lets in, and whether it can renew by itself. A plan withdrawn from sale is kept: memberships already sold point at it. /staff/memberships/plans lists and edits them; MembershipPlanList(onChoose) is the price list for a page that sells.

How does a visit card count its visits?#

As a pack of Osysharp.PrepaidCredits, granted when it starts and scoped to the membership, at what each visit was paid. So a card's visits never go below zero, each use is a line in its holder's credits statement, and the value still owed counts them. When the card runs out of time, what is left expires with it.

What happens at the door?#

CheckIn(person, place) makes an Admission and decides it in the same commit. The best membership first — one without a visit limit before a card, then the one that ends first; a membership somebody is ON (a family member) counts like their own. On a card a visit is spent (for a family member, from the holder's pack, recorded for them). A membership that starts on its first visit starts. When nothing lets them in the admission is kept, refused, with the sentence to read out: No membership on record., Monthly is paused until 3 March 2027., Ten swims has no visits left., Day ticket ran out on 10 October 2026. EntryFor(person) answers the same question without using anything. A mistaken scan is undone with UndoAdmission, and the visit goes back. MembersCheckIn lets a person check themselves in — a kiosk, a phone at the door.

How is a change kept?#

Every change is a MembershipChange — paused, resumed, end date changed, ended, transferred, admission undone — with who made it and why; its run applies it in the same commit. Nobody edits or deletes one, and no grant opens a direct write to a membership's dates (deny update ValidUntil …).

How does a pause work?#

A pause runs until a day (at most what the plan still allows — MaxPauseDays in all) and ends by itself then; resumed earlier, it ends then. Either way the membership's end moves by the calendar days it was paused. A holder may pause their own when MembersMayPause is on.

What happens before and at the end?#

NoticeDays before a membership ends, the app is told once: MembershipEndingSoon — tell the holder — or, for one set to renew, MembershipRenewalDue — charge for the next one (Osysharp.Payments, Osysharp.Shop) and call RenewMembership, which starts the next one when this one ends and is made once however often it is called. At the end it ends — MembershipEnded — and a card's visits left expire with it. A pause or a new end date moves both.

What else can happen to a membership?#

TransferMembership ends it for its holder and gives it — the same end, the visits left — to somebody else (HoldersMayTransfer lets a holder do it). AddToMembership puts somebody else on a membership, up to its plan's People. UnusedValueOf says what is left of one in money at what was paid: the share of visits not used, or of days not yet run (paused days do not count) — what a refund or a change of plan is made from; EndMembership then ends it.

Which pages does it serve?#

/memberships — whether the signed-in person can come in, their memberships (pause, resume, renew automatically) and their visits. /staff/memberships — the desk: find a person, see at once whether they may come in, check them in, sell, pause, resume, change the end, renew, transfer, end; today's door log with undo. /staff/memberships/plans — the plans. Each stands in the app's working shell, every colour and size a theme token; take one over by declaring a component of the same name, or place MyMemberships, MembershipsDesk, MembershipPlans or MembershipPlanList on a page of your own. The desk reads visits left through PrepaidCredits' rules, so answer ManagesCredits/SpendsCredits for the same staff.

Whose data is kept, and how does it go?#

Where somebody checked in, why they were refused and why a change was made are Personal.Content. With Osysharp.Privacy, Osysharp.Privacy.Memberships — which arrives by itself (Bridges — a package that arrives when an app uses the two it completes) — counts, copies and erases a person's memberships, their place on others' and their check-ins; their visits' credits go with Osysharp.Privacy.PrepaidCredits.

Examples#

A co-working space selling day passes and months#

app Cowork {
  use Osysharp.Identity@0;
  use Osysharp.PrepaidCredits@0;
  use Osysharp.Memberships@0;
  use Osysharp.Ui;
  use Osysharp.Workflow;
  model "model/**/*.osy";
}

using Osysharp.Identity;
using Osysharp.Memberships;

[Role] enum SpaceRole { Authenticator, Coworker, Host }
policy IsHost => RoleGrant.Any(g => g.Member == user && g.Role == SpaceRole.Host);
policy SellsMemberships => IsHost;
policy AdmitsMembers => IsHost;
policy ManagesCredits => IsHost;
policy SpendsCredits => IsHost;

app.Memberships = new MembershipsSetup { NoticeDays = 5, MembersMayPause = true, MembersCheckIn = true };

/// A coworker taps in at the front door with their phone; the answer is what the door shows.
[Server, Authorize(IsAuthenticated)]
public string TapIn() {
  var a = CheckIn(CurrentPrincipal()!, "Front door");
  return a.State == AdmissionState.Admitted ? "Welcome in." : a.Reason;
}

/// A month set to renew is due: the space charges the card it keeps, then renews.
[On]
void RenewTheMonth(MembershipRenewalDue e) { RenewMembership(e.Membership); }

See also#

Related

The PrepaidCredits kit — prepaid uses, spent from the pack that expires first, never below zero

`Osysharp.PrepaidCredits` keeps prepaid uses — a class pack, a ten-visit card, machine hours — as packs of credits a…

deny … except via

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

What may go past the caller's own rights — and what never does

A request always runs as the person who made it; there is no system mode, sudo or definer's rights a function can…

[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…

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, …)…