Skip to content

Commit

Permalink
Added GetCurrentProfile request, and refined organization and members…
Browse files Browse the repository at this point in the history
…hips use cases.
  • Loading branch information
jezzsantos committed Apr 18, 2024
1 parent 6bb8c06 commit f7d2dc5
Show file tree
Hide file tree
Showing 67 changed files with 1,782 additions and 903 deletions.
1 change: 1 addition & 0 deletions docs/design-principles/0000-all-use-cases.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ When a person is registered we also query the `IAvatarService` to see if we can
2. Change the address of the profile
3. Add an Avatar image the profile
4. Remove the Avatar from the profile
5. Inspect the profile of the current (Authenticated) user

## Backend for Frontend

Expand Down
2 changes: 1 addition & 1 deletion docs/design-principles/0020-api-framework.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ public sealed class CarsApi : IWebApiService
var car = await _carsApplication.RegisterCarAsync(_contextFactory.Create(), request.Make, request.Model, request.Year,
cancellationToken);

return () => car.HandleApplicationResult<GetCarResponse, Car>(c =>
return () => car.HandleApplicationResult<Car, GetCarResponse>(c =>
new PostResult<GetCarResponse>(new GetCarResponse { Car = c }, $"/cars/{c.Id}"));
}

Expand Down
46 changes: 31 additions & 15 deletions docs/design-principles/0160-user-lifecycle.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,28 +28,30 @@ Users self-register in the `Identity` subdomain via either `PasswordCredentials`

### Organizations

An `Organization` has a type of either `Personal` or `Shared`.
An `Organization` has a classification of either `Personal` or `Shared`.

* `Shared` organizations are intended for use by companies/workgroups/organizations/teams/etc.
* `Personal` organizations are for each `EndUser` (person or machine) to use on the platform and cannot be shared with others.
* A person/machine will have a membership to one `Personal` organization at all times, and can have a membership to one or more `Shared` organizations.

> Note: By default, The primary use case of a `Personal` org is for the user to still be able to login to the platform and at least use basic functionality of the product without having to upgrade to paid features. The basic plan is assumed by default to be a "free" (unpaid) plan. This is intended so that they have to have a space in the product of their own, from which they can still use the product even if they are removed from other companies organizations, and from there add other (paid) organizations if they wish to, or be invited into other organizations later.
#### Roles and Responsibilities

* Any person can have the `Member` and/or `Owner` and/or `BillingAdmin` roles in an organization.
* Only `Owner` roles can assign/unassign roles to other members.
* Any person can have the `BillingAdmin` role of an organization, but they must also have the `Owner` role.
* Every organization (`Shared` or `Personal`) will always have a person who is the "billing subscriber". This person has a fiscal responsibility to pay for billing charges for that organization (tenant). The "billing subscriber" must always have the `Owner` and `BillingAdmin` roles at all times.
* When a person creates a new organization, they automatically become an `Owner` and `BillingAdmin` of it, as well as the "billing subscriber" for it.
* Any person can have the `Member` and/or `Owner` and/or `BillingAdmin` roles in an organization. Machines can be members of other organizations too.
* Only `Owner` roles can assign/unassign the roles of other members.
* Any person (not machine) can have the `BillingAdmin` role of an organization, but they must also have the `Owner` role.
* Every organization (`Shared` or `Personal`) will always have a person who is the "billing subscriber". This person has a fiscal responsibility to pay for billing charges for that organization (tenant). The "billing subscriber" must always have the `Owner` and `BillingAdmin` roles at all times. (A "Billing Subscriber" is not a role, but it is an attribute of an organization).
* When a person (not a machine) creates a new `Shared` organization, they automatically become an `Owner` and `BillingAdmin` of it, as well as the "billing subscriber" for it.
* From that point, they can assign the `Owner` and `BillingAdmin` roles to one or more other members of the organization.
* The "billing subscriber" responsibility must be transferred via a (voluntary) payment method submission from another person with the `BillingAdmin` (role).
* The "billing subscriber" responsibility can only be transferred via a (voluntary) payment method submission from another `BillingAdmin` of the organization. (and when the current 'Billing Subscriber' payment method has expired, and then an existing `BillingAdmin` can assume the position)

#### Personal Organizations

* Every `EndUser` (person or machine) has one `Personal` organization.

* It is automatically created for them when they register on the platform. It is named after that person/machine.
* That person/machine is the only member of that organization.
* That person/machine is the only member of that organization. No other members (person or machines) can be added to it.

* They have the roles of `Owner` and `BillingAdmin`, and they are also the "billing subscriber" for it.

Expand All @@ -59,21 +61,35 @@ An `Organization` has a type of either `Personal` or `Shared`.
#### Shared Organizations

