diff --git a/src/commands/area/add-owner-form.ts b/src/commands/area/add-owner-form.ts index a509972a..7c5f512c 100644 --- a/src/commands/area/add-owner-form.ts +++ b/src/commands/area/add-owner-form.ts @@ -2,7 +2,7 @@ import {flow, pipe} from 'fp-ts/lib/function'; import * as RA from 'fp-ts/ReadonlyArray'; import * as E from 'fp-ts/Either'; import * as O from 'fp-ts/Option'; -import {DomainEvent, User} from '../../types'; +import {EmailAddress, User} from '../../types'; import * as t from 'io-ts'; import {StatusCodes} from 'http-status-codes'; import {formatValidationErrors} from 'io-ts-reporters'; @@ -11,20 +11,31 @@ import { failureWithStatus, } from '../../types/failure-with-status'; import {Form} from '../../types/form'; -import {AreaOwners} from '../../read-models/members/get-potential-owners'; -import {readModels} from '../../read-models'; import {html, joinHtml, safe, sanitizeString} from '../../types/html'; -import {Member} from '../../read-models/members/return-types'; import {pageTemplate} from '../../templates'; import {renderMemberNumber} from '../../templates/member-number'; import {SharedReadModel} from '../../read-models/shared-state'; -import {areasTable} from '../../read-models/shared-state/state'; -import {eq} from 'drizzle-orm'; +import { + areasTable, + membersTable, + ownersTable, +} from '../../read-models/shared-state/state'; +import {eq, notInArray} from 'drizzle-orm'; + +type Member = { + memberNumber: number; + emailAddress: EmailAddress; + name: O.Option; + agreementSigned: O.Option; +}; type ViewModel = { user: User; areaId: string; - areaOwners: AreaOwners; + areaOwners: { + existing: ReadonlyArray; + potential: ReadonlyArray; + }; areaName: string; }; @@ -144,15 +155,52 @@ const getAreaId = (input: unknown) => ) ); -const getPotentialOwners = ( - events: ReadonlyArray, +const getExistingAndPotentialOwners = ( + db: SharedReadModel['db'], areaId: string -) => - pipe( - events, - readModels.members.getPotentialOwners(areaId), - E.fromOption(failureWithStatus('No such area', StatusCodes.NOT_FOUND)) - ); +): ViewModel['areaOwners'] => { + const existing = db + .select({ + memberNumber: ownersTable.memberNumber, + emailAddress: membersTable.emailAddress, + name: membersTable.name, + agreementSigned: membersTable.agreementSigned, + }) + .from(ownersTable) + .innerJoin( + membersTable, + eq(membersTable.memberNumber, ownersTable.memberNumber) + ) + .where(eq(ownersTable.areaId, areaId)) + .all() + .map(member => ({ + ...member, + agreementSigned: O.fromNullable(member.agreementSigned), + })); + const potential = db + .select({ + memberNumber: membersTable.memberNumber, + emailAddress: membersTable.emailAddress, + name: membersTable.name, + agreementSigned: membersTable.agreementSigned, + }) + .from(membersTable) + .where( + notInArray( + membersTable.memberNumber, + existing.map(({memberNumber}) => memberNumber) + ) + ) + .all() + .map(member => ({ + ...member, + agreementSigned: O.fromNullable(member.agreementSigned), + })); + return { + existing, + potential, + }; +}; const getAreaName = (db: SharedReadModel['db'], areaId: string) => pipe( @@ -172,13 +220,15 @@ const getAreaName = (db: SharedReadModel['db'], areaId: string) => const constructForm: Form['constructForm'] = input => - ({user, events, readModel}): E.Either => + ({user, readModel}): E.Either => pipe( {user}, E.right, E.bind('areaId', () => getAreaId(input)), - E.bind('areaOwners', ({areaId}) => getPotentialOwners(events, areaId)), - E.bind('areaName', ({areaId}) => getAreaName(readModel.db, areaId)) + E.bind('areaName', ({areaId}) => getAreaName(readModel.db, areaId)), + E.bind('areaOwners', ({areaId}) => + E.right(getExistingAndPotentialOwners(readModel.db, areaId)) + ) ); export const addOwnerForm: Form = { diff --git a/src/read-models/members/get-potential-owners.ts b/src/read-models/members/get-potential-owners.ts deleted file mode 100644 index af88b968..00000000 --- a/src/read-models/members/get-potential-owners.ts +++ /dev/null @@ -1,39 +0,0 @@ -import * as O from 'fp-ts/Option'; -import * as RA from 'fp-ts/ReadonlyArray'; -import {DomainEvent} from '../../types'; -import {pipe} from 'fp-ts/lib/function'; -import {replayState} from '../shared-state'; -import {Member} from './return-types'; - -export type AreaOwners = { - existing: ReadonlyArray; - potential: ReadonlyArray; -}; - -export const getPotentialOwners = - (areaId: string) => - (events: ReadonlyArray): O.Option => - pipe( - events, - replayState, - O.some, - O.bind('requestedArea', ({areas}) => O.fromNullable(areas.get(areaId))), - O.bind('existing', ({members, requestedArea}) => - pipe( - requestedArea.owners, - owners => Array.from(owners.values()), - O.traverseArray(memberNumber => - O.fromNullable(members.get(memberNumber)) - ) - ) - ), - O.bind('potential', ({requestedArea, members}) => - pipe( - Array.from(members.values()), - RA.filter( - ({memberNumber: number}) => !requestedArea.owners.has(number) - ), - O.some - ) - ) - ); diff --git a/src/read-models/members/index.ts b/src/read-models/members/index.ts index f8a3f27f..65a472c1 100644 --- a/src/read-models/members/index.ts +++ b/src/read-models/members/index.ts @@ -1,6 +1,5 @@ import {getAll, getAllDetails, getAllDetailsAsActor} from './get-all'; import {lookupByEmail} from './lookup-by-email'; -import {getPotentialOwners} from './get-potential-owners'; import {getFailedImports} from './get-failed-imports'; export const members = { @@ -9,7 +8,6 @@ export const members = { getAllDetails, getAllDetailsAsActor, getFailedImports, - getPotentialOwners, }; export {FailedLinking} from './failed-linking'; diff --git a/tests/read-models/members/get-potential-owners.test.ts b/tests/read-models/members/get-potential-owners.test.ts deleted file mode 100644 index 577a81bd..00000000 --- a/tests/read-models/members/get-potential-owners.test.ts +++ /dev/null @@ -1,105 +0,0 @@ -import {faker} from '@faker-js/faker'; -import * as O from 'fp-ts/Option'; -import {getPotentialOwners} from '../../../src/read-models/members/get-potential-owners'; -import {TestFramework, initTestFramework} from '../test-framework'; -import {EmailAddress} from '../../../src/types'; -import {NonEmptyString, UUID} from 'io-ts-types'; -import {getSomeOrFail} from '../../helpers'; -import {pipe} from 'fp-ts/lib/function'; - -describe('getPotentialOwners', () => { - let framework: TestFramework; - beforeEach(async () => { - framework = await initTestFramework(); - }); - - const callQuery = async (areaId: UUID) => - pipe( - await framework.getAllEvents(), - getPotentialOwners(areaId as string), - getSomeOrFail - ); - - let result: Awaited>; - - describe('when the area does not exist', () => { - it('returns None', async () => { - expect( - getPotentialOwners(faker.string.uuid())(await framework.getAllEvents()) - ).toStrictEqual(O.none); - }); - }); - - const linkNumber = { - email: faker.internet.email() as EmailAddress, - memberNumber: faker.number.int(), - }; - const addName = { - name: faker.person.fullName(), - memberNumber: linkNumber.memberNumber, - }; - const createArea = { - id: faker.string.uuid() as UUID, - name: faker.company.buzzNoun() as NonEmptyString, - }; - - describe('when a member is already an owner of the area', () => { - beforeEach(async () => { - await framework.commands.memberNumbers.linkNumberToEmail(linkNumber); - await framework.commands.area.create(createArea); - await framework.commands.area.addOwner({ - memberNumber: linkNumber.memberNumber, - areaId: createArea.id, - }); - await framework.commands.members.editName(addName); - result = await callQuery(createArea.id); - }); - - it('includes them in the existing owners', () => { - expect(result.existing).toHaveLength(1); - expect(result.potential).toHaveLength(0); - expect(result.existing[0].memberNumber).toStrictEqual( - linkNumber.memberNumber - ); - expect(result.existing[0].name).toStrictEqual(O.some(addName.name)); - }); - }); - - describe('when a member is not an owner of the area', () => { - beforeEach(async () => { - await framework.commands.memberNumbers.linkNumberToEmail(linkNumber); - await framework.commands.members.editName(addName); - await framework.commands.area.create(createArea); - result = await callQuery(createArea.id); - }); - - it('includes them in the potential owners', () => { - expect(result.existing).toHaveLength(0); - expect(result.potential).toHaveLength(1); - expect(result.potential[0].memberNumber).toStrictEqual( - linkNumber.memberNumber - ); - expect(result.potential[0].name).toStrictEqual(O.some(addName.name)); - }); - }); - - describe('when a member has signed the owner agreement', () => { - const signAgreement = { - signedAt: faker.date.soon(), - memberNumber: linkNumber.memberNumber, - }; - beforeEach(async () => { - await framework.commands.memberNumbers.linkNumberToEmail(linkNumber); - await framework.commands.members.editName(addName); - await framework.commands.members.signOwnerAgreement(signAgreement); - await framework.commands.area.create(createArea); - result = await callQuery(createArea.id); - }); - - it('includes the date they signed', () => { - expect(result.potential[0].agreementSigned).toStrictEqual( - O.some(signAgreement.signedAt) - ); - }); - }); -});