feat: 下线支付功能; 月卡生效时间从第一次约课开始

This commit is contained in:
richarjiang
2026-10-30 20:51:21 +08:00
parent a192bbb186
commit 21558f841d
42 changed files with 490 additions and 196 deletions

View File

@@ -277,6 +277,43 @@ describe('BookingService', () => {
expect(tx.booking.create).toHaveBeenCalledWith(expect.objectContaining({data:expect.objectContaining({membershipId:planned.id})}))
expect(tx.membership.update).not.toHaveBeenCalled()
})
it('keeps a new monthly card undated through reservation and confirmation', async () => {
jest.useFakeTimers().setSystemTime(new Date('2099-12-30T00:00:00Z'))
try {
const pending = { ...mockLimitedDurationMembership, startDate: null, expireDate: null, activationDurationDays: 30, status: MembershipStatus.SCHEDULED }
const tx = buildTxMock()
tx.timeSlot.findUnique.mockResolvedValue(mockOpenSlot)
tx.booking.findUnique.mockResolvedValue(null)
tx.membership.findUnique.mockResolvedValue(pending)
tx.booking.create.mockResolvedValue({ ...mockConfirmedBooking, status: BookingStatus.PENDING_CONFIRMATION })
;(prisma.$transaction as jest.Mock).mockImplementation(fn => fn(tx))
;(prisma.booking.findUnique as jest.Mock).mockResolvedValue({ ...mockConfirmedBooking, timeSlot: mockOpenSlot, membership: pending })
;(prisma.user.findMany as jest.Mock).mockResolvedValue([])
await service.createBooking(MOCK_USER_ID, { timeSlotId: MOCK_SLOT_ID, membershipId: pending.id })
expect(tx.membership.update).not.toHaveBeenCalled()
tx.booking.findUnique.mockResolvedValue({ ...mockConfirmedBooking, status: BookingStatus.PENDING_CONFIRMATION, membershipId: pending.id, timeSlot: mockOpenSlot, membership: pending })
tx.booking.update.mockResolvedValue(mockConfirmedBooking)
;(prisma.user.findUnique as jest.Mock).mockResolvedValue(null)
studioService.getInfo.mockResolvedValue(mockStudioConfig as never)
await service.confirmBooking(MOCK_BOOKING_ID, MOCK_ADMIN_ID)
expect(tx.membership.update).toHaveBeenCalledWith({ where: { id: pending.id }, data: { remainingTimes: 4, status: MembershipStatus.SCHEDULED } })
expect(pending.startDate).toBeNull()
expect(pending.expireDate).toBeNull()
} finally { jest.useRealTimers() }
})
it('refunds cancellation to the pending card without setting activation dates', async () => {
const pending = { ...mockLimitedDurationMembership, startDate: null, expireDate: null, activationDurationDays: 30, status: MembershipStatus.SCHEDULED }
const tx = buildTxMock()
const booking = { ...mockConfirmedBooking, membershipId: pending.id, membership: pending, timeSlot: { ...mockOpenSlot, bookedCount: 1 } }
;(prisma.booking.findUnique as jest.Mock).mockResolvedValue(booking)
;(prisma.user.findUnique as jest.Mock).mockResolvedValue(null)
;(prisma.$transaction as jest.Mock).mockImplementation(fn => fn(tx))
studioService.getInfo.mockResolvedValue(mockStudioConfig as never)
tx.booking.update.mockResolvedValue({ ...booking, status: BookingStatus.CANCELLED })
const result = await service.cancelBooking(MEMBER_ACTOR, MOCK_BOOKING_ID)
expect(result.refunded).toBe(true)
expect(tx.membership.update).toHaveBeenCalledWith({ where: { id: pending.id }, data: { remainingTimes: 6, status: MembershipStatus.SCHEDULED } })
})
it.each(['before','after'])('rejects a course %s the monthly period before writing a booking',async boundary=>{
const tx=buildTxMock()
const membership=boundary==='before'?{...planned,startDate:new Date('2100-01-01T00:00:00Z')}:{...planned,expireDate:new Date('2099-12-31T00:00:00Z')}