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

Pickup #40

Merged
merged 8 commits into from
Feb 5, 2025
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/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
Loading