Admin BFF — BR 3.5

Контракты

  • 4 admin endpoint’а Paykeeper Adapter — см. API раздел «POST /internal/paykeeper/accounts/{id}/employees/*»
  • Существующий BFF для PK уже есть (BR 3.3) — добавляем 4 прокси-роута в существующий router

Что делаем

Прокси-роуты

  • bff/src/routes/admin/paykeeper.ts — добавить 4 роута. Используем существующий serviceFetcher (X-Service-Token + JWT forwarding).
// Все роуты требуют JWT с permissions integrations.manage + employees.edit (preview/import)
// или integrations.read (imports list/details). BFF только проксирует — backend сам проверит.
 
router.post('/api/v1/admin/paykeeper/accounts/:id/employees/preview', async (req, res) => {
  // POST → POST /internal/paykeeper/accounts/{id}/employees/preview в Adapter
});
 
router.post('/api/v1/admin/paykeeper/accounts/:id/employees/import', async (req, res) => {
  // body — { decisions: [...] }
  // timeout 65 секунд (чуть больше backend'а)
});
 
router.get('/api/v1/admin/paykeeper/accounts/:id/employees/imports', async (req, res) => {
  // query: limit, since
});
 
router.get('/api/v1/admin/paykeeper/accounts/:id/employees/imports/:run_id', async (req, res) => {
  // run details + errors_json
});
  • Использовать существующий middleware для прокси (createAdapterProxy или аналог) — не дублировать auth логику.

Shared TypeScript types

  • shared/types/paykeeper-users.ts:
export type UserPreviewItem = {
  pk_user_id: string;
  pk_login: string;
  pk_email: string | null;
  pk_fio: string | null;
  pk_admin: boolean;
  pk_invoices_only: boolean;
  match_status: 'new' | 'matched_email' | 'already_linked';
  matched_employee: {
    id: string;
    email: string;
    first_name: string;
    last_name: string;
  } | null;
  linked_employee_id: string | null;
};
 
export type ImportAction =
  | 'create_new'
  | 'create_with_alt_email'
  | 'link_existing'
  | 'update_existing'
  | 'skip';
 
export type EmployeeImportData = {
  first_name: string;
  last_name: string;
  email: string;
  password: string | null;
  generate_password: boolean;
  phone: string | null;
  pin: string | null;
  is_courier: boolean;
  roles: { role_id: string; store_ids: string[] }[];
};
 
export type ImportDecision = {
  pk_user_id: string;
  pk_login: string;
  action: ImportAction;
  matched_employee_id: string | null;
  employee_data: EmployeeImportData | null;
};
 
export type ImportRunSummary = {
  id: string;
  trigger: 'manual';
  initiated_by_user_id: string;
  initiated_by_user_name: string;
  started_at: string;
  finished_at: string | null;
  status: 'running' | 'success' | 'partial' | 'failed';
  users_total: number;
  users_created: number;
  users_linked: number;
  users_updated: number;
  users_skipped: number;
  users_errored: number;
};
 
export type ImportRunDetails = ImportRunSummary & {
  account_id: string;
  last_error: string | null;
  errors_json: ImportError[];
};
 
export type ImportError = {
  pk_user_id: string;
  pk_login: string;
  action: ImportAction;
  message: string;
};

API клиент в web

  • web/src/api/paykeeper-users.ts — расширение существующего paykeeper.ts либо новый файл:
export const previewUserImport = (accountId: string) =>
  apiPost<UserPreviewItem[]>(`/api/v1/admin/paykeeper/accounts/${accountId}/employees/preview`);
 
export const importUsers = (accountId: string, decisions: ImportDecision[]) =>
  apiPost<ImportRunSummary>(`/api/v1/admin/paykeeper/accounts/${accountId}/employees/import`, { decisions });
 
export const listUserImports = (accountId: string, params?: { limit?: number; since?: string }) =>
  apiGet<ImportRunSummary[]>(`/api/v1/admin/paykeeper/accounts/${accountId}/employees/imports`, { params });
 
export const getUserImportDetails = (accountId: string, runId: string) =>
  apiGet<ImportRunDetails>(`/api/v1/admin/paykeeper/accounts/${accountId}/employees/imports/${runId}`);

Тесты

  • BFF unit-тесты на 4 роута (mock backend response, проверка форвардинга headers/body/status).

Не делаем

  • ❌ Кеш на BFF — нет смысла (preview всегда fresh, imports пагинируются на бэке)
  • ❌ Бизнес-логика — BFF только проксирует
  • ❌ Изменения существующих paykeeper/* роутов — BR 3.3/3.4 не трогаем

Verification

  1. curl POST /api/v1/admin/paykeeper/accounts/{id}/employees/preview с валидным JWT → должен вернуть массив из koala-test
  2. Без JWT → 401 (BFF middleware)
  3. С JWT без integrations.manage → 403 (от backend)

Ссылки