import { describe, it, expect, vi, beforeEach } from 'vitest' const mockGetTariffs = vi.fn() const mockPayloadFind = vi.fn() const mockGetPayload = vi.fn(() => ({ find: mockPayloadFind })) vi.mock('@/lib/ezy', () => ({ getTariffs: mockGetTariffs })) vi.mock('payload', () => ({ getPayload: mockGetPayload })) vi.mock('@payload-config', () => ({ default: {} })) const ezyTariffs = [ { id: 1, name: 'Дорослий', price: 250 }, { id: 2, name: 'Дитячий', price: 150 }, ] const dbTariffs = [ { ezy_id: 1, display_name: 'Дорослий (UA)', category_tag: 'adult', sort: 0 }, { ezy_id: 2, display_name: 'Дитячий (UA)', category_tag: 'child', sort: 1 }, ] let GET: () => Promise beforeEach(async () => { vi.resetModules() mockGetTariffs.mockReset() mockPayloadFind.mockReset() const mod = await import('@/app/api/tickets/tariffs/route') GET = mod.GET }) describe('GET /api/tickets/tariffs', () => { it('returns merged tariffs sorted by sort field', async () => { mockGetTariffs.mockResolvedValueOnce(ezyTariffs) mockPayloadFind.mockResolvedValueOnce({ docs: dbTariffs }) const res = await GET() expect(res.status).toBe(200) const { tariffs } = await res.json() expect(tariffs).toHaveLength(2) expect(tariffs[0].id).toBe(1) expect(tariffs[0].name).toBe('Дорослий (UA)') expect(tariffs[0].price).toBe(250) }) it('filters out ezy tariffs with no matching DB record', async () => { mockGetTariffs.mockResolvedValueOnce([...ezyTariffs, { id: 99, name: 'Unknown', price: 0 }]) mockPayloadFind.mockResolvedValueOnce({ docs: dbTariffs }) const res = await GET() const { tariffs } = await res.json() expect(tariffs).toHaveLength(2) expect(tariffs.map((t: { id: number }) => t.id)).not.toContain(99) }) it('uses db display_name over ezy name', async () => { mockGetTariffs.mockResolvedValueOnce([{ id: 1, name: 'EZY Name', price: 300 }]) mockPayloadFind.mockResolvedValueOnce({ docs: [{ ezy_id: 1, display_name: 'DB Name', sort: 0 }], }) const res = await GET() const { tariffs } = await res.json() expect(tariffs[0].name).toBe('DB Name') }) it('falls back to DB tariffs when ezy throws, with warning', async () => { mockGetTariffs.mockRejectedValueOnce(new Error('ezy down')) mockPayloadFind.mockResolvedValueOnce({ docs: [ { ezy_id: 1, display_name: 'Adult', last_synced_price: 250, last_synced_name: 'Adult', sort: 0, }, ], }) const res = await GET() expect(res.status).toBe(200) const json = await res.json() expect(json.warning).toMatch(/outdated/i) expect(json.tariffs[0].stale).toBe(true) }) it('returns 503 when ezy fails and DB is empty', async () => { mockGetTariffs.mockRejectedValueOnce(new Error('ezy down')) mockPayloadFind.mockResolvedValueOnce({ docs: [] }) const res = await GET() expect(res.status).toBe(503) }) it('returns 503 when both ezy and DB fail', async () => { mockGetTariffs.mockRejectedValueOnce(new Error('ezy down')) mockPayloadFind.mockRejectedValueOnce(new Error('DB down')) const res = await GET() expect(res.status).toBe(503) const json = await res.json() expect(json.error).toBeDefined() }) it('treats null sort as 0 for ordering purposes', async () => { mockGetTariffs.mockResolvedValueOnce([ { id: 1, name: 'A', price: 100 }, { id: 2, name: 'B', price: 200 }, ]) mockPayloadFind.mockResolvedValueOnce({ docs: [ { ezy_id: 2, display_name: 'B', sort: null }, { ezy_id: 1, display_name: 'A', sort: 1 }, ], }) const res = await GET() const { tariffs } = await res.json() expect(tariffs[0].id).toBe(2) // null → 0, sorts before 1 expect(tariffs[1].id).toBe(1) }) it('falls back to ezy name when display_name is null', async () => { mockGetTariffs.mockResolvedValueOnce([{ id: 1, name: 'Ezy Name', price: 100 }]) mockPayloadFind.mockResolvedValueOnce({ docs: [{ ezy_id: 1, display_name: null, sort: 0 }], }) const res = await GET() const { tariffs } = await res.json() expect(tariffs[0].name).toBe('Ezy Name') }) it('includes categoryTag, description, image, and icon in merged response', async () => { mockGetTariffs.mockResolvedValueOnce([{ id: 1, name: 'Adult', price: 250 }]) mockPayloadFind.mockResolvedValueOnce({ docs: [ { ezy_id: 1, display_name: 'Adult UA', category_tag: 'adult', description: 'Full-day pass', image: { url: '/img/adult.jpg' }, icon: '🎿', sort: 0, }, ], }) const res = await GET() const { tariffs } = await res.json() expect(tariffs[0].categoryTag).toBe('adult') expect(tariffs[0].description).toBe('Full-day pass') expect(tariffs[0].image).toEqual({ url: '/img/adult.jpg' }) expect(tariffs[0].icon).toBe('🎿') }) it('fallback response uses last_synced_price and includes stale:true', async () => { mockGetTariffs.mockRejectedValueOnce(new Error('ezy down')) mockPayloadFind.mockResolvedValueOnce({ docs: [ { ezy_id: 3, display_name: 'Weekend', last_synced_name: 'Weekend pass', last_synced_price: 199, category_tag: 'weekend', description: null, image: null, icon: null, sort: 0, }, ], }) const res = await GET() const { tariffs } = await res.json() expect(tariffs[0].price).toBe(199) expect(tariffs[0].stale).toBe(true) expect(tariffs[0].id).toBe(3) }) it('returns tariffs sorted in ascending sort order', async () => { mockGetTariffs.mockResolvedValueOnce([ { id: 2, name: 'B', price: 100 }, { id: 1, name: 'A', price: 200 }, ]) mockPayloadFind.mockResolvedValueOnce({ docs: [ { ezy_id: 1, display_name: 'A', sort: 0 }, { ezy_id: 2, display_name: 'B', sort: 5 }, ], }) const res = await GET() const { tariffs } = await res.json() expect(tariffs[0].id).toBe(1) expect(tariffs[1].id).toBe(2) }) })