# The Accounts add-on — the tier, entitlements, account team and VIPs of a customer organisation

> `Osysharp.Organizations.ServicePlans` adds what a vendor keeps about the organisations it SERVES to the base Organizations kit: the tier each is on, what it is entitled to (by tier or its own contract, for a period), the account team looking after it, and its VIP people — set by the app's organisation managers alone, and carried for support and sales kits to route and promise by.

<!-- id: security-organizations-accounts-kit · area: security · stability: preview · html: https://osysharp.com/reference/security/organizations-accounts-kit/ -->

## Summary        {#summary}
`use Osysharp.Organizations.ServicePlans@0;` (with `Osysharp.Organizations`, which it uses) gives an app `CustomerAccount` —
an `Organization` it serves, with a `Tier` — to derive its customers from, `OrganizationTier` rows, `Entitlement`s by
tier or by contract, an `AccountTeamMember` per person looking after an account, and `VipMembership` for a VIP at one.
It asks the base kit's one question, `policy ManagesOrganizations`, and nothing more.

## Signature      {#signature}
```osy syntax
[Authority(Extensible)] public entity CustomerAccount : Organization { OrganizationTier? Tier; }
public entity OrganizationTier { string Name; int Rank; string Description; bool IsDefault; }
[Authority(Extensible)] public entity Entitlement { OrganizationTier? Tier; CustomerAccount? Organization; string Name;
  DateTime? ValidFrom; DateTime? ValidUntil; }
public entity AccountTeamMember { CustomerAccount Organization; Principal Member; AccountTeamRole Role; } // Owner | Member | Backup
public entity VipMembership { Membership Membership; }

List<Entitlement> EntitlementsOf(CustomerAccount org);   OrganizationTier? DefaultTier();
OrganizationTier? TierOfAccount(CustomerAccount? org);
Principal? AccountOwnerOf(CustomerAccount org);           List<AccountTeamMember> AccountTeamOf(CustomerAccount org);
AccountTeamMember SetAccountOwner(CustomerAccount org, Principal person);
AccountTeamMember AddToAccountTeam(CustomerAccount org, Principal person, AccountTeamRole role = AccountTeamRole.Member);
VipMembership MarkVip(CustomerAccount org, Principal person);   void UnmarkVip(CustomerAccount org, Principal person);
bool IsVipPerson(Principal person);
```

## Description    {#description}

### A customer is an organisation you serve     {#customer-account}
Derive the organisations the app serves from `CustomerAccount` — `Company : CustomerAccount` — and keep its own teams
plain `Organization`s ([The Organizations kit — companies, teams, and roles held within one](https://osysharp.com/reference/security/organizations-kit/)): nobody serves a team, so it has no tier. A `CustomerAccount` is
still an `Organization`, so memberships, roles held there and mail domains work on it unchanged. A derived type states
its own `security { }` grants; the add-on's floors hold whatever it grants.

### Tiers and entitlements                       {#tiers}
- **Tiers** are `OrganizationTier` rows the organisation managers define — Standard, Premium, Enterprise — ordered by
  `Rank`, one of them `IsDefault`. They are rows rather than an enum because they are configuration, changed without a
  release. `TierOfAccount(org)` answers the organisation's tier, or the default.
- **Entitlements** — a channel, support hours, a response target — attach to a tier (everybody on it) or to one
  organisation (its own contract), for a period. Derive the kind the app needs (`ResponseTarget : Entitlement { int
  FirstReplyHours; }`); `EntitlementsOf(org)` answers what applies now, the organisation's own first, then its tier's.
- **The tiers page** (`/settings/tiers`) joins the app's settings area for its organisation managers. An app replaces it
  with a component named `TierSettings`, and may render `TierEditor(description: "…")` inside it to keep the editor.

### The account team and VIPs                   {#team}
- **The account team** is the app's own people looking after an organisation: one `Owner`, a pod (`Member`), and a
  `Backup` who covers while the owner is away. An organisation's people can see who looks after them.
- **A VIP** is a person at one organisation — `MarkVip(org, person)` — an executive, a key contact. `IsVipPerson(person)`
  asks across their ACTIVE memberships, so a person whose membership is suspended stops being one.
- **The tier, the account team and who is a VIP are the managers' to set**, never the organisation's own people's,
  whatever the app lets them edit about their company (SECURITY-INVARIANT `organizations-service-is-the-apps-to-assign`).

## Examples       {#examples}

### A supplier's customers on tiers, with an account owner   {#example-accounts}
```osy title="customers on tiers, a contract of their own, an account owner and a VIP" test app=security-organizations-accounts-kit
app SupplierAccounts {
  use Osysharp.Identity;
  use Osysharp.Organizations;
  use Osysharp.Organizations.ServicePlans;
  model "model/**/*.osy";
}

using Osysharp.Identity;
using Osysharp.Organizations;
using Osysharp.Organizations.ServicePlans;

[Role] enum PortalRole { Authenticator, Staff, Buyer }
policy IsStaff => RoleGrant.Any(g => g.Member == user && g.Role == PortalRole.Staff);
policy ManagesOrganizations => IsStaff;

entity Person : Principal {
  [MaxLength(200)] string CodeHash = "";
  security {
    allow read when IsAuthenticated || IsAuthenticator;
    allow create when IsAuthenticator;
    deny read CodeHash when !IsAuthenticator;
  }
}

entity Company : CustomerAccount {
  [MaxLength(40)] string AccountNumber = "";
  security {
    allow read, create, update when IsStaff;
    allow read when IsAuthenticated where IsMemberOf(this);
  }
}

entity ResponseTarget : Entitlement {
  int FirstReplyHours = 24;
  security { allow read, create, update, delete when IsStaff; }
}

Company OpenAccount(string name, string tier, Person owner, Person keyContact) {
  var company = new Company { Name = name, Tier = OrganizationTier.FirstOrDefault(t => t.Name == tier) ?? DefaultTier() };
  SetAccountOwner(company, owner);
  MarkVip(company, keyContact);
  return company;
}

int FirstReplyHoursFor(Company company) {
  var hours = 48;
  foreach (var t in EntitlementsOf(company).OfType<ResponseTarget>()) {
    if (t.FirstReplyHours < hours) { hours = t.FirstReplyHours; }
  }
  return hours;
}

[AuthMethod]
string SignInWithCode(string email, string code) {
  var p = Person.FirstOrDefault(x => x.Email == NormalisedEmail(email));
  if (p == null || !p.IsActive) { Security.VerifyPassword(code); return ""; }
  if (!Security.VerifyPassword(code, p.CodeHash)) { return ""; }
  return Security.IssueJwt(p.Id, p.Email);
}

app.AuthBootstrap = new AuthBootstrap { Role = PortalRole.Authenticator, Login = SignInWithCode };
app.Auth = new PasswordAuth { LoginField = Email, PasswordField = CodeHash };
```

## See also       {#see-also}
- [The Organizations kit — companies, teams, and roles held within one](https://osysharp.com/reference/security/organizations-kit/) — the base kit: memberships, roles held within one organisation, mail domains
- [The WorkQueues kit's floor — restricted queues, who may hold an item, and who moves whose work](https://osysharp.com/reference/security/inbox-kit/) — routes a customer's work to its account team and chooses its promise by tier
- [entity Sub : Base](https://osysharp.com/reference/entity/inheritance/) — deriving `Company` from `CustomerAccount`, and the floor it stands on
