Files
portal/app/tests/Frontend/dealsApiMapper.spec.ts
T
Дмитрий 339e8ea53b phase2(deals-list-api): GET /api/deals + замена MOCK_DEALS на fetch с fallback
Закрыт TODO (c) из v1.50: backend-эндпоинт для списка сделок и его
интеграция в DealsView/KanbanView вместо статичного MOCK_DEALS.

Backend (DealController::index):
- Query-params: tenant_id (required, 422/404), status_in[] (whereIn),
  project_id, manager_id, search (ILIKE по phone+contact_name),
  limit clamp [1..500] default 100, offset default 0.
- ORDER BY received_at DESC, id DESC. Eager-load project + manager.
- RLS-обёртка SET LOCAL app.current_tenant_id + defense-in-depth
  where(tenant_id) — на тестах через postgres superuser RLS обходится
  BYPASSRLS, app-фильтр гарантирует изоляцию.
- Ответ: {deals: [...], total, limit, offset}; manager_name/initials
  форматируются через ManagerController::formatName/formatInitials.

Pest +12 (DealIndexTest):
- 422/404, пустой список, relations (project_name+manager_name+initials),
  RLS-изоляция, ORDER BY, status_in[], project_id, manager_id, search
  ILIKE, limit+offset, manager=null edge case.

Frontend:
- api/deals.ts::listDeals — типизированный helper c ApiDeal/ListDeals*.
- composables/dealsApiMapper.ts::mapApiDeal — converter ApiDeal→MockDeal:
  contact_name fallback на phone, manager.name='Не назначен' /
  initials='—' при null, project='—' при null, cost=0,
  receivedMinutesAgo=max(0, …) от clock-skew.
- DealsView/KanbanView: onMounted(loadDeals) async-вызывает listDeals
  если auth.user.tenant_id, на success replace через splice, на fail
  fetchError=true + v-alert warning, MOCK_DEALS как fallback.

Vitest +14:
- dealsApiMapper.spec.ts (8): 1:1, fallback'и, edge cases.
- DealsListIntegration.spec.ts (6): без tenant_id — НЕ вызывает API,
  с tenant_id — replace state, reject → fetchError + alert + fallback;
  для DealsView и KanbanView.

PHPStan baseline регенерирован. cspell-glossary +ILIKE +DTO.

Регресс:
- Lint+type-check+format passed.
- Vitest 261/261 за 19.62 сек (+14 от 247).
- Vite build 989 ms.
- Pint + PHPStan passed.
- Pest 186/186 за 22 сек (+12 от 174, 742 assertions).

Реестр v1.59→v1.60 / CLAUDE.md v1.50→v1.51.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 07:34:39 +03:00

69 lines
2.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { describe, expect, it } from 'vitest';
import { mapApiDeal } from '../../resources/js/composables/dealsApiMapper';
import type { ApiDeal } from '../../resources/js/api/deals';
describe('mapApiDeal', () => {
const baseApi: ApiDeal = {
id: 42,
tenant_id: 1,
project_id: 7,
project_name: 'Окна Москва',
phone: '+7 (999) 111-11-11',
contact_name: 'Анна С.',
status: 'new',
manager_id: 3,
manager_name: 'Иван П.',
manager_initials: 'ИП',
received_at: '2026-05-09T10:00:00Z',
};
it('маппит обязательные поля 1:1', () => {
const m = mapApiDeal(baseApi, new Date('2026-05-09T10:30:00Z'));
expect(m.id).toBe(42);
expect(m.phone).toBe('+7 (999) 111-11-11');
expect(m.statusSlug).toBe('new');
expect(m.project).toBe('Окна Москва');
expect(m.manager.name).toBe('Иван П.');
expect(m.manager.initials).toBe('ИП');
});
it('name берёт contact_name; fallback на phone когда contact_name = null', () => {
const m1 = mapApiDeal(baseApi);
expect(m1.name).toBe('Анна С.');
const m2 = mapApiDeal({ ...baseApi, contact_name: null });
expect(m2.name).toBe('+7 (999) 111-11-11');
});
it('manager.name = «Не назначен» / initials = «—» если manager null', () => {
const m = mapApiDeal({ ...baseApi, manager_id: null, manager_name: null, manager_initials: null });
expect(m.manager.name).toBe('Не назначен');
expect(m.manager.initials).toBe('—');
});
it('project = «—» если project_name null', () => {
const m = mapApiDeal({ ...baseApi, project_name: null });
expect(m.project).toBe('—');
});
it("cost всегда 0 (отдельного endpoint'а пока нет)", () => {
const m = mapApiDeal(baseApi);
expect(m.cost).toBe(0);
});
it('receivedMinutesAgo вычисляется из received_at', () => {
const m = mapApiDeal(baseApi, new Date('2026-05-09T10:30:00Z'));
expect(m.receivedMinutesAgo).toBe(30);
});
it('receivedMinutesAgo не уходит в отрицательные числа при future timestamp', () => {
const m = mapApiDeal(baseApi, new Date('2026-05-09T09:00:00Z'));
expect(m.receivedMinutesAgo).toBe(0);
});
it('received_at = null → 0 минут (используем "сейчас")', () => {
const m = mapApiDeal({ ...baseApi, received_at: null }, new Date('2026-05-09T10:30:00Z'));
expect(m.receivedMinutesAgo).toBe(0);
});
});