* `Shared` organizations can be created at any time by any person on the platform (not machines).
* Any other person/machine can be invited to join them. When they are, they are created a `Membership` to that `Organization`, and each `Membership` maintains its own roles.
* `Shared` organizations can be created at any time by any person (not machine) on the platform.
* Any other person/machine can be invited to join them. When the person/machine is added, they are created a `Membership` to that `Organization` with a set of default roles (i.e. `Member`). Each `Membership` maintains its own roles.
* A person (or machine) can be a `Member` (role) of any number of `Shared` organizations into which they can be invited, removed, or they can leave themselves.
* A person (not a machine) can be assigned/unassigned any number of roles in those other organizations.
* A person (not a machine) can be assigned/unassigned any number of other roles in those other organizations. A machine can only be a `Member`, and not a `Onwer` or `BillingAdmin`.
* A `Shared` organization must have at least one `Owner` (role) and one `BillingAdmin` (role) at all times, and they can be the same person or different persons. Like a `Personal` organization, a `Shared` will have one and only one "billing subscriber", who is ultimately responsible for any charges for the organization.
* A `Shared` organization can be deleted. However, they have to be deleted by the designated "billing subscriber" and only once all members are removed from it.

### Memberships

When an `EndUser` joins an `Organization` they get a membership to that organization.

Memberships belong to the `EndUser` (subdomain), not to the `Organization`.

> Although, in practice, there is a very tight coupling between `EndUser` -> `Membership` <- `Organization`. Nonetheless, since the `EndUser` and `Organization` subdomains are presently separated, `Membership` belongs to `EndUser` subdomain. You may notice that the `Organization` keeps mementos of its `Memberships` in that subdomain also.
A user can have one or more memberships. A user will always have at least one membership to their own `Personal` organization. Which they should never lose.

Every user will also have a "default" membership (a.k.a. their "default organization"). That is, a membership that the system can assume is the one they are working with at any time. Of course, this default can change at any time.

When a user is invited into another organization, or if they create a new organization themselves, this default will always change to be the last organization they joined.

### Guest Invitations

Guest invitations are the mechanism to introduce and refer new users to the product.
Guest invitations are the mechanism to introduce (and refer) new users to the product.

#### To The Platform

* Any authenticated user can invite a guest to the platform (i.e., without any affiliation to any organization)
* A "guest invitation" requires only an email address and has an expiry (7 days, by default)
* A "guest invitation" requires only an email address and has an expiry (14 days, by default)
* The person is contacted at that email address and given a link to register with the platform (in the web app). The link contains an `InvitationToken`.
* In the web app, the `InvitationToken` is first verified to check if it is valid (and not expired), and if so, the guest is presented with a registration form, which accepts an email address and password, which is pre-populated with the email address and a "guessed" name (derived from their email address). Or they can sign up with an SSO provider.
* In either case, when signing up with password credentials or signing in with SSO, the registration could include the referral `InvitationToken`. It will not include this token if they sign up on their own.
Expand All @@ -83,6 +99,6 @@ Guest invitations are the mechanism to introduce and refer new users to the prod
#### To An Organization

