perf: 优化订阅刷新逻辑
This commit is contained in:
@@ -1,11 +1,12 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing'
|
||||
import { NotFoundException } from '@nestjs/common'
|
||||
import { BadRequestException, NotFoundException } from '@nestjs/common'
|
||||
import { UserService } from '../user.service'
|
||||
import { PrismaService } from '../../prisma/prisma.service'
|
||||
import {
|
||||
MembershipStatus,
|
||||
BookingStatus,
|
||||
UserRole,
|
||||
CardTypeCategory,
|
||||
SubscriptionMessageScene,
|
||||
} from '@mp-pilates/shared'
|
||||
import { ConfigService } from '@nestjs/config'
|
||||
@@ -23,6 +24,7 @@ const makeUser = (overrides: Record<string, unknown> = {}) => ({
|
||||
avatarUrl: 'https://example.com/avatar.png',
|
||||
role: UserRole.MEMBER,
|
||||
adminBookingSubscriptionCount: 0,
|
||||
lastLoginAt: new Date('2024-06-01T08:00:00Z'),
|
||||
createdAt: new Date('2024-01-01T00:00:00Z'),
|
||||
updatedAt: new Date('2024-01-01T00:00:00Z'),
|
||||
_count: { memberships: 2 },
|
||||
@@ -53,6 +55,8 @@ const makeBooking = (
|
||||
const mockPrisma = {
|
||||
user: {
|
||||
findUnique: jest.fn(),
|
||||
findMany: jest.fn(),
|
||||
count: jest.fn(),
|
||||
update: jest.fn(),
|
||||
},
|
||||
subscriptionMessageConsent: {
|
||||
@@ -61,6 +65,13 @@ const mockPrisma = {
|
||||
},
|
||||
booking: {
|
||||
findMany: jest.fn(),
|
||||
groupBy: jest.fn(),
|
||||
},
|
||||
membership: {
|
||||
findFirst: jest.fn(),
|
||||
update: jest.fn(),
|
||||
create: jest.fn(),
|
||||
updateMany: jest.fn(),
|
||||
},
|
||||
}
|
||||
|
||||
@@ -120,6 +131,7 @@ describe('UserService', () => {
|
||||
avatarUrl: 'https://example.com/avatar.png',
|
||||
role: UserRole.MEMBER,
|
||||
activeMembershipCount: 3,
|
||||
inviteShareEligible: true,
|
||||
adminBookingSubscriptionCount: 0,
|
||||
subscriptionMessageTemplates: {
|
||||
templates: [
|
||||
@@ -423,4 +435,153 @@ describe('UserService', () => {
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('getMemberDetail', () => {
|
||||
const cardType = {
|
||||
id: 'ct-1',
|
||||
name: '10次卡',
|
||||
type: CardTypeCategory.TIMES,
|
||||
totalTimes: 10,
|
||||
durationDays: 180,
|
||||
price: 150000,
|
||||
originalPrice: null,
|
||||
description: null,
|
||||
coverUrl: null,
|
||||
isActive: true,
|
||||
sortOrder: 0,
|
||||
createdAt: new Date('2024-01-01T00:00:00Z'),
|
||||
updatedAt: new Date('2024-01-01T00:00:00Z'),
|
||||
}
|
||||
|
||||
const membership = {
|
||||
id: 'mem-1',
|
||||
userId: 'user-1',
|
||||
cardTypeId: 'ct-1',
|
||||
remainingTimes: 6,
|
||||
totalTimes: 10,
|
||||
startDate: new Date('2024-01-01T00:00:00Z'),
|
||||
expireDate: new Date('2099-01-01T00:00:00Z'),
|
||||
status: MembershipStatus.ACTIVE,
|
||||
createdAt: new Date('2024-01-01T00:00:00Z'),
|
||||
updatedAt: new Date('2024-01-01T00:00:00Z'),
|
||||
cardType,
|
||||
}
|
||||
|
||||
it('returns profile, memberships, stats and upcoming bookings', async () => {
|
||||
mockPrisma.user.findUnique.mockResolvedValue({
|
||||
...makeUser(),
|
||||
memberships: [membership],
|
||||
})
|
||||
mockPrisma.booking.groupBy.mockResolvedValue([
|
||||
{ userId: 'user-1', status: BookingStatus.COMPLETED, _count: { id: 3 } },
|
||||
{ userId: 'user-1', status: BookingStatus.CANCELLED, _count: { id: 1 } },
|
||||
{ userId: 'user-1', status: BookingStatus.NO_SHOW, _count: { id: 1 } },
|
||||
])
|
||||
mockPrisma.booking.findMany.mockResolvedValue([
|
||||
{
|
||||
id: 'booking-up-1',
|
||||
status: BookingStatus.CONFIRMED,
|
||||
timeSlot: {
|
||||
date: new Date('2099-12-31T00:00:00Z'),
|
||||
startTime: '09:00',
|
||||
endTime: '10:00',
|
||||
},
|
||||
membership: { cardType: { name: '10次卡' } },
|
||||
},
|
||||
])
|
||||
|
||||
const result = await service.getMemberDetail('user-1')
|
||||
|
||||
expect(result.user.userId).toBe('user-1')
|
||||
expect(result.user.lastLoginAt).toBe('2024-06-01T08:00:00.000Z')
|
||||
expect(result.memberships).toHaveLength(1)
|
||||
expect(result.memberships[0].cardType.name).toBe('10次卡')
|
||||
expect(result.stats).toEqual({
|
||||
totalBookings: 5,
|
||||
completedBookings: 3,
|
||||
cancelledBookings: 1,
|
||||
noShowBookings: 1,
|
||||
})
|
||||
expect(result.upcomingBookings).toEqual([
|
||||
{
|
||||
id: 'booking-up-1',
|
||||
status: BookingStatus.CONFIRMED,
|
||||
date: '2099-12-31',
|
||||
startTime: '09:00',
|
||||
endTime: '10:00',
|
||||
cardName: '10次卡',
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
it('throws NotFoundException when member does not exist', async () => {
|
||||
mockPrisma.user.findUnique.mockResolvedValue(null)
|
||||
|
||||
await expect(service.getMemberDetail('missing')).rejects.toThrow(NotFoundException)
|
||||
})
|
||||
})
|
||||
|
||||
describe('updateMemberProfile', () => {
|
||||
it('updates nickname and phone then returns the dossier', async () => {
|
||||
mockPrisma.user.findUnique
|
||||
.mockResolvedValueOnce(makeUser())
|
||||
.mockResolvedValueOnce({ ...makeUser({ nickname: 'Bob', phone: '13900000000' }), memberships: [] })
|
||||
mockPrisma.user.update.mockResolvedValue(makeUser({ nickname: 'Bob', phone: '13900000000' }))
|
||||
mockPrisma.booking.groupBy.mockResolvedValue([])
|
||||
mockPrisma.booking.findMany.mockResolvedValue([])
|
||||
|
||||
const result = await service.updateMemberProfile('user-1', {
|
||||
nickname: 'Bob',
|
||||
phone: '13900000000',
|
||||
})
|
||||
|
||||
expect(mockPrisma.user.update).toHaveBeenCalledWith({
|
||||
where: { id: 'user-1' },
|
||||
data: { nickname: 'Bob', phone: '13900000000' },
|
||||
})
|
||||
expect(result.user.nickname).toBe('Bob')
|
||||
expect(result.user.phone).toBe('13900000000')
|
||||
})
|
||||
})
|
||||
|
||||
describe('deleteUserMembership', () => {
|
||||
it('expires only the selected membership', async () => {
|
||||
mockPrisma.membership.findFirst.mockResolvedValue({
|
||||
id: 'mem-1',
|
||||
userId: 'user-1',
|
||||
status: MembershipStatus.ACTIVE,
|
||||
})
|
||||
mockPrisma.membership.update.mockResolvedValue({
|
||||
id: 'mem-1',
|
||||
status: MembershipStatus.EXPIRED,
|
||||
})
|
||||
|
||||
await service.deleteUserMembership('user-1', 'mem-1')
|
||||
|
||||
expect(mockPrisma.membership.findFirst).toHaveBeenCalledWith({
|
||||
where: { id: 'mem-1', userId: 'user-1' },
|
||||
})
|
||||
expect(mockPrisma.membership.update).toHaveBeenCalledWith({
|
||||
where: { id: 'mem-1' },
|
||||
data: { status: MembershipStatus.EXPIRED },
|
||||
})
|
||||
expect(mockPrisma.membership.updateMany).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('rejects when the membership is missing or belongs to another user', async () => {
|
||||
mockPrisma.membership.findFirst.mockResolvedValue(null)
|
||||
|
||||
await expect(service.deleteUserMembership('user-1', 'mem-other')).rejects.toThrow(
|
||||
NotFoundException,
|
||||
)
|
||||
expect(mockPrisma.membership.update).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('rejects a missing membershipId', async () => {
|
||||
await expect(service.deleteUserMembership('user-1', '')).rejects.toThrow(
|
||||
BadRequestException,
|
||||
)
|
||||
expect(mockPrisma.membership.findFirst).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user