Osy#betaRun it on Osyrin: osyrin.com

Reference / Security

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

use Osysharp.Organizations.ServicePlans@0; · entity Company : CustomerAccount { … } · EntitlementsOf(company) · SetAccountOwner(company, person)

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

preview1 example compiled by CIsecuritykitorganizations

Source on GitHub: Osysharp.Organizations.ServicePlans — github.com/osysharp/organizations/serviceplans ↗

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, Entitlements 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#

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

A customer is an organisation you serve#

Derive the organisations the app serves from CustomerAccount — Company : CustomerAccount — and keep its own teams plain Organizations (The Organizations kit — companies, teams, and roles held within one): 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 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#

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

A supplier's customers on tiers, with an account owner#

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#

Related

The Organizations kit — companies, teams, and roles held within one

`Osysharp.Organizations` holds the groups people belong to — the companies an app's customers work for and the teams…

The WorkQueues kit's floor — restricted queues, who may hold an item, and who moves whose work

What holds in every app on `Osysharp.WorkQueues`, whatever the app grants: an item in a restricted queue is invisible —…

entity Sub : Base

Derives one entity from another. The subtype is its own type with its own name, its own security and its own workflows…

[Authority]

What an app that imports your entity may do to the `security { }` you declared on it. The default is `Narrowable` — the…