asdasd
This commit is contained in:
@@ -23,6 +23,7 @@ export function SseProvider() {
|
|||||||
|
|
||||||
function invalidateOrderQueries(orderId: unknown) {
|
function invalidateOrderQueries(orderId: unknown) {
|
||||||
if (!orderId) return
|
if (!orderId) return
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['me', 'orders'] })
|
||||||
queryClient.invalidateQueries({ queryKey: ['me', 'orders', orderId] })
|
queryClient.invalidateQueries({ queryKey: ['me', 'orders', orderId] })
|
||||||
queryClient.invalidateQueries({ queryKey: ['admin', 'orders', 'detail', orderId] })
|
queryClient.invalidateQueries({ queryKey: ['admin', 'orders', 'detail', orderId] })
|
||||||
queryClient.invalidateQueries({ queryKey: ['admin', 'orders'] })
|
queryClient.invalidateQueries({ queryKey: ['admin', 'orders'] })
|
||||||
|
|||||||
@@ -107,6 +107,7 @@ describe('SseProvider', () => {
|
|||||||
|
|
||||||
expect(mockInvalidateQueries).toHaveBeenCalledWith({ queryKey: ['me', 'messages', 'unread-count'] })
|
expect(mockInvalidateQueries).toHaveBeenCalledWith({ queryKey: ['me', 'messages', 'unread-count'] })
|
||||||
expect(mockInvalidateQueries).toHaveBeenCalledWith({ queryKey: ['me', 'conversations'] })
|
expect(mockInvalidateQueries).toHaveBeenCalledWith({ queryKey: ['me', 'conversations'] })
|
||||||
|
expect(mockInvalidateQueries).toHaveBeenCalledWith({ queryKey: ['me', 'orders'] })
|
||||||
expect(mockInvalidateQueries).toHaveBeenCalledWith({ queryKey: ['me', 'orders', 'o1'] })
|
expect(mockInvalidateQueries).toHaveBeenCalledWith({ queryKey: ['me', 'orders', 'o1'] })
|
||||||
expect(mockInvalidateQueries).toHaveBeenCalledWith({ queryKey: ['admin', 'orders', 'detail', 'o1'] })
|
expect(mockInvalidateQueries).toHaveBeenCalledWith({ queryKey: ['admin', 'orders', 'detail', 'o1'] })
|
||||||
expect(mockInvalidateQueries).toHaveBeenCalledWith({ queryKey: ['admin', 'orders'] })
|
expect(mockInvalidateQueries).toHaveBeenCalledWith({ queryKey: ['admin', 'orders'] })
|
||||||
@@ -118,6 +119,7 @@ describe('SseProvider', () => {
|
|||||||
renderSse()
|
renderSse()
|
||||||
const handler = mockEventHandlers['order:statusChanged']
|
const handler = mockEventHandlers['order:statusChanged']
|
||||||
handler(new MessageEvent('order:statusChanged', { data: JSON.stringify({ orderId: 'o2' }) }))
|
handler(new MessageEvent('order:statusChanged', { data: JSON.stringify({ orderId: 'o2' }) }))
|
||||||
|
expect(mockInvalidateQueries).toHaveBeenCalledWith({ queryKey: ['me', 'orders'] })
|
||||||
expect(mockInvalidateQueries).toHaveBeenCalledWith({ queryKey: ['me', 'orders', 'o2'] })
|
expect(mockInvalidateQueries).toHaveBeenCalledWith({ queryKey: ['me', 'orders', 'o2'] })
|
||||||
expect(mockInvalidateQueries).toHaveBeenCalledWith({ queryKey: ['admin', 'orders', 'detail', 'o2'] })
|
expect(mockInvalidateQueries).toHaveBeenCalledWith({ queryKey: ['admin', 'orders', 'detail', 'o2'] })
|
||||||
expect(mockInvalidateQueries).toHaveBeenCalledWith({ queryKey: ['admin', 'orders'] })
|
expect(mockInvalidateQueries).toHaveBeenCalledWith({ queryKey: ['admin', 'orders'] })
|
||||||
@@ -129,6 +131,7 @@ describe('SseProvider', () => {
|
|||||||
renderSse()
|
renderSse()
|
||||||
const handler = mockEventHandlers['order:updated']
|
const handler = mockEventHandlers['order:updated']
|
||||||
handler(new MessageEvent('order:updated', { data: JSON.stringify({ orderId: 'o3' }) }))
|
handler(new MessageEvent('order:updated', { data: JSON.stringify({ orderId: 'o3' }) }))
|
||||||
|
expect(mockInvalidateQueries).toHaveBeenCalledWith({ queryKey: ['me', 'orders'] })
|
||||||
expect(mockInvalidateQueries).toHaveBeenCalledWith({ queryKey: ['me', 'orders', 'o3'] })
|
expect(mockInvalidateQueries).toHaveBeenCalledWith({ queryKey: ['me', 'orders', 'o3'] })
|
||||||
expect(mockInvalidateQueries).toHaveBeenCalledWith({ queryKey: ['admin', 'orders', 'detail', 'o3'] })
|
expect(mockInvalidateQueries).toHaveBeenCalledWith({ queryKey: ['admin', 'orders', 'detail', 'o3'] })
|
||||||
expect(mockInvalidateQueries).toHaveBeenCalledWith({ queryKey: ['admin', 'orders'] })
|
expect(mockInvalidateQueries).toHaveBeenCalledWith({ queryKey: ['admin', 'orders'] })
|
||||||
|
|||||||
Binary file not shown.
@@ -39,6 +39,19 @@ describe('isAdminUser', () => {
|
|||||||
expect(isAdminUser(null)).toBe(false)
|
expect(isAdminUser(null)).toBe(false)
|
||||||
expect(isAdminUser(undefined)).toBe(false)
|
expect(isAdminUser(undefined)).toBe(false)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('normalizes email before comparing with ADMIN_EMAIL', () => {
|
||||||
|
const previousAdminEmail = process.env.ADMIN_EMAIL
|
||||||
|
process.env.ADMIN_EMAIL = 'Admin@Test.com'
|
||||||
|
|
||||||
|
expect(isAdminUser({ email: ' admin@test.com ' })).toBe(true)
|
||||||
|
|
||||||
|
if (previousAdminEmail === undefined) {
|
||||||
|
delete process.env.ADMIN_EMAIL
|
||||||
|
} else {
|
||||||
|
process.env.ADMIN_EMAIL = previousAdminEmail
|
||||||
|
}
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('buildSseListeners', () => {
|
describe('buildSseListeners', () => {
|
||||||
@@ -96,6 +109,20 @@ describe('buildSseListeners', () => {
|
|||||||
cleanup()
|
cleanup()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('forwards order:statusChanged to admin', () => {
|
||||||
|
const cleanup = buildSseListeners('admin-1', true, eventBus, write)
|
||||||
|
eventBus.emit('order:statusChanged', {
|
||||||
|
orderId: 'o1',
|
||||||
|
userId: 'user-1',
|
||||||
|
oldStatus: 'READY_FOR_PICKUP',
|
||||||
|
newStatus: 'DONE',
|
||||||
|
})
|
||||||
|
expect(write).toHaveBeenCalledTimes(1)
|
||||||
|
expect(write.mock.calls[0][0]).toContain('event: order:statusChanged')
|
||||||
|
expect(write.mock.calls[0][0]).toContain('"orderId":"o1"')
|
||||||
|
cleanup()
|
||||||
|
})
|
||||||
|
|
||||||
it('forwards payment:statusChanged to matching userId', () => {
|
it('forwards payment:statusChanged to matching userId', () => {
|
||||||
const cleanup = buildSseListeners('user-1', false, eventBus, write)
|
const cleanup = buildSseListeners('user-1', false, eventBus, write)
|
||||||
eventBus.emit('payment:statusChanged', { orderId: 'o1', userId: 'user-1', paymentStatus: 'paid' })
|
eventBus.emit('payment:statusChanged', { orderId: 'o1', userId: 'user-1', paymentStatus: 'paid' })
|
||||||
@@ -104,6 +131,15 @@ describe('buildSseListeners', () => {
|
|||||||
cleanup()
|
cleanup()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('forwards payment:statusChanged to admin', () => {
|
||||||
|
const cleanup = buildSseListeners('admin-1', true, eventBus, write)
|
||||||
|
eventBus.emit('payment:statusChanged', { orderId: 'o1', userId: 'user-1', paymentStatus: 'paid' })
|
||||||
|
expect(write).toHaveBeenCalledTimes(1)
|
||||||
|
expect(write.mock.calls[0][0]).toContain('event: order:statusChanged')
|
||||||
|
expect(write.mock.calls[0][0]).toContain('"orderId":"o1"')
|
||||||
|
cleanup()
|
||||||
|
})
|
||||||
|
|
||||||
it('forwards order:deliveryFeeAdjusted to matching userId', () => {
|
it('forwards order:deliveryFeeAdjusted to matching userId', () => {
|
||||||
const cleanup = buildSseListeners('user-1', false, eventBus, write)
|
const cleanup = buildSseListeners('user-1', false, eventBus, write)
|
||||||
eventBus.emit('order:deliveryFeeAdjusted', { orderId: 'o1', userId: 'user-1', totalCents: 50000 })
|
eventBus.emit('order:deliveryFeeAdjusted', { orderId: 'o1', userId: 'user-1', totalCents: 50000 })
|
||||||
@@ -112,6 +148,15 @@ describe('buildSseListeners', () => {
|
|||||||
cleanup()
|
cleanup()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('forwards order:deliveryFeeAdjusted to admin', () => {
|
||||||
|
const cleanup = buildSseListeners('admin-1', true, eventBus, write)
|
||||||
|
eventBus.emit('order:deliveryFeeAdjusted', { orderId: 'o1', userId: 'user-1', totalCents: 50000 })
|
||||||
|
expect(write).toHaveBeenCalledTimes(1)
|
||||||
|
expect(write.mock.calls[0][0]).toContain('event: order:updated')
|
||||||
|
expect(write.mock.calls[0][0]).toContain('"orderId":"o1"')
|
||||||
|
cleanup()
|
||||||
|
})
|
||||||
|
|
||||||
it('forwards order:created to admin', () => {
|
it('forwards order:created to admin', () => {
|
||||||
const cleanup = buildSseListeners('admin-1', true, eventBus, write)
|
const cleanup = buildSseListeners('admin-1', true, eventBus, write)
|
||||||
eventBus.emit('order:created', { orderId: 'o1', userId: 'user-1', totalCents: 50000 })
|
eventBus.emit('order:created', { orderId: 'o1', userId: 'user-1', totalCents: 50000 })
|
||||||
|
|||||||
@@ -0,0 +1,88 @@
|
|||||||
|
import { randomUUID } from 'node:crypto'
|
||||||
|
import jwt from '@fastify/jwt'
|
||||||
|
import Fastify from 'fastify'
|
||||||
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||||
|
import { NOTIFICATION_EVENTS } from '../../../../shared/constants/notification-events.js'
|
||||||
|
import { prisma } from '../../lib/prisma.js'
|
||||||
|
import { registerUserOrderRoutes } from '../user-orders.js'
|
||||||
|
|
||||||
|
const JWT_SECRET = 'test-secret'
|
||||||
|
|
||||||
|
let app
|
||||||
|
let testUser
|
||||||
|
let testUserEmail
|
||||||
|
|
||||||
|
async function buildApp() {
|
||||||
|
const fastify = Fastify({ logger: false })
|
||||||
|
await fastify.register(jwt, { secret: JWT_SECRET })
|
||||||
|
fastify.decorate('authenticate', async function (request, reply) {
|
||||||
|
try {
|
||||||
|
await request.jwtVerify()
|
||||||
|
} catch {
|
||||||
|
return reply.code(401).send({ error: 'Unauthorized' })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
fastify.decorate('eventBus', { emit: vi.fn() })
|
||||||
|
await registerUserOrderRoutes(fastify)
|
||||||
|
await fastify.ready()
|
||||||
|
return fastify
|
||||||
|
}
|
||||||
|
|
||||||
|
async function signToken(user) {
|
||||||
|
return app.jwt.sign({ sub: user.id, email: user.email })
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createOrder(data = {}) {
|
||||||
|
return prisma.order.create({
|
||||||
|
data: {
|
||||||
|
userId: testUser.id,
|
||||||
|
status: 'SHIPPED',
|
||||||
|
deliveryType: 'delivery',
|
||||||
|
deliveryFeeLocked: true,
|
||||||
|
paymentMethod: 'online',
|
||||||
|
itemsSubtotalCents: 10000,
|
||||||
|
deliveryFeeCents: 50000,
|
||||||
|
totalCents: 60000,
|
||||||
|
currency: 'RUB',
|
||||||
|
...data,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('user order routes', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
testUserEmail = `user-orders-${randomUUID()}@example.com`
|
||||||
|
testUser = await prisma.user.create({ data: { email: testUserEmail } })
|
||||||
|
app = await buildApp()
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await app?.close()
|
||||||
|
if (testUser?.id) {
|
||||||
|
await prisma.order.deleteMany({ where: { userId: testUser.id } })
|
||||||
|
await prisma.user.deleteMany({ where: { id: testUser.id } })
|
||||||
|
} else if (testUserEmail) {
|
||||||
|
await prisma.user.deleteMany({ where: { email: testUserEmail } })
|
||||||
|
}
|
||||||
|
vi.clearAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('emits order status event when user confirms receiving an order', async () => {
|
||||||
|
const order = await createOrder()
|
||||||
|
const token = await signToken(testUser)
|
||||||
|
|
||||||
|
const res = await app.inject({
|
||||||
|
method: 'POST',
|
||||||
|
url: `/api/me/orders/${order.id}/confirm-received`,
|
||||||
|
headers: { authorization: `Bearer ${token}` },
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(res.statusCode).toBe(200)
|
||||||
|
expect(app.eventBus.emit).toHaveBeenCalledWith(NOTIFICATION_EVENTS.ORDER_STATUS_CHANGED, {
|
||||||
|
orderId: order.id,
|
||||||
|
userId: testUser.id,
|
||||||
|
oldStatus: 'SHIPPED',
|
||||||
|
newStatus: 'DONE',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -10,7 +10,13 @@ const {
|
|||||||
} = NOTIFICATION_EVENTS
|
} = NOTIFICATION_EVENTS
|
||||||
|
|
||||||
export function isAdminUser(user) {
|
export function isAdminUser(user) {
|
||||||
return !!(process.env.ADMIN_EMAIL && user?.email === process.env.ADMIN_EMAIL)
|
const adminEmail = String(process.env.ADMIN_EMAIL || '')
|
||||||
|
.trim()
|
||||||
|
.toLowerCase()
|
||||||
|
const userEmail = String(user?.email || '')
|
||||||
|
.trim()
|
||||||
|
.toLowerCase()
|
||||||
|
return !!(adminEmail && userEmail === adminEmail)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function formatSSE(event, data) {
|
export function formatSSE(event, data) {
|
||||||
@@ -53,21 +59,21 @@ export function buildSseListeners(userId, admin, eventBus, write) {
|
|||||||
|
|
||||||
on(
|
on(
|
||||||
ORDER_STATUS_CHANGED,
|
ORDER_STATUS_CHANGED,
|
||||||
(p) => p.userId === userId,
|
(p) => admin || p.userId === userId,
|
||||||
'order:statusChanged',
|
'order:statusChanged',
|
||||||
(p) => ({ orderId: p.orderId, newStatus: p.newStatus }),
|
(p) => ({ orderId: p.orderId, newStatus: p.newStatus }),
|
||||||
)
|
)
|
||||||
|
|
||||||
on(
|
on(
|
||||||
PAYMENT_STATUS_CHANGED,
|
PAYMENT_STATUS_CHANGED,
|
||||||
(p) => p.userId === userId,
|
(p) => admin || p.userId === userId,
|
||||||
'order:statusChanged',
|
'order:statusChanged',
|
||||||
(p) => ({ orderId: p.orderId }),
|
(p) => ({ orderId: p.orderId }),
|
||||||
)
|
)
|
||||||
|
|
||||||
on(
|
on(
|
||||||
DELIVERY_FEE_ADJUSTED,
|
DELIVERY_FEE_ADJUSTED,
|
||||||
(p) => p.userId === userId,
|
(p) => admin || p.userId === userId,
|
||||||
'order:updated',
|
'order:updated',
|
||||||
(p) => ({ orderId: p.orderId }),
|
(p) => ({ orderId: p.orderId }),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -267,6 +267,12 @@ export async function registerUserOrderRoutes(fastify) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await prisma.order.update({ where: { id }, data: { status: 'DONE' } })
|
await prisma.order.update({ where: { id }, data: { status: 'DONE' } })
|
||||||
|
request.server.eventBus.emit(NOTIFICATION_EVENTS.ORDER_STATUS_CHANGED, {
|
||||||
|
orderId: order.id,
|
||||||
|
userId: order.userId,
|
||||||
|
oldStatus: order.status,
|
||||||
|
newStatus: 'DONE',
|
||||||
|
})
|
||||||
return { ok: true, status: 'DONE' }
|
return { ok: true, status: 'DONE' }
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user