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
OrganizationTierrows the organisation managers define — Standard, Premium, Enterprise — ordered byRank, one of themIsDefault. 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 namedTierSettings, and may renderTierEditor(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 aBackupwho 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#
- The Organizations kit — companies, teams, and roles held within one — 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 — routes a customer's work to its account team and chooses its promise by tier
- entity Sub : Base — deriving
CompanyfromCustomerAccount, and the floor it stands on