* Any organization owner (role) can invite a guest to their organization (by email) or invite an existing user to their organization (by email or by ID)
* As above, a "guest invitation" requires only an email address and has an expiry (7 days, by default)
* As above, a "guest invitation" requires only an email address and has an expiry (14 days, by default)
* A "guest invitation" follows the same process as above, except that when they eventually register (either by accepting the guest invitation with a different email address or by registering with the same email address that they were invited with), they will added to the organization.
* This organization (or the last one they were invited to) will become their default organization (rather than their `Personal` organization).
* This organization (or the last one they were invited to) will become their "default" organization (rather than their `Personal` organization).
2 changes: 1 addition & 1 deletion src/AncillaryInfrastructure/Api/Audits/AuditsApi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ public async Task<ApiPostResult<bool, DeliverMessageResponse>> Deliver(DeliverAu
var delivered =
await _ancillaryApplication.DeliverAuditAsync(_contextFactory.Create(), request.Message, cancellationToken);

return () => delivered.HandleApplicationResult<DeliverMessageResponse, bool>(_ =>
return () => delivered.HandleApplicationResult<bool, DeliverMessageResponse>(_ =>
new PostResult<DeliverMessageResponse>(new DeliverMessageResponse { IsDelivered = true }));
}

Expand Down
2 changes: 1 addition & 1 deletion src/AncillaryInfrastructure/Api/Emails/EmailsApi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ public async Task<ApiPostResult<bool, DeliverMessageResponse>> Deliver(DeliverEm
var delivered =
await _ancillaryApplication.DeliverEmailAsync(_contextFactory.Create(), request.Message, cancellationToken);

return () => delivered.HandleApplicationResult<DeliverMessageResponse, bool>(_ =>
return () => delivered.HandleApplicationResult<bool, DeliverMessageResponse>(_ =>
new PostResult<DeliverMessageResponse>(new DeliverMessageResponse { IsDelivered = true }));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ public async Task<ApiPostResult<bool, DeliverMessageResponse>> Notify(NotifyProv
await _ancillaryApplication.NotifyProvisioningAsync(_contextFactory.Create(), request.Message,
cancellationToken);

return () => delivered.HandleApplicationResult<DeliverMessageResponse, bool>(_ =>
return () => delivered.HandleApplicationResult<bool, DeliverMessageResponse>(_ =>
new PostResult<DeliverMessageResponse>(new DeliverMessageResponse { IsDelivered = true }));
}

Expand Down
2 changes: 1 addition & 1 deletion src/AncillaryInfrastructure/Api/Usages/UsagesApi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ public async Task<ApiPostResult<bool, DeliverMessageResponse>> Deliver(DeliverUs
var delivered =
await _ancillaryApplication.DeliverUsageAsync(_contextFactory.Create(), request.Message, cancellationToken);

return () => delivered.HandleApplicationResult<DeliverMessageResponse, bool>(_ =>
return () => delivered.HandleApplicationResult<bool, DeliverMessageResponse>(_ =>
new PostResult<DeliverMessageResponse>(new DeliverMessageResponse { IsDelivered = true }));
}

Expand Down
13 changes: 11 additions & 2 deletions src/Application.Resources.Shared/UserProfile.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ public class UserProfile : IIdentifiableResource

public string? AvatarUrl { get; set; }

public UserProfileClassification Classification { get; set; }

public required string DisplayName { get; set; }

public string? EmailAddress { get; set; }
Expand All @@ -19,13 +21,20 @@ public class UserProfile : IIdentifiableResource

public string? Timezone { get; set; }

public UserProfileClassification Classification { get; set; }

public required string UserId { get; set; }

public required string Id { get; set; }
}

public class UserProfileForCurrent : UserProfileWithDefaultMembership
{
public List<string> Features { get; set; } = new();

public bool IsAuthenticated { get; set; }

public List<string> Roles { get; set; } = new();
}

public enum UserProfileClassification
{
Person = 0,
Expand Down
3 changes: 3 additions & 0 deletions src/Application.Services.Shared/IEndUsersService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ Task<Result<Optional<EndUser>, Error>> FindPersonByEmailPrivateAsync(ICallerCont
Task<Result<EndUserWithMemberships, Error>> GetMembershipsPrivateAsync(ICallerContext caller, string id,
CancellationToken cancellationToken);

Task<Result<EndUser, Error>> GetUserPrivateAsync(ICallerContext caller, string id,
CancellationToken cancellationToken);

Task<Result<SearchResults<MembershipWithUserProfile>, Error>> ListMembershipsForOrganizationAsync(
ICallerContext caller,
string organizationId, SearchOptions searchOptions, GetOptions getOptions, CancellationToken cancellationToken);
Expand Down
2 changes: 1 addition & 1 deletion src/BookingsInfrastructure/Api/Bookings/BookingsApi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ public async Task<ApiPostResult<Booking, MakeBookingResponse>> Make(MakeBookingR
var booking = await _bookingsApplication.MakeBookingAsync(_contextFactory.Create(), request.OrganizationId!,
request.CarId, request.StartUtc, request.EndUtc, cancellationToken);

return () => booking.HandleApplicationResult<MakeBookingResponse, Booking>(c =>
return () => booking.HandleApplicationResult<Booking, MakeBookingResponse>(c =>
new PostResult<MakeBookingResponse>(new MakeBookingResponse { Booking = c }));
}

Expand Down
2 changes: 1 addition & 1 deletion src/CarsInfrastructure/Api/Cars/CarsApi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ public async Task<ApiPostResult<Car, GetCarResponse>> Register(RegisterCarReques
var car = await _carsApplication.RegisterCarAsync(_contextFactory.Create(), request.OrganizationId!,
request.Make, request.Model, request.Year, request.Jurisdiction, request.NumberPlate, cancellationToken);

return () => car.HandleApplicationResult<GetCarResponse, Car>(c =>
return () => car.HandleApplicationResult<Car, GetCarResponse>(c =>
new PostResult<GetCarResponse>(new GetCarResponse { Car = c }, new GetCarRequest { Id = c.Id }.ToUrl()));
}

Expand Down
3 changes: 3 additions & 0 deletions src/Domain.Events.Shared/EndUsers/MembershipAdded.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using Domain.Common;
using Domain.Common.ValueObjects;
using Domain.Shared.Organizations;
using JetBrains.Annotations;

namespace Domain.Events.Shared.EndUsers;
Expand All @@ -23,5 +24,7 @@ public MembershipAdded()

public required string OrganizationId { get; set; }

public OrganizationOwnership Ownership { get; set; }

public required List<string> Roles { get; set; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,13 @@ public MembershipDefaultChanged()
{
}

public required string FromMembershipId { get; set; }
public required List<string> Features { get; set; }

public string? FromMembershipId { get; set; }

public required List<string> Roles { get; set; }

public required string ToMembershipId { get; set; }

public required string ToOrganizationId { get; set; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using Domain.Common;
using Domain.Common.ValueObjects;
using JetBrains.Annotations;

namespace Domain.Events.Shared.UserProfiles;

public sealed class DefaultOrganizationChanged : DomainEvent
{
public DefaultOrganizationChanged(Identifier id) : base(id)
{
}

[UsedImplicitly]
public DefaultOrganizationChanged()
{
}

public string? FromOrganizationId { get; set; }

public required string ToOrganizationId { get; set; }
}
Loading

0 comments on commit f7d2dc5

Please sign in to comment.