Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/wishlist #34

Merged
merged 5 commits into from
Apr 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/adverts/advert-claims/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { normalizeAdvertClaims } from './normalize-advert-claims'

export { normalizeAdvertClaims }
88 changes: 88 additions & 0 deletions src/adverts/advert-claims/normalize-advert-claims.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import type { AdvertClaim } from '../types'
import { AdvertClaimType } from '../types'
import { normalizeAdvertClaims } from './normalize-advert-claims'

const makeClaim = (c: Partial<AdvertClaim>): AdvertClaim => ({
quantity: 1,
by: '',
at: '',
type: AdvertClaimType.collected,
events: [],
...c,
})

const reserved = (c: Partial<AdvertClaim>) =>
makeClaim({ type: AdvertClaimType.reserved, ...c })
const collected = (c: Partial<AdvertClaim>) =>
makeClaim({ type: AdvertClaimType.collected, ...c })

describe('normalizeAdvertClaims', () => {
const testGroupingByType = (make: typeof makeClaim) => {
const n = normalizeAdvertClaims([
make({
by: 'me',
at: '2020-01-01',
quantity: 1,
}),
make({
by: 'you',
at: '2020-02-02',
quantity: 2,
}),
make({
by: 'me',
at: '2020-03-03',
quantity: 3,
}),
])
expect(n).toMatchObject([
make({
by: 'you',
at: '2020-02-02',
quantity: 2,
}),
make({
by: 'me',
at: '2020-03-03',
quantity: 4,
}),
])
}
it('removes claims with zero quantity', () => {
const n = normalizeAdvertClaims([
reserved({ by: 'a', at: '2021-01-01', quantity: 0 }),
collected({ by: 'b', at: '2022-02-02', quantity: 0 }),
])
expect(n).toHaveLength(0)
})

it('sort claims by date', () => {
const n = normalizeAdvertClaims([
reserved({ by: 'a', at: '2023-03-03' }),
collected({ by: 'b', at: '2022-02-02' }),
])
expect(n).toMatchObject([
collected({ by: 'b', at: '2022-02-02' }),
reserved({ by: 'a', at: '2023-03-03' }),
])
})

it('groups reservations by user', () => testGroupingByType(reserved))
it('groups collects by user', () => testGroupingByType(collected))

it('groups by owner and type', () => {
const at = new Date().toISOString()
expect(
normalizeAdvertClaims([
reserved({ by: 'a', quantity: 2, at }),
collected({ by: 'a', at }),
reserved({ by: 'b', at }),
reserved({ by: 'a', quantity: 3, at }),
])
).toMatchObject([
reserved({ by: 'a', quantity: 5, at }),
collected({ by: 'a', at }),
reserved({ by: 'b', at }),
])
})
})
27 changes: 27 additions & 0 deletions src/adverts/advert-claims/normalize-advert-claims.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { mapValues, sortBy, toLookup } from '../../lib'
import type { AdvertClaim } from '../types'
import { AdvertClaimType } from '../types'

const max = <T>(a: T, b: T): T => (b > a ? b : a)

const stripQuantityZero = (claims: AdvertClaim[]) =>
claims.filter(({ quantity }) => quantity > 0)

