feat: add monthly card periods and apparatus lesson feedback

This commit is contained in:
richarjiang
2026-10-18 15:47:29 +08:00
parent b452de379d
commit a3d58fa8f2
40 changed files with 968 additions and 170 deletions

View File

@@ -84,6 +84,7 @@ const mockActiveMembership = {
const mockDurationMembership = {
...mockActiveMembership,
id: 'mem-duration-001',
expireDate: new Date('2100-01-01'),
cardTypeId: mockDurationCardType.id,
remainingTimes: null,
cardType: mockDurationCardType,
@@ -138,6 +139,7 @@ const mockStudioConfig = {
function buildTxMock(overrides: Record<string, unknown> = {}) {
return {
$queryRaw: jest.fn(),
timeSlot: {
findUnique: jest.fn(),
update: jest.fn(),
@@ -255,6 +257,54 @@ describe('BookingService', () => {
afterEach(() => jest.clearAllMocks())
describe('monthly period booking boundaries', () => {
const planned = { ...mockLimitedDurationMembership, startDate:new Date('2099-12-30T00:00:00Z'),expireDate:new Date('2100-01-29T00:00:00Z'),status:MembershipStatus.SCHEDULED }
it('lets a paid future period reserve only its own dates and keeps its quota separate',async()=>{
const tx=buildTxMock()
tx.timeSlot.findUnique.mockResolvedValue(mockOpenSlot)
tx.booking.findUnique.mockResolvedValue(null)
tx.membership.findUnique.mockResolvedValue(planned)
tx.booking.create.mockResolvedValue(mockConfirmedBooking)
;(prisma.$transaction as jest.Mock).mockImplementation(fn=>fn(tx))
;(prisma.booking.findUnique as jest.Mock).mockResolvedValue({...mockConfirmedBooking,timeSlot:mockOpenSlot,membership:planned})
;(prisma.user.findMany as jest.Mock).mockResolvedValue([])
await service.createBooking(MOCK_USER_ID,{timeSlotId:MOCK_SLOT_ID,membershipId:planned.id})
expect(tx.booking.create).toHaveBeenCalledWith(expect.objectContaining({data:expect.objectContaining({membershipId:planned.id})}))
expect(tx.membership.update).not.toHaveBeenCalled()
})
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')}
tx.timeSlot.findUnique.mockResolvedValue(mockOpenSlot)
tx.booking.findUnique.mockResolvedValue(null)
tx.membership.findUnique.mockResolvedValue(membership)
;(prisma.$transaction as jest.Mock).mockImplementation(fn=>fn(tx))
await expect(service.createBooking(MOCK_USER_ID,{timeSlotId:MOCK_SLOT_ID,membershipId:membership.id})).rejects.toThrow('周期')
expect(tx.booking.create).not.toHaveBeenCalled()
})
it('deducts a reserved future period without activating it ahead of time',async()=>{
const tx=buildTxMock()
tx.booking.findUnique.mockResolvedValue({...mockConfirmedBooking,status:BookingStatus.PENDING_CONFIRMATION,timeSlot:mockOpenSlot,membership:planned,membershipId:planned.id})
tx.booking.update.mockResolvedValue(mockConfirmedBooking)
;(prisma.$transaction as jest.Mock).mockImplementation(fn=>fn(tx))
;(prisma.booking.findUnique as jest.Mock).mockResolvedValue({...mockConfirmedBooking,timeSlot:mockOpenSlot,membership:planned})
;(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:planned.id},data:{remainingTimes:4,status:MembershipStatus.SCHEDULED}})
})
it('rejects administrative arrangement outside the selected period',async()=>{
const tx=buildTxMock()
tx.timeSlot.findUnique.mockResolvedValue(mockOpenSlot)
tx.user.findUnique.mockResolvedValue({id:MOCK_USER_ID})
tx.booking.findUnique.mockResolvedValue(null)
tx.membership.findUnique.mockResolvedValue({...planned,startDate:new Date('2100-01-01T00:00:00Z')})
;(prisma.$transaction as jest.Mock).mockImplementation(fn=>fn(tx))
await expect(service.adminArrangeBooking(MOCK_ADMIN_ID,{userId:MOCK_USER_ID,timeSlotId:MOCK_SLOT_ID,membershipId:planned.id})).rejects.toThrow('周期')
expect(tx.timeSlot.updateMany).not.toHaveBeenCalled()
})
})
describe('confirmBooking', () => {
it('sends booking confirmed subscription message after admin confirmation', async () => {
const tx = buildTxMock({