Масова відправка документів на підписання з трекінгом та нагадуваннями
При злитті двох компаній потрібно розіслати 5000 трудових договорів за 48 годин. Ручна відправка потребує 10 співробітників на тиждень — з помилками, втраченими листами та нервотрепкою. Ми автоматизуємо цей процес: завантаження CSV, генерація персоналізованих PDF, відправка через чергу завдань, відстеження статусів та автоматичні нагадування. Нижче — архітектура, яка витримає навантаження 10K+ документів без падіння.
Які проблеми вирішуємо?
Генерація тисяч документів без зависання сервера. Синхронна генерація вб'є сервер — використовуємо чергу завдань на BullMQ. Кожен документ генерується окремим воркером, паралельність до 10. Навіть при піку в 5000 документів система залишається чуйною.
Rate limiting email-провайдерів. Resend дає 100 листів на секунду, Postmark — стільки ж. Для batch в 10K листів ми впроваджуємо throttling в черзі: відправка йде зі швидкістю 100–200 листів на хвилину, щоб не потрапити в блокування. Таймаути та повторні спроби — обов'язкові.
Відсутність підписання у частини отримувачів. Автоматичні нагадування через 2 та 4 дні. Налаштовувана кількість (до 3). Дашборд показує, хто не відкрив лист, і дозволяє надіслати нагадування вручну.
Як ми це робимо?
Типовий стек: PostgreSQL + Redis + BullMQ + Node.js (NestJS) + Puppeteer для PDF. Воркери розгорнуті в Docker на окремому інстансі. Код зберігається в GitLab CI, деплой на AWS ECS.
Кейс: для маркетплейсу впровадили масову відправку актів виконаних робіт. Щомісяця 15 000 актів. Раніше бухгалтерія витрачала 3 дні на розсилку через Outlook. Після впровадження — 30 хвилин. Помилки скоротилися з 5% до 0.2%.
Процес роботи
- Аналітика. Збираємо вимоги: обсяг, шаблони документів, інтеграції (CRM, бухгалтерія).
- Проектування. Схема БД, черга завдань, API, сторінка підписання.
- Реалізація. Розробка модулів: завантаження CSV, генерація PDF, відправка, дашборд.
- Тестування. Навантажувальне тестування з 10K документів, сценарії помилок.
- Деплой. На сервер клієнта або хмару. Налаштування CI/CD, моніторинг (Sentinel, Grafana).
Чому черга завдань краща за синхронну генерацію?
Синхронна генерація 5000 PDF викличе тайм-аут сервера. Черга BullMQ з воркерами (паралельність 10) обробляє документи поступово, не блокуючи запити. Redis зберігає прогрес. Якщо воркер впав — завдання автоматично перезапускається. Це дає надійність та масштабованість.
Як працює черга в коді?
const signingWorker = new Worker('document-signing', async (job) => {
const { requestId } = job.data;
const request = await db.signingRequests.findByPk(requestId);
try {
await db.signingRequests.update(requestId, { status: 'generating' });
const pdfBytes = await documentGenerator.generate(
request.template, request.templateData
);
const document = await documentStorage.store(pdfBytes, {
batchId: request.batchId,
requestId: request.id,
});
const signingUrl = `${process.env.APP_URL}/sign/${request.signingToken}`;
await emailService.send({
to: request.recipientEmail,
subject: 'Документ ожидает вашей подписи',
template: 'signing-invitation',
data: {
recipientName: request.recipientName,
documentName: request.template.name,
signingUrl,
expiresAt: request.expiresAt,
},
});
await db.signingRequests.update(requestId, {
status: 'sent',
documentId: document.id,
sentAt: new Date(),
});
await db.signingBatches.increment(request.batchId, 'sent_count');
} catch (error) {
await db.signingRequests.update(requestId, {
status: 'failed',
errorMessage: error.message,
});
await db.signingBatches.increment(request.batchId, 'failed_count');
}
}, {
concurrency: 10,
connection: redisConnection,
});
Обмеження та надійність
Email-провайдери блокують при перевищенні лімітів. Ми налаштовуємо чергу з паузою між відправками. Використовуємо виділені сервери відправки (SMTP-ретранслятори) та відстежуємо статуси листів (bounce, spam). Гарантуємо доставку 99%.
Автоматичні нагадування
Якщо отримувач не підписав, автоматика надсилає нагадування на 2-й та 4-й день. На 7-й день — позначка "прострочено" та сповіщення менеджеру. У дашборді можна надіслати повторний лист з новим посиланням. Налаштовуваний ліміт нагадувань — від 0 до 5.
async function sendSigningReminders() {
const pending = await db.signingRequests.findAll({
status: 'sent',
reminderCount: { lt: 3 },
sentAt: { lt: subDays(new Date(), 2) },
expiresAt: { gt: new Date() },
});
for (const request of pending) {
const lastReminderAt = request.lastReminderAt || request.sentAt;
const daysSinceLastReminder = differenceInDays(new Date(), lastReminderAt);
if (daysSinceLastReminder >= 2) {
await emailService.sendReminder(request);
await db.signingRequests.update(request.id, {
reminderCount: request.reminderCount + 1,
lastReminderAt: new Date(),
});
}
}
}
Порівняння ручної та автоматичної розсилки
| Параметр | Ручна (10 співробітників) | Автоматична (наше рішення) |
|---|---|---|
| Час на 5000 документів | 40 людино-годин | 2 години машинного часу |
| Помилки (опечатки, втрата) | 5–8% | <0.5% |
| Вартість за одну розсилку | значні витрати | економія |
| Трекінг статусів | Excel | Дашборд в реальному часі |
Також додаємо порівняння за швидкістю: автоматична розсилка в 20 разів швидша за ручну (40 годин проти 2).
Модель даних
CREATE TABLE signing_batches (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name VARCHAR(500),
template_id UUID REFERENCES document_templates(id),
initiated_by UUID REFERENCES users(id),
total_count INT NOT NULL,
sent_count INT DEFAULT 0,
signed_count INT DEFAULT 0,
failed_count INT DEFAULT 0,
status VARCHAR(50) DEFAULT 'pending',
-- pending → processing → completed / partially_failed
deadline_at TIMESTAMPTZ,
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE TABLE signing_requests (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
batch_id UUID REFERENCES signing_batches(id),
recipient_email VARCHAR(500) NOT NULL,
recipient_name VARCHAR(500),
recipient_phone VARCHAR(50),
template_data JSONB NOT NULL,
document_id UUID REFERENCES documents(id),
signing_token UUID UNIQUE DEFAULT gen_random_uuid(),
status VARCHAR(50) DEFAULT 'pending',
sent_at TIMESTAMPTZ,
opened_at TIMESTAMPTZ,
signed_at TIMESTAMPTZ,
reminder_count INT DEFAULT 0,
expires_at TIMESTAMPTZ,
error_message TEXT
);
Завантаження та валідація CSV
async function processBatchUpload(file: Express.Multer.File, templateId: string) {
const records = await parseCSV(file.buffer, { headers: true });
const template = await db.documentTemplates.findByPk(templateId);
const requiredFields = extractTemplateVariables(template.content);
const errors: ValidationError[] = [];
const validRows: RecipientRow[] = [];
records.forEach((row, index) => {
const rowErrors = [];
if (!row.email || !isValidEmail(row.email)) {
rowErrors.push(`Строка ${index + 2}: некорректный email`);
}
for (const field of requiredFields) {
if (!row[field]) {
rowErrors.push(`Строка ${index + 2}: отсутствует поле "${field}"`);
}
}
if (rowErrors.length > 0) {
errors.push(...rowErrors);
} else {
validRows.push(row);
}
});
return { valid: validRows, errors, totalRows: records.length };
}
Сторінка підписання за токеном
Отримувач переходить за посиланням https://app.example.com/sign/{token} — авторизація не потрібна, доступ тільки за токеном:
app.get('/sign/:token', async (req, res) => {
const request = await db.signingRequests.findOne({
signingToken: req.params.token,
status: { not: ['expired', 'signed', 'declined'] },
});
if (!request) return res.redirect('/sign/invalid');
if (request.expiresAt < new Date()) {
await db.signingRequests.update(request.id, { status: 'expired' });
return res.redirect('/sign/expired');
}
if (!request.openedAt) {
await db.signingRequests.update(request.id, { openedAt: new Date() });
}
res.render('signing-page', { request, document: request.document });
});
Дашборд моніторингу batch
Прогрес-бар: відправлено/підписано/не відкрито/прострочено. Таблиця з фільтрами за статусом. Експорт у CSV. Кнопка «Надіслати нагадування» для вибраних отримувачів.
Орієнтовні строки
| Етап | Строки |
|---|---|
| Завантаження CSV, валідація, черга + генерація PDF + відправка | 7–10 днів |
| Сторінка підписання за токеном, нагадування, дашборд | 5–7 днів |
| Інтеграція з CRM / імпорт наявних шаблонів | 3–5 днів додатково |
Що входить в роботу?
Детальний список
- Вихідний код на GitHub/GitLab (закритий репозиторій)
- Інструкція з розгортання (Docker + docker-compose)
- Документація по API (OpenAPI/Swagger)
- Міграції БД
- Навантажувальне тестування (звіт)
- Гарантія 1 місяць на баги
Зв'яжіться з нами для оцінки вашого проєкту. Отримайте консультацію з автоматизації підписання документів.







