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

> `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.

<!-- id: ui-memberships-kit · area: ui · stability: preview · html: https://osysharp.com/reference/ui/memberships-kit/ -->

## Summary        {#summary}
`use Osysharp.Memberships@0;` sells memberships and lets their holders in. The app says who sells and who checks
people in:

```osy syntax
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      {#signature}
```osy syntax
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    {#description}

### What is a plan?   {#plans}
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?   {#visits}
As a pack of [Osysharp.PrepaidCredits](https://osysharp.com/reference/ui/prepaid-credits-kit/), 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?   {#check-in}
`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?   {#changes}
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?   {#pause}
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?   {#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?   {#more}
`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?   {#pages}
`/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?   {#privacy}
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](https://osysharp.com/reference/project/bridges/)) — 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       {#examples}

### A co-working space selling day passes and months   {#example-coworking}
```osy title="day passes and hot-desk months at a co-working space, a desk that checks people in, a door that says why" test app=ui-memberships-kit
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       {#see-also}
- [The PrepaidCredits kit — prepaid uses, spent from the pack that expires first, never below zero](https://osysharp.com/reference/ui/prepaid-credits-kit/) — the packs a visit card's visits are kept in
- [deny … except via](https://osysharp.com/reference/security/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](https://osysharp.com/reference/security/past-the-callers-rights/) — the desk acting for a member
- [settings a package asks its app for (app settings)](https://osysharp.com/reference/config/app-settings/) — `app.Memberships`
- [The Privacy kit — erasure that keeps only what the company must or may](https://osysharp.com/reference/security/privacy-kit/) — a person's memberships in their privacy requests
- **`kits/memberships/tests/`** — every behaviour above as a test, the screens under `--pixels`