export const normalizeAdvertClaims = (claims: AdvertClaim[]): AdvertClaim[] => {
const groups =
// group by (by, type)
toLookup(stripQuantityZero(claims), ({ by, type }) => `${type}:${by}`)

const combined = mapValues<AdvertClaim[], AdvertClaim>(groups, group =>
group.slice(1).reduce(
(agg, c) => ({
...c,
at: max(agg.at, c.at),
quantity: agg.quantity + c.quantity,
}),
group[0]
)
)

return Object.values(combined).sort(sortBy(({ at }) => at))
}
20 changes: 16 additions & 4 deletions src/adverts/advert-meta/advert-meta.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,18 +16,20 @@ export const getAdvertMeta = (
const mine = advert.createdBy === user.id

const claimCount = advert.claims.reduce((s, c) => s + c.quantity, 0)
const myCollectedCount = advert.claims
.filter(c => c.by === user.id && c.type === AdvertClaimType.collected)
const myClaims = advert.claims.filter(c => c.by === user.id)
const myCollectedCount = myClaims
.filter(c => c.type === AdvertClaimType.collected)
.map(c => c.quantity)
.reduce((s, v) => s + v, 0)

const myReservationCount = advert.claims
.filter(c => c.by === user.id && c.type === AdvertClaimType.reserved)
const myReservationCount = myClaims
.filter(c => c.type === AdvertClaimType.reserved)
.map(c => c.quantity)
.reduce((s, v) => s + v, 0)

const isNotArchived = !advert.archivedAt
const isArchived = !isNotArchived
const isOnMyWaitList = advert.waitlist.includes(user.id)

const {
canEditOwnAdverts,
Expand All @@ -38,6 +40,7 @@ export const getAdvertMeta = (
canManageOwnAdvertsHistory,
canManageAllAdverts,
canManageReturns,
canJoinWaitlist,
} = normalizeRoles(user.roles)

const canManageClaims =
Expand Down Expand Up @@ -71,6 +74,13 @@ export const getAdvertMeta = (
isNotArchived &&
(myReservationCount > 0 || quantity > claimCount) &&
canCollectAdverts,
canJoinWaitList:
isNotArchived &&
canJoinWaitlist &&
(canReserveAdverts || canCollectAdverts) &&
quantity <= claimCount &&
!isOnMyWaitList,
canLeaveWaitList: isOnMyWaitList,
canManageClaims:
canManageOwnAdvertsHistory && (mine || canManageAllAdverts),
canReturn: canManageReturns && hasSingleCollectClaim(advert),
Expand All @@ -93,6 +103,8 @@ export const getAdvertMeta = (
canReserve: false,
canCancelReservation: false,
canCollect: false,
canJoinWaitList: false,
canLeaveWaitList: false,
canManageClaims: false,
canReturn: false,
reservedyMe: myReservationCount,
Expand Down
93 changes: 93 additions & 0 deletions src/adverts/advert-meta/tests/can-join-waitlist.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { getAdvertMeta } from '..'
import { makeAdmin, makeUser } from '../../../login'
import type { HaffaUser } from '../../../login/types'
import { createEmptyAdvert } from '../../mappers'
import type { Advert, AdvertClaim } from '../../types'
import { AdvertClaimType, AdvertType } from '../../types'

const makeClaim = (c?: Partial<AdvertClaim>): AdvertClaim => ({
quantity: 1,
by: '',
at: '',
type: AdvertClaimType.collected,
events: [],
...c,
})
const reserved = (c?: Partial<AdvertClaim>) =>
makeClaim({ type: AdvertClaimType.reserved, ...c })
const collected = (c?: Partial<AdvertClaim>) =>
makeClaim({ type: AdvertClaimType.collected, ...c })

describe('getAdvertMeta::canJoinWatlist', () => {
const joinableUser = makeAdmin({ id: 'test@user' })
const deniedUser = makeUser({
id: 'someone@else',
roles: { canJoinWaitlist: false },
})

const advertsInStock = [
createEmptyAdvert({ quantity: 1 }),
createEmptyAdvert({ quantity: 2, claims: [reserved()] }),
createEmptyAdvert({ quantity: 3, claims: [collected()] }),
createEmptyAdvert({ quantity: 10, claims: [reserved(), collected()] }),
]

const advertsOutOfStock = [
createEmptyAdvert({ quantity: 0 }),
createEmptyAdvert({ quantity: 2, claims: [reserved({ quantity: 2 })] }),
createEmptyAdvert({
quantity: 3,
claims: [collected({ quantity: 2 }), reserved()],
}),
createEmptyAdvert({
quantity: 10,
claims: [reserved({ quantity: 5 }), collected({ quantity: 5 })],
}),
]

const isJoinWaitListForEvery = (
adverts: Advert[],
users: HaffaUser[],
expected: boolean
) =>
users.every(u =>
adverts.every(a => getAdvertMeta(a, u).canJoinWaitList === expected)
)

it('joinWaitList -> false when roles::canJoinWaitList -> false', () =>
expect(
isJoinWaitListForEvery(
[...advertsInStock, ...advertsOutOfStock],
[deniedUser],
false
)
).toBe(true))

it('joinWaitList -> false when in stock', () =>
expect(
isJoinWaitListForEvery(
[...advertsInStock],
[joinableUser, deniedUser],
false
)
).toBe(true))

it('joinWaitlist -> true when out of stock and allowed by roles', () =>
expect(
isJoinWaitListForEvery(advertsOutOfStock, [joinableUser], true)
).toBe(true))

it('joinWaitlist -> false when in stock and allowed by roles', () =>
expect(isJoinWaitListForEvery(advertsInStock, [joinableUser], false)).toBe(
true
))

it('joinWaitList -> false when already on waitlist', () =>
expect(
isJoinWaitListForEvery(
advertsOutOfStock.map(a => ({ ...a, waitlist: [joinableUser.id] })),
[joinableUser],
false
)
).toBe(true))
})
9 changes: 2 additions & 7 deletions src/adverts/advert-mutations/claims/cancel-advert-claim.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,10 @@
import { makeUser } from '../../../login'
import { HaffaUser } from '../../../login/types'
import { NotificationService } from '../../../notifications/types'
import { TxErrors, txBuilder } from '../../../transactions'
import type { Services } from '../../../types'
import { normalizeAdvertClaims } from '../../advert-claims'
import { getAdvertMeta } from '../../advert-meta'
import { AdvertClaimType } from '../../types'
import type { AdvertClaim, Advert, AdvertMutations } from '../../types'
import {
mapTxResultToAdvertMutationResult,
normalizeAdvertClaims,
} from '../mappers'
import { mapTxResultToAdvertMutationResult } from '../mappers'
import {
verifyAll,
verifyReservationLimits,
Expand Down
6 changes: 2 additions & 4 deletions src/adverts/advert-mutations/claims/convert-advert-claim.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
import { makeUser } from '../../../login'
import { TxErrors, txBuilder } from '../../../transactions'
import type { Services } from '../../../types'
import { normalizeAdvertClaims } from '../../advert-claims'
import { getAdvertMeta } from '../../advert-meta'
import type { AdvertClaim, Advert, AdvertMutations } from '../../types'
import {
mapTxResultToAdvertMutationResult,
normalizeAdvertClaims,
} from '../mappers'
import { mapTxResultToAdvertMutationResult } from '../mappers'
import {
verifyAll,
verifyReservationLimits,
Expand Down
2 changes: 1 addition & 1 deletion src/adverts/advert-mutations/claims/notify-claims.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import type { HaffaUser } from '../../../login/types'
import type { NotificationService } from '../../../notifications/types'
import { normalizeAdvertClaims } from '../../advert-claims'
import type { Advert, AdvertClaim } from '../../types'
import { AdvertClaimType } from '../../types'
import { normalizeAdvertClaims } from '../mappers'

const all = (promises: Promise<any>[]) =>
Promise.all(promises).then(() => undefined)
Expand Down
6 changes: 2 additions & 4 deletions src/adverts/advert-mutations/claims/notify-expired-claims.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,14 @@
import { Severity } from '../../../syslog/types'
import { txBuilder } from '../../../transactions'
import type { Services } from '../../../types'
import { normalizeAdvertClaims } from '../../advert-claims'
import {
type Advert,
type AdvertMutations,
type AdvertClaim,
AdvertClaimType,
} from '../../types'
import {
mapTxResultToAdvertMutationResult,
normalizeAdvertClaims,
} from '../mappers'
import { mapTxResultToAdvertMutationResult } from '../mappers'
import { isClaimOverdue } from './mappers'

export const createExpiredClaimsNotifier =
Expand Down
6 changes: 2 additions & 4 deletions src/adverts/advert-mutations/claims/notify-overdue-claims.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,14 @@
import { Severity } from '../../../syslog/types'
import { txBuilder } from '../../../transactions'
import type { Services } from '../../../types'
import { normalizeAdvertClaims } from '../../advert-claims'
import {
type Advert,
type AdvertMutations,
AdvertClaimType,
AdvertClaimEventType,
} from '../../types'
import {
mapTxResultToAdvertMutationResult,
normalizeAdvertClaims,
} from '../mappers'
import { mapTxResultToAdvertMutationResult } from '../mappers'
import { getNextClaimEventDate, isClaimOverdue } from './mappers'

export const createOverdueClaimsNotifier =
Expand Down
6 changes: 2 additions & 4 deletions src/adverts/advert-mutations/claims/notify-reserved-claims.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,14 @@
import { Severity } from '../../../syslog/types'
import { txBuilder } from '../../../transactions'
import type { Services } from '../../../types'
import { normalizeAdvertClaims } from '../../advert-claims'
import {
type Advert,
type AdvertMutations,
AdvertClaimEventType,
AdvertClaimType,
} from '../../types'
import {
mapTxResultToAdvertMutationResult,
normalizeAdvertClaims,
} from '../mappers'
import { mapTxResultToAdvertMutationResult } from '../mappers'
import { getNextClaimEventDate } from './mappers'

export const createReservedClaimsNotifier =
Expand Down
6 changes: 2 additions & 4 deletions src/adverts/advert-mutations/claims/renew-advert-claim.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
import { TxErrors, txBuilder } from '../../../transactions'
import type { Services } from '../../../types'
import { normalizeAdvertClaims } from '../../advert-claims'
import { getAdvertMeta } from '../../advert-meta'
import type { Advert, AdvertMutations } from '../../types'
import {
mapTxResultToAdvertMutationResult,
normalizeAdvertClaims,
} from '../mappers'
import { mapTxResultToAdvertMutationResult } from '../mappers'
import {
verifyAll,
verifyReservationLimits,
Expand Down
6 changes: 2 additions & 4 deletions src/adverts/advert-mutations/collecting/collect-advert.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
import { TxErrors, txBuilder } from '../../../transactions'
import type { Services } from '../../../types'
import { normalizeAdvertClaims } from '../../advert-claims'
import { getAdvertMeta } from '../../advert-meta'
import { AdvertClaimType, type Advert, type AdvertMutations } from '../../types'
import {
mapTxResultToAdvertMutationResult,
normalizeAdvertClaims,
} from '../mappers'
import { mapTxResultToAdvertMutationResult } from '../mappers'
import {
verifyAll,
verifyReservationLimits,
Expand Down
Loading
Loading