Skip to content

Commit

Permalink
Pickup (#40)
Browse files Browse the repository at this point in the history
* pick location management

* pickup location in claims

* filter on pickup locations

* advert.place, terms.places support

* place filter

---------

Co-authored-by: Petter Andersson <[email protected]>
  • Loading branch information
jlarsson and petter-a authored Feb 5, 2025
1 parent 7158f1b commit e52c999
Show file tree
Hide file tree
Showing 26 changed files with 453 additions and 37 deletions.
3 changes: 3 additions & 0 deletions src/advert-field-config/mappers.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ it('should override default values with input', () => {
{ ...getFieldConfig('email') },
{ ...getFieldConfig('phone'), adornment: 'dummy' },
{ ...getFieldConfig('country') },
{ ...getFieldConfig('place') },
])
})

Expand Down Expand Up @@ -65,6 +66,7 @@ it('should handle null document', () => {
{ ...getFieldConfig('email') },
{ ...getFieldConfig('phone') },
{ ...getFieldConfig('country') },
{ ...getFieldConfig('place') },
])
})

Expand Down Expand Up @@ -99,6 +101,7 @@ it('should remove duplicates (Keep last)', () => {
{ ...getFieldConfig('email') },
{ ...getFieldConfig('phone') },
{ ...getFieldConfig('country') },
{ ...getFieldConfig('place') },
])
})

Expand Down
2 changes: 2 additions & 0 deletions src/advert-field-config/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ export const ConfigurableFields: Array<FieldName> = [
'email',
'phone',
'country',
'place',
]

export const FieldLabels: Record<FieldName, string> = {
Expand Down Expand Up @@ -65,6 +66,7 @@ export const FieldLabels: Record<FieldName, string> = {
email: 'Email',
phone: 'Telefon',
country: 'Land',
place: 'Plats',
}

export interface FieldConfig {
Expand Down
1 change: 1 addition & 0 deletions src/adverts/advert-mutations/crud/update-advert.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ describe('updateAdvert', () => {
category: 'c',
externalId: 'eid',
tags: ['t'],
place: 'p',
}
const result = await mappedGqlRequest<AdvertMutationResult>(
'updateAdvert',
Expand Down
114 changes: 105 additions & 9 deletions src/adverts/advert-mutations/reservations/reserve-advert.spec.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,28 @@
import {
normalizePickupLocation,
patchAdvertWithPickupLocation,
} from '../../../pickup/mappers'
import {
T,
createTestNotificationServices,
end2endTest,
} from '../../../test-utils'
import { TxErrors } from '../../../transactions'
import { createEmptyAdvert } from '../../mappers'
import { createEmptyAdvert, createEmptyAdvertLocation } from '../../mappers'
import type { AdvertMutationResult } from '../../types'
import { AdvertClaimType } from '../../types'
import { mutationProps } from '../test-utils/gql-test-definitions'

const reserveAdvertMutation = /* GraphQL */ `
mutation Mutation(
$id: ID!
$quantity: Int
) {
reserveAdvert(id: $id, quantity: $quantity) {
${mutationProps}
}
}
mutation Mutation(
$id: ID!
$quantity: Int!
$pickupLocation: PickupLocationInput
) {
reserveAdvert(id: $id, quantity: $quantity, pickupLocation: $pickupLocation) {
${mutationProps}
}
}
`

describe('reserveAdvert', () => {
Expand Down Expand Up @@ -131,4 +136,95 @@ describe('reserveAdvert', () => {
}
)
})

it('handles pickup locations', () => {
const advertWasReserved = jest.fn(async () => undefined)
const advertWasReservedOwner = jest.fn(async () => undefined)
const notifications = createTestNotificationServices({
advertWasReserved,
advertWasReservedOwner,
})

return end2endTest(
{ services: { notifications } },
async ({ mappedGqlRequest, adverts, user, loginPolicies }) => {
// create a pickup location
const pickupLocation = normalizePickupLocation({
name: 'pl1',
adress: 'pl street',
zipCode: '12345',
city: 'pickup town',
notifyEmail: 'notify@me',
})

// give us rights to handle claims
await loginPolicies.updateLoginPolicies([
{
emailPattern: user.id,
roles: ['canReserveAdverts'],
},
])

// eslint-disable-next-line no-param-reassign
adverts['advert-123'] = {
...createEmptyAdvert(),
id: 'advert-123',
quantity: 5,
createdBy: 'some@owner',
location: createEmptyAdvertLocation({
name: 'orig loc',
adress: 'orig street',
}),
}

const result = await mappedGqlRequest<AdvertMutationResult>(
'reserveAdvert',
reserveAdvertMutation,
{
id: 'advert-123',
quantity: 1,
pickupLocation,
}
)
expect(result.status).toBeNull()

T('should have reservation logged in database', () =>
expect(adverts['advert-123'].claims).toMatchObject([
{
by: user.id,
quantity: 1,
type: AdvertClaimType.reserved,
events: [],
pickupLocation,
},
])
)

// for notifications, the announced advert should have a changed location
const notificationAdvert = patchAdvertWithPickupLocation(
adverts['advert-123'],
pickupLocation
)

T('should have notified about the interesting event', () =>
expect(advertWasReserved).toHaveBeenCalledWith(
user.id,
expect.objectContaining(user),
1,
notificationAdvert,
null
)
)
T('pickup location manager should be notified', () =>
expect(advertWasReservedOwner).toHaveBeenCalledWith(
'notify@me',
expect.objectContaining(user),
1,
notificationAdvert,
null
)
)
}
)
})
})
12 changes: 7 additions & 5 deletions src/adverts/advert-mutations/reservations/reserve-advert.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { patchAdvertWithPickupLocation } from '../../../pickup/mappers'
import { txBuilder } from '../../../transactions'
import type { Services } from '../../../types'
import { normalizeAdvertClaims } from '../../advert-claims'
Expand All @@ -18,7 +19,7 @@ export const createReserveAdvert =
Services,
'adverts' | 'notifications'
>): AdvertMutations['reserveAdvert'] =>
(user, id, quantity) =>
(user, id, quantity, location) =>
txBuilder<Advert>()
.load(() => adverts.getAdvert(user, id))
.validate(() => {})
Expand All @@ -30,14 +31,14 @@ export const createReserveAdvert =
user.id,
user,
quantity,
patched,
patchAdvertWithPickupLocation(patched, location),
null
),
notifications.advertWasReservedOwner(
advert.createdBy,
location?.notifyEmail || advert.createdBy,
user,
quantity,
patched,
patchAdvertWithPickupLocation(patched, location),
null
),
])
Expand All @@ -47,7 +48,7 @@ export const createReserveAdvert =
by === user.id && type === AdvertClaimType.reserved
const reservedByMeCount = advert.claims
.filter(isReservedByMe)
.map(({ quantity }) => quantity)
.map(c => c.quantity)
.reduce((s, v) => s + v, 0)

