feat: add monthly card periods and apparatus lesson feedback
This commit is contained in:
@@ -6,6 +6,7 @@ import {
|
||||
computeMembershipGrant,
|
||||
getMembershipRenewalHint,
|
||||
pickRenewalTarget,
|
||||
getMembershipEffectiveState,
|
||||
} from '@mp-pilates/shared'
|
||||
import type { RenewalHintMembership } from '@mp-pilates/shared'
|
||||
|
||||
@@ -143,7 +144,23 @@ describe('computeMembershipGrant', () => {
|
||||
expect(result.expireDate.getTime()).toBe(now.getTime() + 30 * 86_400_000)
|
||||
})
|
||||
|
||||
it('stacks sessions for an active count-limited DURATION card', () => {
|
||||
it.each([
|
||||
{ expireDate: '2026-05-01T00:00:00.000Z', status: MembershipStatus.EXPIRED },
|
||||
{ expireDate: '2026-05-01T00:00:00.000Z', status: MembershipStatus.ACTIVE },
|
||||
{ expireDate: now.toISOString(), status: MembershipStatus.ACTIVE },
|
||||
{ expireDate: '2026-07-01T00:00:00.000Z', status: MembershipStatus.EXPIRED },
|
||||
{ expireDate: '2026-05-01T00:00:00.000Z', status: MembershipStatus.USED_UP },
|
||||
])('resets an expired 10-session monthly plan with 6 unused sessions: %j', expiry => {
|
||||
const result = computeMembershipGrant({ existing: { remainingTimes: 6, totalTimes: 10, ...expiry }, cardType: limitedDurationCard, now })
|
||||
expect(result).toEqual({ startDate: now, isRenewal: true, remainingTimes: 10, totalTimes: 10, expireDate: new Date(now.getTime() + 30 * 86_400_000) })
|
||||
})
|
||||
|
||||
it('does not extend an explicitly expired unlimited monthly plan from a future stored date', () => {
|
||||
const result = computeMembershipGrant({ existing: { remainingTimes: null, totalTimes: null, expireDate: '2026-07-01T00:00:00.000Z', status: MembershipStatus.EXPIRED }, cardType: durationCard, now })
|
||||
expect(result.expireDate.getTime()).toBe(now.getTime() + 30 * 86_400_000)
|
||||
})
|
||||
|
||||
it('keeps a renewed monthly quota separate and schedules it after the current period', () => {
|
||||
const expireDate = new Date('2026-07-01T00:00:00Z')
|
||||
const result = computeMembershipGrant({
|
||||
existing: {
|
||||
@@ -158,13 +175,39 @@ describe('computeMembershipGrant', () => {
|
||||
|
||||
expect(result).toMatchObject({
|
||||
isRenewal: true,
|
||||
remainingTimes: 13,
|
||||
totalTimes: 20,
|
||||
remainingTimes: 10,
|
||||
totalTimes: 10,
|
||||
startDate: expireDate,
|
||||
})
|
||||
expect(result.expireDate.getTime()).toBe(expireDate.getTime() + 30 * 86_400_000)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Effective monthly-card benefits', () => {
|
||||
it('expires unused monthly sessions immediately even before the scheduled status update', () => {
|
||||
const original = membership({ cardTypeId: 'monthly', remainingTimes: 6, status: MembershipStatus.ACTIVE, expireDate: now.toISOString(), cardType: { type: CardTypeCategory.DURATION } })
|
||||
expect(getMembershipEffectiveState(original, now)).toEqual({ status: MembershipStatus.EXPIRED, remainingTimes: 0 })
|
||||
expect(original.remainingTimes).toBe(6)
|
||||
expect(original.status).toBe(MembershipStatus.ACTIVE)
|
||||
})
|
||||
it('keeps unused monthly sessions available until the exact expiry instant', () => {
|
||||
const original = membership({ cardTypeId: 'monthly', remainingTimes: 6, expireDate: new Date(now.getTime() + 1).toISOString(), cardType: { type: CardTypeCategory.DURATION } })
|
||||
expect(getMembershipEffectiveState(original, now)).toEqual({ status: MembershipStatus.ACTIVE, remainingTimes: 6 })
|
||||
})
|
||||
it('honors explicit EXPIRED status even when an administrator left a future date', () => {
|
||||
const original = membership({ cardTypeId: 'monthly', remainingTimes: 6, status: MembershipStatus.EXPIRED, cardType: { type: CardTypeCategory.DURATION } })
|
||||
expect(getMembershipEffectiveState(original, now)).toEqual({ status: MembershipStatus.EXPIRED, remainingTimes: 0 })
|
||||
})
|
||||
it('keeps expired times-card balances for history without marking them usable', () => {
|
||||
const original = membership({ cardTypeId: 'times', remainingTimes: 6, expireDate: now.toISOString() })
|
||||
expect(getMembershipEffectiveState(original, now)).toEqual({ status: MembershipStatus.EXPIRED, remainingTimes: 6 })
|
||||
})
|
||||
it('preserves the unlimited-plan representation after expiry', () => {
|
||||
const original = membership({ cardTypeId: 'monthly', remainingTimes: null, expireDate: now.toISOString(), cardType: { type: CardTypeCategory.DURATION } })
|
||||
expect(getMembershipEffectiveState(original, now)).toEqual({ status: MembershipStatus.EXPIRED, remainingTimes: null })
|
||||
})
|
||||
})
|
||||
|
||||
describe('getMembershipRenewalHint', () => {
|
||||
it('prefers a TIMES card running low over a DURATION card near expiry', () => {
|
||||
const hint = getMembershipRenewalHint(
|
||||
@@ -303,6 +346,13 @@ describe('getMembershipRenewalHint', () => {
|
||||
})
|
||||
|
||||
describe('pickRenewalTarget', () => {
|
||||
it('queues monthly purchases after the live tail instead of an explicitly expired future-dated record', () => {
|
||||
const target = pickRenewalTarget([
|
||||
membership({cardTypeId:'monthly',expireDate:'2099-01-01T00:00:00Z',status:MembershipStatus.EXPIRED,cardType:{type:CardTypeCategory.DURATION}}),
|
||||
membership({cardTypeId:'monthly',startDate:'2026-07-01T00:00:00Z',expireDate:'2026-07-31T00:00:00Z',status:MembershipStatus.SCHEDULED,cardType:{type:CardTypeCategory.DURATION}}),
|
||||
],'monthly')
|
||||
expect(target?.expireDate).toBe('2026-07-31T00:00:00Z')
|
||||
})
|
||||
it('picks the latest expiring membership of the same card type', () => {
|
||||
const target = pickRenewalTarget(
|
||||
[
|
||||
|
||||
Reference in New Issue
Block a user