Skip to content

Commit

Permalink
Merge pull request #476 from bcgov/development
Browse files Browse the repository at this point in the history
ownership change functionality
  • Loading branch information
bdolor authored Apr 23, 2024
2 parents 7a2a656 + 68c3900 commit 98ca37e
Show file tree
Hide file tree
Showing 8 changed files with 421 additions and 45 deletions.
157 changes: 123 additions & 34 deletions src/back-end/lib/db/affiliation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
readOneOrganizationSlim
} from "back-end/lib/db/organization";
import { readOneUser } from "back-end/lib/db/user";
import { Knex } from "knex";
import { valid } from "shared/lib/http";
import {
Affiliation,
Expand Down Expand Up @@ -265,60 +266,148 @@ export const approveAffiliation = tryDb<[Id], Affiliation>(
export const updateAdminStatus = tryDb<
[Id, MembershipType, AuthenticatedSession],
Affiliation
>(
async (
connection,
id,
membershipType: MembershipType,
session: AuthenticatedSession
) => {
>(async (connection, id, membershipType, session) => {
const now = new Date();
return await connection.transaction(async (trx) => {
const [affiliation] = await connection<RawAffiliation>("affiliations")
.transacting(trx)
.update(
{
membershipType,
updatedAt: now
} as RawAffiliation,
"*"
)
.where({
id
})
.whereIn("organization", function () {
this.select("id").from("organizations").where({
active: true
});
});

if (!affiliation) {
throw new Error("unable to update admin status");
}

const [affiliationEvent] = await connection<RawHistoryRecord>(
"affiliationEvents"
)
.transacting(trx)
.insert(
{
id: generateUuid(),
affiliation: affiliation.id,
event:
membershipType === MembershipType.Admin
? AffiliationEvent.AdminStatusGranted
: AffiliationEvent.AdminStatusRevoked,
createdAt: now,
createdBy: session.user.id
},
"*"
);

if (!affiliationEvent) {
throw new Error("unable to create affiliation event");
}

return valid(await rawAffiliationToAffiliation(connection, affiliation));
});
});

/**
* Utility for affiliation updates.
*
* @param connection Knex connection
* @param rawAffiliation partial affiliation
* @param queryObject object containing queryable columns
* @returns Knex Affiliation Query
*/
function affiliationUpdateQuery(
connection: Connection,
rawAffiliation: Partial<RawAffiliation>,
queryObject: Record<string, Knex.MaybeRawColumn<string>>
) {
return connection<RawAffiliation>("affiliations")
.update(rawAffiliation, "*")
.where(queryObject)
.whereIn("organization", function () {
this.select("id").from("organizations").where({
active: true
});
});
}

/**
* Sets original owner to a member and the passed the passed in id's member to
* the new owner. Records the events in the affiliation events table.
*/
export const changeOwner = tryDb<[Id, Id, AuthenticatedSession], Affiliation>(
async (connection, id, orgId, session) => {
const now = new Date();
return await connection.transaction(async (trx) => {
const [affiliation] = await connection<RawAffiliation>("affiliations")
.transacting(trx)
.update(
{
membershipType,
updatedAt: now
} as RawAffiliation,
"*"
)
.where({
id
})
.whereIn("organization", function () {
this.select("id").from("organizations").where({
active: true
});
});
const [previousOwnerAffiliation] = await affiliationUpdateQuery(
connection,
{ membershipType: MembershipType.Member, updatedAt: now },
{ organization: orgId, membershipType: MembershipType.Owner }
).transacting(trx);

if (!previousOwnerAffiliation) {
throw new Error("unable to set affiliation type to member");
}

const [previousOwnerAffiliationEvent] =
await connection<RawHistoryRecord>("affiliationEvents")
.transacting(trx)
.insert(
{
id: generateUuid(),
affiliation: previousOwnerAffiliation.id,
event: AffiliationEvent.OwnerStatusRevoked,
createdAt: now,
createdBy: session.user.id
},
"*"
);

if (!previousOwnerAffiliationEvent) {
throw new Error("unable to create affiliation event");
}

const [newOwnerAffiliation] = await affiliationUpdateQuery(
connection,
{ membershipType: MembershipType.Owner, updatedAt: now },
{ id }
).transacting(trx);

if (!affiliation) {
throw new Error("unable to update admin status");
if (!newOwnerAffiliation) {
throw new Error("unable to set affiliation type to owner");
}

const [affiliationEvent] = await connection<RawHistoryRecord>(
const [newOwnerAffiliationEvent] = await connection<RawHistoryRecord>(
"affiliationEvents"
)
.transacting(trx)
.insert(
{
id: generateUuid(),
affiliation: affiliation.id,
event:
membershipType === MembershipType.Admin
? AffiliationEvent.AdminStatusGranted
: AffiliationEvent.AdminStatusRevoked,
affiliation: id,
event: AffiliationEvent.OwnerStatusGranted,
createdAt: now,
createdBy: session.user.id
},
"*"
);

if (!affiliationEvent) {
if (!newOwnerAffiliationEvent) {
throw new Error("unable to create affiliation event");
}

return valid(await rawAffiliationToAffiliation(connection, affiliation));
return valid(
await rawAffiliationToAffiliation(connection, newOwnerAffiliation)
);
});
}
);
Expand Down
47 changes: 45 additions & 2 deletions src/back-end/lib/resources/affiliation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ import {
UpdateValidationErrors,
UpdateRequestBody as SharedUpdateRequestBody,
adminStatusToAffiliationMembershipType,
memberIsOwner
memberIsOwner,
memberIsPending
} from "shared/lib/resources/affiliation";
import { Organization } from "shared/lib/resources/organization";
import { AuthenticatedSession, Session } from "shared/lib/resources/session";
Expand Down Expand Up @@ -57,7 +58,10 @@ export type UpdateRequestBody = SharedUpdateRequestBody | null;

type ValidatedUpdateRequestBody = {
session: AuthenticatedSession;
body: ADT<"approve"> | ADT<"updateAdminStatus", MembershipType>;
body:
| ADT<"approve">
| ADT<"updateAdminStatus", MembershipType>
| ADT<"changeOwner", Organization["id"]>;
};

type ValidatedDeleteRequestBody = Id;
Expand Down Expand Up @@ -312,6 +316,8 @@ const update: crud.Update<
return null;
}
}
case "changeOwner":
return adt("changeOwner");
default:
return null;
}
Expand Down Expand Up @@ -399,6 +405,35 @@ const update: crud.Update<
body: adt("updateAdminStatus" as const, membershipType)
});
}
case "changeOwner": {
if (memberIsOwner(existingAffiliation)) {
return invalid({
affiliation: ["Membership type is already owner."]
});
}

// Do not allow Pending Members
if (memberIsPending(existingAffiliation)) {
return invalid({
affiliation: ["Membership type is pending."]
});
}

// Only admins are able to perform ownership changes.
if (!permissions.isAdmin(request.session)) {
return invalid({
permissions: [permissions.ERROR_MESSAGE]
});
}

return valid({
session: request.session,
body: adt(
"changeOwner" as const,
existingAffiliation.organization.id
)
});
}
default:
return invalid({ affiliation: adt("parseFailure" as const) });
}
Expand Down Expand Up @@ -426,6 +461,14 @@ const update: crud.Update<
session
);
break;
case "changeOwner":
dbResult = await db.changeOwner(
connection,
id,
body.value,
session
);
break;
}
switch (dbResult.tag) {
case "valid":
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {
doesOrganizationMeetSWUQualification,
doesOrganizationMeetTWUQualification
} from "shared/lib/resources/organization";
import { compareStrings } from "shared/lib";

interface ValidState<K extends Tab.TabId> extends Tab.ParentState<K> {
viewerUser: User;
Expand Down Expand Up @@ -153,7 +154,10 @@ function makeComponent<K extends Tab.TabId>(): component_.page.Component<
organization,
swuQualified,
twuQualified,
affiliations,
// Sort affiliations for a consistent team UI
affiliations: affiliations.sort((a, b) =>
compareStrings(a.user.name, b.user.name)
),
viewerUser: state.viewerUser
});
// Everything checks out, return valid state.
Expand Down
Loading

0 comments on commit 98ca37e

Please sign in to comment.