return {
Expand All @@ -60,6 +61,7 @@ export const createReserveAdvert =
quantity: reservedByMeCount + quantity,
type: AdvertClaimType.reserved,
events: [],
pickupLocation: location,
},
]),
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export const advertProps = `
city
country
}
place
`

export const advertWithMetaProps = `
Expand Down Expand Up @@ -70,6 +71,7 @@ export const advertWithMetaProps = `
city
country
}
place
meta {
reservableQuantity
collectableQuantity
Expand Down
7 changes: 5 additions & 2 deletions src/adverts/adverts-gql-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,9 +68,12 @@ export const createAdvertsGqlModule = (
.then(result =>
mapAdvertMutationResultToAdvertWithMetaMutationResult(user, result)
),
reserveAdvert: async ({ ctx: { user }, args: { id, quantity } }) =>
reserveAdvert: async ({
ctx: { user },
args: { id, quantity, pickupLocation },
}) =>
createAdvertMutations(services)
.reserveAdvert(user, id, quantity)
.reserveAdvert(user, id, quantity, pickupLocation)
.then(result =>
mapAdvertMutationResultToAdvertWithMetaMutationResult(user, result)
),
Expand Down
14 changes: 13 additions & 1 deletion src/adverts/adverts.gql.schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,11 @@ export const advertsGqlSchema = /* GraphQL */ `
createAdvert(input: AdvertInput!): AdvertMutationResult
updateAdvert(id: ID!, input: AdvertInput!): AdvertMutationResult
removeAdvert(id: ID!): AdvertMutationResult
reserveAdvert(id: ID!, quantity: Int = 1): AdvertMutationResult
reserveAdvert(
id: ID!
quantity: Int = 1
pickupLocation: PickupLocationInput
): AdvertMutationResult
cancelAdvertReservation(id: ID!): AdvertMutationResult
collectAdvert(id: ID!, quantity: Int = 1): AdvertMutationResult
archiveAdvert(id: ID!): AdvertMutationResult
Expand Down Expand Up @@ -144,12 +148,18 @@ export const advertsGqlSchema = /* GraphQL */ `
cursor: String
}
input AdvertWorkflowInput {
pickupLocationTrackingNames: [String]
places: [String]
}
input AdvertFilterInput {
search: String
fields: AdvertFieldsFilterInput
restrictions: AdvertRestrictionsInput
sorting: AdvertSortingInput
paging: AdvertPagingInput
workflow: AdvertWorkflowInput
}
input AdvertLocationInput {
Expand Down Expand Up @@ -188,6 +198,7 @@ export const advertsGqlSchema = /* GraphQL */ `
tags: [String]
location: AdvertLocationInput
contact: AdvertContactInput
place: String
}
enum AdvertClaimEventType {
reminder
Expand Down Expand Up @@ -293,6 +304,7 @@ export const advertsGqlSchema = /* GraphQL */ `
tags: [String]
location: AdvertLocation
contact: AdvertContact
place: String
}
type AdvertListPaging {
Expand Down
64 changes: 49 additions & 15 deletions src/adverts/filters/advert-filter-predicate.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import type { HaffaUser } from '../../login/types'
import { getAdvertMeta } from '../advert-meta'
import {
AdvertClaimType,
type Advert,
type AdvertFilterInput,
type AdvertRestrictionsFilterInput,
import { AdvertClaimType } from '../types'
import type {
AdvertWorkflowInput,
Advert,
AdvertFilterInput,
AdvertRestrictionsFilterInput,
} from '../types'
import { createFieldFilterPredicate } from './field-filter-predicate'
import type { Predicate } from './types'
Expand Down Expand Up @@ -34,6 +35,24 @@ const createFreeTextPredicate = (search: string): Predicate<Advert> => {
: () => true
}

const createWorkflowPredicate = (
workflow?: AdvertWorkflowInput
): Predicate<Advert> | null => {
const pickupLocationTrackingNames = (): Predicate<Advert> | null => {
const s = new Set(
(workflow?.pickupLocationTrackingNames || []).filter(v => v)
)
return s.size > 0
? a => a.claims.some(c => s.has(c.pickupLocation?.name || ''))
: null
}
const places = (): Predicate<Advert> | null => {
const s = new Set((workflow?.places || []).filter(v => v))
return s.size > 0 ? a => s.has(a.place) : null
}
return combineAnd(pickupLocationTrackingNames(), places())
}

const createRestrictionsPredicate = (
user: HaffaUser,
restrictions: AdvertRestrictionsFilterInput
Expand Down Expand Up @@ -96,15 +115,29 @@ const createRestrictionsPredicate = (
: () => true
}

const combineAnd =
(...matchers: Predicate<Advert>[]): Predicate<Advert> =>
advert =>
matchers.every(matcher => matcher(advert))
const combineAnd = (
...matchers: (Predicate<Advert> | null)[]
): Predicate<Advert> | null => {
const ml = matchers.filter(v => v)
// eslint-disable-next-line no-nested-ternary
return ml.length === 0
? null
: ml.length === 1
? ml[0]
: advert => ml.every(m => m!(advert))
}

const combineOr =
(...matchers: Predicate<Advert>[]): Predicate<Advert> =>
advert =>
matchers.some(matcher => matcher(advert))
const combineOr = (
...matchers: (Predicate<Advert> | null)[]
): Predicate<Advert> | null => {
const ml = matchers.filter(v => v)
// eslint-disable-next-line no-nested-ternary
return ml.length === 0
? null
: ml.length === 1
? ml[0]
: advert => ml.some(m => m!(advert))
}

export const createAdvertFilterPredicate = (
user: HaffaUser,
Expand All @@ -120,5 +153,6 @@ export const createAdvertFilterPredicate = (
createFieldFilterPredicate(fields)
) || [])
),
createRestrictionsPredicate(user, input?.restrictions || {})
)
createRestrictionsPredicate(user, input?.restrictions || {}),
createWorkflowPredicate(input?.workflow)
) ?? (() => true)
Loading

0 comments on commit e52c999

Please sign in to comment.