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 logThen 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#
- The PrepaidCredits kit — prepaid uses, spent from the pack that expires first, never below zero — the packs a visit card's visits are kept in
- deny … except via — how a membership's dates are changed only by its changes
- What may go past the caller's own rights — and what never does — the desk acting for a member
- settings a package asks its app for (app settings) —
app.Memberships - The Privacy kit — erasure that keeps only what the company must or may — a person's memberships in their privacy requests
kits/memberships/tests/— every behaviour above as a test, the screens under--pixels