From 2993fa66226557e144401f869bfef92c7a83e353 Mon Sep 17 00:00:00 2001 From: Joakim Larsson Date: Fri, 22 Sep 2023 10:09:06 +0200 Subject: [PATCH] bugfix: claims are merged --- .../claims/cancel-advert-claim.ts | 13 ++++-- .../claims/convert-advert-claim.ts | 26 ++++++----- .../collecting/collect-advert.ts | 9 ++-- src/adverts/advert-mutations/mappers.ts | 29 ++++++++++++ .../reservations/cancel-advert-reservation.ts | 17 ++++--- .../reservations/reserve-advert.ts | 9 ++-- src/adverts/mappers.spec.ts | 45 +++++++++++++++++++ 7 files changed, 121 insertions(+), 27 deletions(-) diff --git a/src/adverts/advert-mutations/claims/cancel-advert-claim.ts b/src/adverts/advert-mutations/claims/cancel-advert-claim.ts index e8052c8..3babcc3 100644 --- a/src/adverts/advert-mutations/claims/cancel-advert-claim.ts +++ b/src/adverts/advert-mutations/claims/cancel-advert-claim.ts @@ -3,7 +3,10 @@ import type { Services } from '../../../types' import { getAdvertMeta } from '../../advert-meta' import { AdvertClaimType } from '../../types' import type { AdvertClaim, Advert, AdvertMutations } from '../../types' -import { mapTxResultToAdvertMutationResult } from '../mappers' +import { + mapTxResultToAdvertMutationResult, + normalizeAdvertClaims, +} from '../mappers' import { verifyAll, verifyReservationLimits, @@ -48,9 +51,11 @@ export const createCancelAdvertClaim = ) return { ...advert, - claims: advert.claims - .filter(c => !matchClaim(c)) - .filter(({ quantity }) => quantity > 0), + claims: normalizeAdvertClaims( + advert.claims + .filter(c => !matchClaim(c)) + .filter(({ quantity }) => quantity > 0) + ), } }) .verify((_, ctx) => diff --git a/src/adverts/advert-mutations/claims/convert-advert-claim.ts b/src/adverts/advert-mutations/claims/convert-advert-claim.ts index a39bb55..57fa557 100644 --- a/src/adverts/advert-mutations/claims/convert-advert-claim.ts +++ b/src/adverts/advert-mutations/claims/convert-advert-claim.ts @@ -3,7 +3,10 @@ import type { Services } from '../../../types' import { getAdvertMeta } from '../../advert-meta' import { AdvertClaimType } from '../../types' import type { AdvertClaim, Advert, AdvertMutations } from '../../types' -import { mapTxResultToAdvertMutationResult } from '../mappers' +import { + mapTxResultToAdvertMutationResult, + normalizeAdvertClaims, +} from '../mappers' import { verifyAll, verifyReservationLimits, @@ -49,16 +52,17 @@ export const createConvertAdvertClaim = ) return { ...advert, - claims: advert.claims - .filter(c => !matchClaim(c)) - .concat( - claims.map(c => ({ - ...c, - at: new Date().toISOString(), - type: newType, - })) - ) - .filter(({ quantity }) => quantity > 0), + claims: normalizeAdvertClaims( + advert.claims + .filter(c => !matchClaim(c)) + .concat( + claims.map(c => ({ + ...c, + at: new Date().toISOString(), + type: newType, + })) + ) + ), } }) .verify((_, ctx) => diff --git a/src/adverts/advert-mutations/collecting/collect-advert.ts b/src/adverts/advert-mutations/collecting/collect-advert.ts index c17bc63..2d34e88 100644 --- a/src/adverts/advert-mutations/collecting/collect-advert.ts +++ b/src/adverts/advert-mutations/collecting/collect-advert.ts @@ -2,7 +2,10 @@ import { TxErrors, txBuilder } from '../../../transactions' import type { Services } from '../../../types' import { getAdvertMeta } from '../../advert-meta' import { AdvertClaimType, type Advert, type AdvertMutations } from '../../types' -import { mapTxResultToAdvertMutationResult } from '../mappers' +import { + mapTxResultToAdvertMutationResult, + normalizeAdvertClaims, +} from '../mappers' import { verifyAll, verifyReservationLimits, @@ -47,7 +50,7 @@ export const createCollectAdvert = .reduce((s, v) => s + v, 0) return { ...advert, - claims: [ + claims: normalizeAdvertClaims([ ...advert.claims.filter(({ by }) => by !== user.id), // all except mine { by: user.id, @@ -61,7 +64,7 @@ export const createCollectAdvert = quantity: collectedByMeCount + quantity, type: AdvertClaimType.collected, }, - ].filter(({ quantity }) => quantity > 0), + ]), } } return advert diff --git a/src/adverts/advert-mutations/mappers.ts b/src/adverts/advert-mutations/mappers.ts index 0376d1c..f720aa9 100644 --- a/src/adverts/advert-mutations/mappers.ts +++ b/src/adverts/advert-mutations/mappers.ts @@ -1,6 +1,8 @@ +import { mapValues, toLookup } from '../../lib' import type { TxError, TxResult } from '../../transactions' import type { Advert, + AdvertClaim, AdvertMutationResult, AdvertMutationStatus, } from '../types' @@ -28,3 +30,30 @@ export const mapTxResultToAdvertMutationResult = ( advert: null, status: null, } + +export const normalizeAdvertClaims = (claims: AdvertClaim[]): AdvertClaim[] => { + const keyOfClaim = ({ by, type }: AdvertClaim) => `${type}:${by}` + + const groups = toLookup( + claims.filter(c => c.quantity > 0), + keyOfClaim + ) + + const max = (a: T, b: T): T => (b > a ? b : a) + + const combined = mapValues(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(({ at: a }, { at: b }) => + // eslint-disable-next-line no-nested-ternary + a > b ? 1 : a < b ? -1 : 0 + ) +} diff --git a/src/adverts/advert-mutations/reservations/cancel-advert-reservation.ts b/src/adverts/advert-mutations/reservations/cancel-advert-reservation.ts index 0f2422c..6b2e569 100644 --- a/src/adverts/advert-mutations/reservations/cancel-advert-reservation.ts +++ b/src/adverts/advert-mutations/reservations/cancel-advert-reservation.ts @@ -1,7 +1,10 @@ import { txBuilder } from '../../../transactions' import type { Services } from '../../../types' import { AdvertClaimType, type Advert, type AdvertMutations } from '../../types' -import { mapTxResultToAdvertMutationResult } from '../mappers' +import { + mapTxResultToAdvertMutationResult, + normalizeAdvertClaims, +} from '../mappers' export const createCancelAdvertReservation = ({ @@ -30,11 +33,13 @@ export const createCancelAdvertReservation = ) return { ...advert, - claims: advert.claims // remove all reservations for user - .filter( - ({ by, type }) => - !(by === user.id && type === AdvertClaimType.reserved) - ), + claims: normalizeAdvertClaims( + advert.claims // remove all reservations for user + .filter( + ({ by, type }) => + !(by === user.id && type === AdvertClaimType.reserved) + ) + ), } }) .verify(update => update) diff --git a/src/adverts/advert-mutations/reservations/reserve-advert.ts b/src/adverts/advert-mutations/reservations/reserve-advert.ts index 8f79186..198cb7b 100644 --- a/src/adverts/advert-mutations/reservations/reserve-advert.ts +++ b/src/adverts/advert-mutations/reservations/reserve-advert.ts @@ -2,7 +2,10 @@ import { txBuilder } from '../../../transactions' import type { Services } from '../../../types' import type { Advert, AdvertClaim, AdvertMutations } from '../../types' import { AdvertClaimType } from '../../types' -import { mapTxResultToAdvertMutationResult } from '../mappers' +import { + mapTxResultToAdvertMutationResult, + normalizeAdvertClaims, +} from '../mappers' import { verifyAll, verifyReservationLimits, @@ -36,7 +39,7 @@ export const createReserveAdvert = return { ...advert, - claims: [ + claims: normalizeAdvertClaims([ ...advert.claims.filter(a => !isReservedByMe(a)), { by: user.id, @@ -44,7 +47,7 @@ export const createReserveAdvert = quantity: reservedByMeCount + quantity, type: AdvertClaimType.reserved, }, - ], + ]), } } return advert diff --git a/src/adverts/mappers.spec.ts b/src/adverts/mappers.spec.ts index 331809d..8072c3a 100644 --- a/src/adverts/mappers.spec.ts +++ b/src/adverts/mappers.spec.ts @@ -1,4 +1,6 @@ +import { normalizeAdvertClaims } from './advert-mutations/mappers' import { createEmptyAdvertInput, mapCreateAdvertInputToAdvert } from './mappers' +import { AdvertClaim, AdvertClaimType } from './types' describe('mapCreateAdvertInputToAdvert', () => { it('should set input field, user and timestamps', () => { @@ -32,3 +34,46 @@ describe('mapCreateAdvertInputToAdvert', () => { expect(advert.id.length).toBeGreaterThan(32) }) }) + +describe('normalizeAdvertsClaims', () => { + const reserve = (claim: Partial): AdvertClaim => ({ + by: '', + at: new Date().toDateString(), + type: AdvertClaimType.reserved, + quantity: 1, + ...claim, + }) + const collect = (claim: Partial): AdvertClaim => ({ + by: '', + at: new Date().toDateString(), + type: AdvertClaimType.collected, + quantity: 1, + ...claim, + }) + + it('merges entries by owner and type', () => { + expect( + normalizeAdvertClaims([ + reserve({ by: 'a', quantity: 2 }), + collect({ by: 'a' }), + reserve({ by: 'b' }), + reserve({ by: 'a', quantity: 3 }), + ]) + ).toMatchObject([ + reserve({ by: 'a', quantity: 5 }), + collect({ by: 'a' }), + reserve({ by: 'b' }), + ]) + }) + + it('removes entries with 0 quantity', () => { + expect( + normalizeAdvertClaims([ + reserve({ by: 'a', quantity: 0 }), + collect({ by: 'a' }), + reserve({ by: 'b', quantity: 0 }), + reserve({ by: 'a', quantity: 3 }), + ]) + ).toMatchObject([collect({ by: 'a' }), reserve({ by: 'a', quantity: 3 })]) + }) +})