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