diff --git a/lib/utils/payday.test.ts b/lib/utils/payday.test.ts new file mode 100644 index 000000000..beaecd0eb --- /dev/null +++ b/lib/utils/payday.test.ts @@ -0,0 +1,355 @@ +import { getPayday, countDaysBeforePayday, countDaysAfterPayday, countDaysAfterBlock } from './payday'; +import { WorkspaceDBScheme } from '@hawk.so/types'; +import { ObjectId } from 'mongodb'; + +/** + * Mock the Date constructor to allow controlling "now" + */ +let mockedNow: number | null = null; + +const setMockedNow = (date: Date): void => { + mockedNow = date.getTime(); +}; + +const resetMockedNow = (): void => { + mockedNow = null; +}; + +// Override Date constructor +const RealDate = Date; +global.Date = class extends RealDate { + /** + * Constructor for mocked Date class + * @param args - arguments passed to Date constructor + */ + constructor(...args: unknown[]) { + if (args.length === 0 && mockedNow !== null) { + super(mockedNow); + } else { + super(...(args as [])); + } + } + + public static now(): number { + return mockedNow !== null ? mockedNow : RealDate.now(); + } +} as DateConstructor; + +describe('Payday utility functions', () => { + afterEach(() => { + resetMockedNow(); + }); + + describe('getPayday', () => { + it('should return paidUntil date when provided', () => { + const lastChargeDate = new Date('2025-11-01'); + const paidUntil = new Date('2025-12-15'); + + const result = getPayday(lastChargeDate, paidUntil); + + expect(result).toEqual(paidUntil); + }); + + it('should calculate payday as one month after lastChargeDate when paidUntil is not provided', () => { + const lastChargeDate = new Date('2025-11-01'); + + const result = getPayday(lastChargeDate); + + expect(result.getFullYear()).toBe(2025); + expect(result.getMonth()).toBe(11); // December (0-indexed) + expect(result.getDate()).toBe(1); + }); + + it('should handle year transition correctly', () => { + const lastChargeDate = new Date('2025-12-15'); + + const result = getPayday(lastChargeDate); + + expect(result.getFullYear()).toBe(2026); + expect(result.getMonth()).toBe(0); // January (0-indexed) + expect(result.getDate()).toBe(15); + }); + + it('should add one day when isDebug is true', () => { + const lastChargeDate = new Date('2025-12-01'); + + const result = getPayday(lastChargeDate, null, true); + + expect(result.getFullYear()).toBe(2025); + expect(result.getMonth()).toBe(11); // December (0-indexed) + expect(result.getDate()).toBe(2); + }); + + it('should prioritize paidUntil over debug mode', () => { + const lastChargeDate = new Date('2025-11-01'); + const paidUntil = new Date('2025-12-15'); + + const result = getPayday(lastChargeDate, paidUntil, true); + + expect(result).toEqual(paidUntil); + }); + + it('should handle end of month dates correctly', () => { + const lastChargeDate = new Date('2025-01-31'); + + const result = getPayday(lastChargeDate); + + // JavaScript will adjust to the last day of February + expect(result.getFullYear()).toBe(2025); + expect(result.getMonth()).toBe(2); // March (0-indexed) + expect(result.getDate()).toBe(3); // Adjusted from Feb 31 to Mar 3 + }); + }); + + describe('countDaysBeforePayday', () => { + it('should return positive days when payday is in the future', () => { + const now = new Date('2025-12-01'); + const lastChargeDate = new Date('2025-11-20'); + + setMockedNow(now); + + const result = countDaysBeforePayday(lastChargeDate); + + expect(result).toBe(19); // Dec 20 - Dec 1 = 19 days + }); + + it('should return 0 when payday is today', () => { + // Payday is calculated as one month after lastChargeDate, so Dec 20 12pm + const now = new Date('2025-12-20T12:00:00.000Z'); + const lastChargeDate = new Date('2025-11-20T12:00:00.000Z'); + + setMockedNow(now); + + const result = countDaysBeforePayday(lastChargeDate); + + expect(result).toBe(0); + }); + + it('should return negative days when payday has passed', () => { + const now = new Date('2025-12-25'); + const lastChargeDate = new Date('2025-11-20'); + + setMockedNow(now); + + const result = countDaysBeforePayday(lastChargeDate); + + expect(result).toBe(-5); // Dec 20 - Dec 25 = -5 days + }); + + it('should use paidUntil when provided', () => { + const now = new Date('2025-12-01'); + const lastChargeDate = new Date('2025-10-01'); + const paidUntil = new Date('2025-12-15'); + + setMockedNow(now); + + const result = countDaysBeforePayday(lastChargeDate, paidUntil); + + expect(result).toBe(14); // Dec 15 - Dec 1 = 14 days + }); + + it('should work correctly in debug mode', () => { + const now = new Date('2025-12-01T00:00:00Z'); + const lastChargeDate = new Date('2025-11-30T00:00:00Z'); + + setMockedNow(now); + + const result = countDaysBeforePayday(lastChargeDate, null, true); + + expect(result).toBe(0); // Next day is Dec 1, same as now + }); + + it('should handle cross-year payday correctly', () => { + const now = new Date('2025-12-20'); + const lastChargeDate = new Date('2025-12-15'); + + setMockedNow(now); + + const result = countDaysBeforePayday(lastChargeDate); + + expect(result).toBe(26); // Jan 15, 2026 - Dec 20, 2025 = 26 days + }); + }); + + describe('countDaysAfterPayday', () => { + it('should return 0 when payday is today', () => { + const now = new Date('2025-12-20T12:00:00Z'); + const lastChargeDate = new Date('2025-11-20T00:00:00Z'); + + setMockedNow(now); + + const result = countDaysAfterPayday(lastChargeDate); + + expect(result).toBe(0); + }); + + it('should return positive days when payday has passed', () => { + const now = new Date('2025-12-25'); + const lastChargeDate = new Date('2025-11-20'); + + setMockedNow(now); + + const result = countDaysAfterPayday(lastChargeDate); + + expect(result).toBe(5); // Dec 25 - Dec 20 = 5 days + }); + + it('should return negative days when payday is in the future', () => { + const now = new Date('2025-12-01'); + const lastChargeDate = new Date('2025-11-20'); + + setMockedNow(now); + + const result = countDaysAfterPayday(lastChargeDate); + + expect(result).toBe(-19); // Dec 1 - Dec 20 = -19 days + }); + + it('should use paidUntil when provided', () => { + const now = new Date('2025-12-20'); + const lastChargeDate = new Date('2025-10-01'); + const paidUntil = new Date('2025-12-15'); + + setMockedNow(now); + + const result = countDaysAfterPayday(lastChargeDate, paidUntil); + + expect(result).toBe(5); // Dec 20 - Dec 15 = 5 days + }); + + it('should work correctly in debug mode', () => { + const now = new Date('2025-12-03T00:00:00Z'); + const lastChargeDate = new Date('2025-12-01T00:00:00Z'); + + setMockedNow(now); + + const result = countDaysAfterPayday(lastChargeDate, null, true); + + expect(result).toBe(1); // Dec 3 - Dec 2 = 1 day + }); + + it('should be the inverse of countDaysBeforePayday', () => { + const now = new Date('2025-12-15'); + const lastChargeDate = new Date('2025-11-20'); + + setMockedNow(now); + + const daysBefore = countDaysBeforePayday(lastChargeDate); + const daysAfter = countDaysAfterPayday(lastChargeDate); + + expect(daysBefore).toBe(-daysAfter); + }); + }); + + describe('countDaysAfterBlock', () => { + it('should return undefined when blockedDate is not set', () => { + const workspace: WorkspaceDBScheme = { + _id: new ObjectId(), + name: 'Test Workspace', + inviteHash: 'test-hash', + tariffPlanId: new ObjectId(), + billingPeriodEventsCount: 0, + lastChargeDate: new Date(), + accountId: 'test-account', + balance: 0, + blockedDate: null, + }; + + const result = countDaysAfterBlock(workspace); + + expect(result).toBeUndefined(); + }); + + it('should return 0 when workspace was blocked today', () => { + const now = new Date('2025-12-18T12:00:00Z'); + const blockedDate = new Date('2025-12-18T00:00:00Z'); + + setMockedNow(now); + + const workspace: WorkspaceDBScheme = { + _id: new ObjectId(), + name: 'Test Workspace', + inviteHash: 'test-hash', + tariffPlanId: new ObjectId(), + billingPeriodEventsCount: 0, + lastChargeDate: new Date(), + accountId: 'test-account', + balance: 0, + blockedDate, + }; + + const result = countDaysAfterBlock(workspace); + + expect(result).toBe(0); + }); + + it('should return correct number of days after block', () => { + const now = new Date('2025-12-18'); + const blockedDate = new Date('2025-12-10'); + + setMockedNow(now); + + const workspace: WorkspaceDBScheme = { + _id: new ObjectId(), + name: 'Test Workspace', + inviteHash: 'test-hash', + tariffPlanId: new ObjectId(), + billingPeriodEventsCount: 0, + lastChargeDate: new Date(), + accountId: 'test-account', + balance: 0, + blockedDate, + }; + + const result = countDaysAfterBlock(workspace); + + expect(result).toBe(8); // Dec 18 - Dec 10 = 8 days + }); + + it('should handle cross-month blocks correctly', () => { + const now = new Date('2025-12-05'); + const blockedDate = new Date('2025-11-28'); + + setMockedNow(now); + + const workspace: WorkspaceDBScheme = { + _id: new ObjectId(), + name: 'Test Workspace', + inviteHash: 'test-hash', + tariffPlanId: new ObjectId(), + billingPeriodEventsCount: 0, + lastChargeDate: new Date(), + accountId: 'test-account', + balance: 0, + blockedDate, + }; + + const result = countDaysAfterBlock(workspace); + + expect(result).toBe(7); // Dec 5 - Nov 28 = 7 days + }); + + it('should handle cross-year blocks correctly', () => { + const now = new Date('2026-01-05'); + const blockedDate = new Date('2025-12-28'); + + setMockedNow(now); + + const workspace: WorkspaceDBScheme = { + _id: new ObjectId(), + name: 'Test Workspace', + inviteHash: 'test-hash', + tariffPlanId: new ObjectId(), + billingPeriodEventsCount: 0, + lastChargeDate: new Date(), + accountId: 'test-account', + balance: 0, + blockedDate, + }; + + const result = countDaysAfterBlock(workspace); + + expect(result).toBe(8); // Jan 5, 2026 - Dec 28, 2025 = 8 days + }); + }); +}); diff --git a/lib/utils/payday.ts b/lib/utils/payday.ts index ad2edda95..09bfaff04 100644 --- a/lib/utils/payday.ts +++ b/lib/utils/payday.ts @@ -7,23 +7,44 @@ import { HOURS_IN_DAY, MINUTES_IN_HOUR, SECONDS_IN_MINUTE, MS_IN_SEC } from './c const MILLISECONDS_IN_DAY = HOURS_IN_DAY * MINUTES_IN_HOUR * SECONDS_IN_MINUTE * MS_IN_SEC; /** - * Returns difference between now and payday in days + * Returns expected payday date * * Pay day is calculated by formula: paidUntil date or last charge date + 1 month * - * @param date - last charge date + * @param lastChargeDate - last charge date * @param paidUntil - paid until date * @param isDebug - flag for debug purposes */ -export function countDaysBeforePayday(date: Date, paidUntil: Date = null, isDebug = false): number { - const expectedPayDay = paidUntil ? new Date(paidUntil) : new Date(date); +export function getPayday(lastChargeDate: Date, paidUntil: Date = null, isDebug = false): Date { + let expectedPayDay: Date; - if (isDebug) { - expectedPayDay.setDate(date.getDate() + 1); - } else if (!paidUntil) { - expectedPayDay.setMonth(date.getMonth() + 1); + if (paidUntil) { + // If paidUntil is provided, use it as the payday + expectedPayDay = new Date(paidUntil); + } else { + // Otherwise calculate from lastChargeDate + expectedPayDay = new Date(lastChargeDate); + if (isDebug) { + expectedPayDay.setDate(lastChargeDate.getDate() + 1); + } else { + expectedPayDay.setMonth(lastChargeDate.getMonth() + 1); + } } + return expectedPayDay; +} + +/** + * Returns difference between now and payday in days + * + * Pay day is calculated by formula: paidUntil date or last charge date + 1 month + * + * @param lastChargeDate - last charge date + * @param paidUntil - paid until date + * @param isDebug - flag for debug purposes + */ +export function countDaysBeforePayday(lastChargeDate: Date, paidUntil: Date = null, isDebug = false): number { + const expectedPayDay = getPayday(lastChargeDate, paidUntil, isDebug); const now = new Date().getTime(); return Math.floor((expectedPayDay.getTime() - now) / MILLISECONDS_IN_DAY); @@ -34,19 +55,12 @@ export function countDaysBeforePayday(date: Date, paidUntil: Date = null, isDebu * * Pay day is calculated by formula: paidUntil date or last charge date + 1 month * - * @param date - last charge date + * @param lastChargeDate - last charge date * @param paidUntil - paid until date * @param isDebug - flag for debug purposes */ -export function countDaysAfterPayday(date: Date, paidUntil: Date = null, isDebug = false): number { - const expectedPayDay = paidUntil ? new Date(paidUntil) : new Date(date); - - if (isDebug) { - expectedPayDay.setDate(date.getDate() + 1); - } else if (!paidUntil) { - expectedPayDay.setMonth(date.getMonth() + 1); - } - +export function countDaysAfterPayday(lastChargeDate: Date, paidUntil: Date = null, isDebug = false): number { + const expectedPayDay = getPayday(lastChargeDate, paidUntil, isDebug); const now = new Date().getTime(); return Math.floor((now - expectedPayDay.getTime()) / MILLISECONDS_IN_DAY); diff --git a/workers/email/scripts/emailOverview.ts b/workers/email/scripts/emailOverview.ts index b49b2b910..ff36ed82a 100644 --- a/workers/email/scripts/emailOverview.ts +++ b/workers/email/scripts/emailOverview.ts @@ -149,6 +149,16 @@ class EmailTestServer { period: 10, reason: 'error on the payment server side', daysAfterPayday: countDaysAfterPayday(workspace.lastChargeDate, workspace.paidUntil), + daysAfterBlock: 5, + daysLeft: 3, + eventsCount: workspace.billingPeriodEventsCount, + eventsLimit: 100000, + tariffPlanId: '5f47f031ff71510040f433c1', + password: '1as2eadd321a3cDf', + plan: { + name: 'Корпоративный' + }, + workspaceName: workspace.name, }; try { diff --git a/workers/email/src/templates/components/backtrace.twig b/workers/email/src/templates/components/backtrace.twig index f1495646d..64a54dcaa 100644 --- a/workers/email/src/templates/components/backtrace.twig +++ b/workers/email/src/templates/components/backtrace.twig @@ -19,10 +19,7 @@ -
- {{ frame.content | escape }} -
+
{{ frame.content | escape }}
{% endfor %} diff --git a/workers/email/src/templates/components/button.twig b/workers/email/src/templates/components/button.twig index e4e40131b..4ff9f5aab 100644 --- a/workers/email/src/templates/components/button.twig +++ b/workers/email/src/templates/components/button.twig @@ -1,8 +1,8 @@
- {{ label }} + style="font-size: 14.4px; color: #dbe6ff;"> + {{ label }}
diff --git a/workers/email/src/templates/components/event-info.twig b/workers/email/src/templates/components/event-info.twig index 13d0572dd..9b8eee70f 100644 --- a/workers/email/src/templates/components/event-info.twig +++ b/workers/email/src/templates/components/event-info.twig @@ -3,7 +3,7 @@
- {{ event.newCount | abbrNumber }} new + {{ event.newCount | abbrNumber }} {{ pluralize_ru(event.newCount, ['новое', 'новых', 'новых']) }}
@@ -11,7 +11,7 @@ {% if event.event.totalCount is not empty %} - {{ event.event.totalCount | abbrNumber }} total + {{ event.event.totalCount | abbrNumber }} всего    @@ -19,7 +19,7 @@ {% if event.daysRepeated is not empty %} - {{ event.daysRepeated }} days repeating + {{ event.daysRepeated }} {{ pluralize_ru(event.daysRepeated, ['день', 'дня', 'дней']) }}    @@ -27,7 +27,7 @@ {% if event.usersAffected %} - {{ event.usersAffected }} users affected + {{ event.usersAffected }} {{ pluralize_ru(event.usersAffected, ['пользователь', 'пользователя', 'пользователей']) }} {% endif %} diff --git a/workers/email/src/templates/components/event.twig b/workers/email/src/templates/components/event.twig index 26932d3f1..f47567451 100644 --- a/workers/email/src/templates/components/event.twig +++ b/workers/email/src/templates/components/event.twig @@ -1,5 +1,5 @@ - + {{ event.payload.title | escape }} @@ -7,4 +7,4 @@ {% include './event-info.twig' with {event: { daysRepeated: daysRepeated, event: {totalCount: event.totalCount}, usersAffected: event.usersAffected}} %} - \ No newline at end of file + diff --git a/workers/email/src/templates/components/layout.twig b/workers/email/src/templates/components/layout.twig index 539967d8d..4d6e4f278 100644 --- a/workers/email/src/templates/components/layout.twig +++ b/workers/email/src/templates/components/layout.twig @@ -152,17 +152,7 @@ - Российский трекер ошибок - - - - - - - Made by - CodeX + Мониторинг ошибок diff --git a/workers/email/src/templates/emails/assignee/html.twig b/workers/email/src/templates/emails/assignee/html.twig index ead2fc8fa..c8c252929 100644 --- a/workers/email/src/templates/emails/assignee/html.twig +++ b/workers/email/src/templates/emails/assignee/html.twig @@ -5,6 +5,8 @@ {% endblock %} {% block content %} + {% set utmParams = 'utm_source=email&utm_medium=transactional&utm_campaign=assignee' %} + @@ -19,7 +21,7 @@ {% else %} {{ whoAssigned.email }} {% endif %} - assigned you to resolve the event + назначил вас ответственным за исправление ошибки @@ -27,8 +29,8 @@ {% include '../../components/event.twig' with daysRepeated, event %} - {% set url = host ~ '/project/' ~ project._id ~ '/event/' ~ event._id ~ '/overview' %} - {% include '../../components/button.twig' with {href: url, label: 'View event'} %} + {% set url = host ~ '/project/' ~ project._id ~ '/event/' ~ event._id ~ '/overview' ~ '?' ~ utmParams %} + {% include '../../components/button.twig' with {href: url, label: 'Смотреть событие'} %} @@ -39,7 +41,5 @@ {% endblock %} {% block unsubscribeText %} - You received this email because you are currently opted in to receive such alerts via your - personal notifications settings. You may adjust your preferences at any time by clicking - the link above. + Вы получили это письмо, потому что подписаны на подобные уведомления. Вы можете изменить настройки, перейдя по ссылке выше. {% endblock %} diff --git a/workers/email/src/templates/emails/assignee/subject.twig b/workers/email/src/templates/emails/assignee/subject.twig index acc159cb3..f73b21c36 100644 --- a/workers/email/src/templates/emails/assignee/subject.twig +++ b/workers/email/src/templates/emails/assignee/subject.twig @@ -1 +1 @@ -You're assigned: ({{ project.name | escape }}) {{ event.payload.title }} \ No newline at end of file +Вы были назначены ответственным за фикс «{{ event.payload.title }}» — {{ project.name | escape }} diff --git a/workers/email/src/templates/emails/assignee/text.twig b/workers/email/src/templates/emails/assignee/text.twig index 3e2fa3c48..97dfc0de1 100644 --- a/workers/email/src/templates/emails/assignee/text.twig +++ b/workers/email/src/templates/emails/assignee/text.twig @@ -1,18 +1,20 @@ -{% if whoAssigned.name %}{{ whoAssigned.name | escape }}{% else %}{{ whoAssigned.email }}{% endif %} assigned you to resolve the event +{% set utmParams = 'utm_source=email&utm_medium=transactional&utm_campaign=assignee' %} + +{% if whoAssigned.name %}{{ whoAssigned.name | escape }}{% else %}{{ whoAssigned.email }}{% endif %} назначил вас ответственным за исправление ошибки в проекте «{{ project.name | escape }}» {{ event.payload.title }} -{{ event.totalCount }} total -{{ daysRepeated }} {% if daysRepeated == 1 %}day{% else %}days{% endif %} repeating -{{ event.usersAffected }} {% if event.usersAffected == 1 %}user{% else %}users{% endif %} affected +{{ event.totalCount }} {{ pluralize_ru(event.totalCount, ['раз', 'раза', 'раз']) }} +{{ daysRepeated }} {{ pluralize_ru(daysRepeated, ['день', 'дня', 'дней']) }} повторяется +{{ event.usersAffected }} {{ pluralize_ru(event.usersAffected, ['пользователь', 'пользователя', 'пользователей']) }} затронуто -View event: {{ host }}/project/{{ project._id }}/event/{{ event._id }}/overview +Смотреть событие: {{ host }}/project/{{ project._id }}/event/{{ event._id }}/overview?{{ utmParams }} *** -You received this email because you are currently opted in to receive such alerts via your personal notifications settings. You may adjust your preferences at any time by clicking the link: {{ host }}/account/notifications +Вы получили это письмо, потому что подписаны на подобные уведомления. Вы можете изменить настройки, перейдя по ссылке: {{ host }}/account/notifications -Hawk -Errors tracking system +*** -Made by CodeX +Хоук +Мониторинг ошибок diff --git a/workers/email/src/templates/emails/block-workspace/html.twig b/workers/email/src/templates/emails/block-workspace/html.twig index 3bdc4830a..2b5e86315 100644 --- a/workers/email/src/templates/emails/block-workspace/html.twig +++ b/workers/email/src/templates/emails/block-workspace/html.twig @@ -5,6 +5,8 @@ {% endblock %} {% block content %} + {% set utmParams = 'utm_source=email&utm_medium=transactional&utm_campaign=block-workspace' %} + @@ -14,7 +16,7 @@ - «{{ workspace.name | escape }}» не принимает события + «{{ workspace.name | escape }}» не видит новые ошибки @@ -33,7 +35,13 @@ - {% include '../../components/button.twig' with {href: host ~ '/workspace/' ~ workspace._id ~ '/settings/billing', label: workspace.tariffPlanId is same as('5f47f031ff71510040f433c1') ? 'Увеличить лимит от 99 ₽' : 'Открыть настройки'} %} + {% set tariffPlanIdString = workspace.tariffPlanId ~ '' %} + {% if tariffPlanIdString == '5f47f031ff71510040f433c1' %} + {% set buttonLabel = 'Увеличить лимит от 99 ₽' %} + {% else %} + {% set buttonLabel = 'Открыть настройки' %} + {% endif %} + {% include '../../components/button.twig' with {href: host ~ '/workspace/' ~ workspace._id ~ '/settings/billing' ~ '?' ~ utmParams, label: buttonLabel} %} {% endblock %} diff --git a/workers/email/src/templates/emails/block-workspace/text.twig b/workers/email/src/templates/emails/block-workspace/text.twig index e0994e1fd..0adee865f 100644 --- a/workers/email/src/templates/emails/block-workspace/text.twig +++ b/workers/email/src/templates/emails/block-workspace/text.twig @@ -1,12 +1,12 @@ +{% set utmParams = 'utm_source=email&utm_medium=transactional&utm_campaign=block-workspace' %} + Мониторинг ошибок остановлен Вы больше не отслеживаете новые ошибки «{{ workspace.name | escape }}», потому что закончился лимит или срок действия тарифного плана -Чтобы продолжить получать события, выберите подходящий тарифный план и продлите подписку в настройках оплаты: {{ host }}/workspace/{{ workspace._id }}/settings/billing +Чтобы продолжить получать события, выберите подходящий тарифный план и продлите подписку в настройках оплаты: {{ host }}/workspace/{{ workspace._id }}/settings/billing?{{ utmParams }} *** Хоук -Российский трекер ошибок - -Made by CodeX \ No newline at end of file +Мониторинг ошибок diff --git a/workers/email/src/templates/emails/blocked-workspace-reminder/html.twig b/workers/email/src/templates/emails/blocked-workspace-reminder/html.twig index 51cfa48fe..9eefd30a0 100644 --- a/workers/email/src/templates/emails/blocked-workspace-reminder/html.twig +++ b/workers/email/src/templates/emails/blocked-workspace-reminder/html.twig @@ -5,6 +5,8 @@ {% endblock %} {% block content %} + {% set utmParams = 'utm_source=email&utm_medium=transactional&utm_campaign=blocked-workspace-reminder' %} + @@ -33,7 +35,13 @@ - {% include '../../components/button.twig' with {href: host ~ '/workspace/' ~ workspace._id ~ '/settings/billing', label: workspace.tariffPlanId is same as('5f47f031ff71510040f433c1') ? 'Выбрать тариф от 99 ₽' : 'Открыть настройки'} %} + {% set tariffPlanIdString = workspace.tariffPlanId ~ '' %} + {% if tariffPlanIdString == '5f47f031ff71510040f433c1' %} + {% set buttonLabel = 'Выбрать тариф от 99 ₽' %} + {% else %} + {% set buttonLabel = 'Открыть настройки' %} + {% endif %} + {% include '../../components/button.twig' with {href: host ~ '/workspace/' ~ workspace._id ~ '/settings/billing' ~ '?' ~ utmParams, label: buttonLabel} %} {% endblock %} diff --git a/workers/email/src/templates/emails/blocked-workspace-reminder/text.twig b/workers/email/src/templates/emails/blocked-workspace-reminder/text.twig index 8d95bc7b0..3f0b22778 100644 --- a/workers/email/src/templates/emails/blocked-workspace-reminder/text.twig +++ b/workers/email/src/templates/emails/blocked-workspace-reminder/text.twig @@ -1,10 +1,9 @@ -Требуется действие: мониторинг ошибок в {{ workspace.name }} не работает уже {{ daysAfterBlock }} {{ pluralize_ru(daysAfterBlock, ['день', 'дня', 'дней']) }} +{% set utmParams = 'utm_source=email&utm_medium=transactional&utm_campaign=blocked-workspace-reminder' %} -Чтобы снова видеть актуальные события, выберите подходящий тарифный план и продлите подписку в настройках оплаты: {{ host }}/workspace/{{ workspace._id }}/settings/billing +Требуется действие: мониторинг ошибок в {{ workspace.name }} не работает уже {{ daysAfterBlock }} {{ pluralize_ru(daysAfterBlock, ['день', 'дня', 'дней']) }} +Чтобы снова видеть актуальные события, выберите подходящий тарифный план и продлите подписку в настройках оплаты: {{ host }}/workspace/{{ workspace._id }}/settings/billing?{{ utmParams }} *** Хоук -Российский трекер ошибок - -Made by CodeX \ No newline at end of file +Мониторинг ошибок diff --git a/workers/email/src/templates/emails/days-limit-almost-reached/html.twig b/workers/email/src/templates/emails/days-limit-almost-reached/html.twig index d568fc185..ad0b38966 100644 --- a/workers/email/src/templates/emails/days-limit-almost-reached/html.twig +++ b/workers/email/src/templates/emails/days-limit-almost-reached/html.twig @@ -5,6 +5,7 @@ {% endblock %} {% block content %} + {% set utmParams = 'utm_source=email&utm_medium=transactional&utm_campaign=days-limit-almost-reached' %} @@ -14,7 +15,7 @@ - {{ daysLeft | escape }} day{{ daysLeft > 1 ? 's' : '' }} left to the paid plan ending + Осталось {{ daysLeft | escape }} {{ pluralize_ru(daysLeft, ['день', 'дня', 'дней']) }} до окончания подписки на мониторинг ошибок. @@ -22,14 +23,16 @@ - Please, make sure that you have linked a card as a payment method for the workspace "{{ workspace.name | escape }}". - Otherwise it will be blocked because the plan was not renewed. + Привяжите карту для автоматического продления тарифа в воркспейсе «{{ workspace.name | escape }}». Или оплатите следующий месяц разово.
+
+ Если план не продлить, то мониторинг ошибок будет приостановлен. +
- {% include '../../components/button.twig' with {href: host ~ '/workspace/' ~ workspace._id ~ '/settings/billing', label: 'Go to payment settings'} %} + {% include '../../components/button.twig' with {href: host ~ '/workspace/' ~ workspace._id ~ '/settings/billing' ~ '?' ~ utmParams, label: 'Перейти к настройкам'} %} {% endblock %} diff --git a/workers/email/src/templates/emails/days-limit-almost-reached/subject.twig b/workers/email/src/templates/emails/days-limit-almost-reached/subject.twig index 099b1cfd5..7a10d8439 100644 --- a/workers/email/src/templates/emails/days-limit-almost-reached/subject.twig +++ b/workers/email/src/templates/emails/days-limit-almost-reached/subject.twig @@ -1 +1 @@ -{{ daysLeft | escape }} day{{ daysLeft > 1 ? 's' : '' }} left to the paid plan ending for workspace {{ workspace.name | escape }}! +Через {{ daysLeft | escape }} {{ pluralize_ru(daysLeft, ['день', 'дня', 'дней']) }} заканчивается подписка воркспейса «{{ workspace.name | escape }}» на мониторинг ошибок! diff --git a/workers/email/src/templates/emails/days-limit-almost-reached/text.twig b/workers/email/src/templates/emails/days-limit-almost-reached/text.twig index 6856d602f..11e529f08 100644 --- a/workers/email/src/templates/emails/days-limit-almost-reached/text.twig +++ b/workers/email/src/templates/emails/days-limit-almost-reached/text.twig @@ -1,10 +1,14 @@ -{{ daysLeft | escape }} day{{ daysLeft > 1 ? 's' : '' }} left to the paid plan ending for workspace {{ workspace.name | escape }}. +{% set utmParams = 'utm_source=email&utm_medium=transactional&utm_campaign=days-limit-almost-reached' %} -Please, check payment settings and renew the plan: {{ host }}/workspace/{{ workspace._id }}/settings/billing +{{ daysLeft | escape }} {{ pluralize_ru(daysLeft, ['день', 'дня', 'дней']) }} до окончания платного тарифа воркспейса «{{ workspace.name | escape }}». -*** +Привяжите карту для автоматического продления тарифа в воркспейсе «{{ workspace.name | escape }}». Или оплатите следующий месяц разово. + +Если план не продлить, то мониторинг ошибок будет приостановлен. -Hawk -Errors tracking system +Перейти к настройкам оплаты: {{ host }}/workspace/{{ workspace._id }}/settings/billing?{{ utmParams }} + +*** -Made by CodeX +Хоук +Мониторинг ошибок diff --git a/workers/email/src/templates/emails/event/html.twig b/workers/email/src/templates/emails/event/html.twig index 35c7c5032..35be79c6c 100644 --- a/workers/email/src/templates/emails/event/html.twig +++ b/workers/email/src/templates/emails/event/html.twig @@ -5,6 +5,7 @@ {% endblock %} {% block content %} + {% set utmParams = 'utm_source=email&utm_medium=transactional&utm_campaign=event' %} {% set event = events[0].event %} {% set daysRepeated = events[0].daysRepeated %} @@ -16,14 +17,18 @@ - {{ event.payload.type ? event.payload.type : 'Uncaught Error' }} + {% if event.payload.type %} + {{ event.payload.type }} + {% else %} + Ошибка + {% endif %} -
+
{{ event.payload.title | escape }} @@ -51,8 +56,8 @@ - {% set eventURL = host ~ '/project/' ~ project._id ~ '/event/' ~ event._id %} - {% include '../../components/button.twig' with {href: eventURL, label: 'View event'} %} + {% set eventURL = host ~ '/project/' ~ project._id ~ '/event/' ~ event._id ~ '?' ~ utmParams %} + {% include '../../components/button.twig' with {href: eventURL, label: 'Смотреть детали'} %} {% endblock %} @@ -62,7 +67,5 @@ {% endblock %} {% block unsubscribeText %} - You received this email because you are currently opted in to receive such alerts via your - project’s notifications settings. You may adjust your preferences at any time by clicking - the link above. + Вы получили это письмо, потому что подписаны на подобные уведомления. Вы можете изменить настройки, перейдя по ссылке выше. {% endblock %} diff --git a/workers/email/src/templates/emails/event/text.twig b/workers/email/src/templates/emails/event/text.twig index 7f60e9429..d12c7f00d 100644 --- a/workers/email/src/templates/emails/event/text.twig +++ b/workers/email/src/templates/emails/event/text.twig @@ -2,15 +2,17 @@ {% set daysRepeated = events[0].daysRepeated %} {% set newCount = events[0].newCount %} {% set usersAffected = events[0].usersAffected %} -{% set newLabel = 'a new event' %} -{% if newCount > 1 %} - {% set newLabel = newCount ~ ' new events' %} +{% set newLabel = newCount ~ ' ' ~ pluralize_ru(newCount, ['новое событие', 'новых события', 'новых событий']) %} +{% if newCount == 1 %} + {# Оставить как есть #} {% endif %} -You have {{ newLabel }} on «{{ project.name }}» project. +{% set utmParams = 'utm_source=email&utm_medium=transactional&utm_campaign=event' %} + +{{ newLabel }} в проекте «{{ project.name }}» ------------------------ -{{ event.payload.type ? event.payload.type : 'Uncaught Error' }}: {{ event.payload.title | escape }} +{% if event.payload.type %}{{ event.payload.type }}: {% endif %}{{ event.payload.title | escape }} ------------------------ {% if event.payload.backtrace is not empty %} {% if event.payload.backtrace[0] is not empty %} @@ -21,20 +23,15 @@ You have {{ newLabel }} on «{{ project.name }}» project. {% endif %} {% endif %} -This event appears {{ event.totalCount }} total times, {{ daysRepeated }} days repeating. +Это событие произошло {{ event.totalCount }} {{ pluralize_ru(event.totalCount, ['раз', 'раза', 'раз']) }} за {{ daysRepeated }} {{ pluralize_ru(daysRepeated, ['день', 'дня', 'дней']) }}. -View event: {{ host }}/project/{{ project._id }}/event/{{ event._id }} +Смотреть детали: {{ host }}/project/{{ project._id }}/event/{{ event._id }}?{{ utmParams }} *** -You received this email because you are currently opted in to receive such alerts via your project’s notifications settings. You may adjust your preferences at any time by clicking the link above. To unsubscribe, follow the link: {{ host }}/unsubscribe/{{ project._id }} - -Hawk -Errors tracking system - -Made by CodeX - - - +Вы получили это письмо, потому что подписаны на подобные уведомления. Вы можете изменить настройки, перейдя по ссылке: {{ host }}/unsubscribe/{{ project._id }} +*** +Хоук +Мониторинг ошибок diff --git a/workers/email/src/templates/emails/events-limit-almost-reached/html.twig b/workers/email/src/templates/emails/events-limit-almost-reached/html.twig index d5b3a1d11..3060288a6 100644 --- a/workers/email/src/templates/emails/events-limit-almost-reached/html.twig +++ b/workers/email/src/templates/emails/events-limit-almost-reached/html.twig @@ -5,6 +5,7 @@ {% endblock %} {% block content %} + {% set utmParams = 'utm_source=email&utm_medium=transactional&utm_campaign=events-limit-almost-reached' %} @@ -14,7 +15,7 @@ - You’re almost out of your error tracking limit + Вы почти исчерпали лимит событий @@ -22,17 +23,17 @@ - You’ve used {{ eventsCount }} of {{ eventsLimit }} events this month in the {{ workspace.name | escape }} workspace. - - Your project is growing — that’s awesome! Let’s make sure you don’t miss any important errors. - - Upgrade your plan to stay on top of everything. + Вы использовали {{ eventsCount }} из {{ eventsLimit }} событий воркспейса «{{ workspace.name | escape }}» в текущем расчетном периоде.
+
+ Ваш проект растёт — это здорово. Давайте убедимся, что ни одна важная ошибка не останется незамеченной.
+
+ Обновите план и оставайтесь в курсе всех происшествий.
- {% include '../../components/button.twig' with {href: host ~ '/workspace/' ~ workspace._id ~ '/settings/billing', label: 'Increase limit — from 99₽'} %} + {% include '../../components/button.twig' with {href: host ~ '/workspace/' ~ workspace._id ~ '/settings/billing' ~ '?' ~ utmParams, label: 'Увеличить лимит — от 99₽'} %} {% endblock %} diff --git a/workers/email/src/templates/emails/events-limit-almost-reached/subject.twig b/workers/email/src/templates/emails/events-limit-almost-reached/subject.twig index 7acf18269..47994c482 100644 --- a/workers/email/src/templates/emails/events-limit-almost-reached/subject.twig +++ b/workers/email/src/templates/emails/events-limit-almost-reached/subject.twig @@ -1 +1 @@ -You’re almost out of error tracking events in {{ workspace.name }} workspace \ No newline at end of file +Лимит событий в воркспейсе {{ workspace.name }} почти достигнут \ No newline at end of file diff --git a/workers/email/src/templates/emails/events-limit-almost-reached/text.twig b/workers/email/src/templates/emails/events-limit-almost-reached/text.twig index c6c413144..a88e7fbe7 100644 --- a/workers/email/src/templates/emails/events-limit-almost-reached/text.twig +++ b/workers/email/src/templates/emails/events-limit-almost-reached/text.twig @@ -1,12 +1,12 @@ -You’ve used {{ eventsCount }} of {{ eventsLimit }} events this month in the {{ workspace.name | escape }} workspace. +Вы использовали {{ eventsCount }} из {{ eventsLimit }} событий воркспейса «{{ workspace.name | escape }}» в текущем расчетном периоде. -Your project is growing — that’s awesome! Let’s make sure you don’t miss any important errors. +Ваш проект растёт — это здорово. Давайте убедимся, что ни одна важная ошибка не останется незамеченной. -Upgrade your plan to stay on top of everything: {{ host }}/workspace/{{ workspace._id }}/settings/billing +{% set utmParams = 'utm_source=email&utm_medium=transactional&utm_campaign=events-limit-almost-reached' %} -*** +При необходимости обновите свой тариф, чтобы всегда быть в курсе всех происшествий: {{ host }}/workspace/{{ workspace._id }}/settings/billing?{{ utmParams }} -Hawk -Errors tracking system +*** -Made by CodeX +Хоук +Мониторинг ошибок diff --git a/workers/email/src/templates/emails/password-reset/html.twig b/workers/email/src/templates/emails/password-reset/html.twig index 9d663a03a..37e511c31 100644 --- a/workers/email/src/templates/emails/password-reset/html.twig +++ b/workers/email/src/templates/emails/password-reset/html.twig @@ -1,6 +1,7 @@ {% extends '../../components/layout.twig' %} {% block content %} + {% set utmParams = 'utm_source=email&utm_medium=transactional&utm_campaign=password-reset' %} @@ -9,21 +10,22 @@ - Your password has been reset + Ваш пароль обновлен - Thank you for using Hawk. Use this password to sign in.

- Your new password: {{ password }} + Используйте новый пароль для входа в ваш аккаунт.
+
+ Новый пароль: {{ password }}
- {% include '../../components/button.twig' with {href: host ~ '/login', label: 'Sign in'} %} + {% include '../../components/button.twig' with {href: host ~ '/login' ~ '?' ~ utmParams, label: 'Войти'} %} diff --git a/workers/email/src/templates/emails/password-reset/subject.twig b/workers/email/src/templates/emails/password-reset/subject.twig index 6072e8631..2cfc60dca 100644 --- a/workers/email/src/templates/emails/password-reset/subject.twig +++ b/workers/email/src/templates/emails/password-reset/subject.twig @@ -1 +1 @@ -Hawk password reset \ No newline at end of file +Новый пароль для вашего аккаунта в Хоуке \ No newline at end of file diff --git a/workers/email/src/templates/emails/password-reset/text.twig b/workers/email/src/templates/emails/password-reset/text.twig index c291546ae..b8cf4788a 100644 --- a/workers/email/src/templates/emails/password-reset/text.twig +++ b/workers/email/src/templates/emails/password-reset/text.twig @@ -1,11 +1,11 @@ -Thank you for using Hawk. Use this password to sign in. +{% set utmParams = 'utm_source=email&utm_medium=transactional&utm_campaign=password-reset' %} -Your new password: {{ password }} -Sign in: {{ host ~ '/login' }} +Используйте новый пароль для входа в ваш аккаунт. -*** +Новый пароль: {{ password }} +Войти: {{ host ~ '/login' }}?{{ utmParams }} -Hawk -Errors tracking system +*** -Made by CodeX \ No newline at end of file +Хоук +Мониторинг ошибок diff --git a/workers/email/src/templates/emails/payment-failed/html.twig b/workers/email/src/templates/emails/payment-failed/html.twig index 531aa86b9..70333e350 100644 --- a/workers/email/src/templates/emails/payment-failed/html.twig +++ b/workers/email/src/templates/emails/payment-failed/html.twig @@ -5,6 +5,7 @@ {% endblock %} {% block content %} +{% set utmParams = 'utm_source=email&utm_medium=transactional&utm_campaign=payment-failed' %} @@ -14,7 +15,7 @@ - Your payment has been failed + Оплата не прошла @@ -22,16 +23,16 @@ - The payment attempt for «{{ workspace.name | escape }}» monthly plan was rejected - because of {{ reason }}.
- Contact support@hawk.so for more details. + Очередная оплата ежемесячного плана для воркспейса «{{ workspace.name | escape }}» была отклонена по причине: {{ reason }}.
+
+ Обратитесь в support@hawk.so для получения дополнительной информации.
- {% set url = host ~ '/workspace/' ~ workspace._id ~ '/settings/billing' %} - {% include '../../components/button.twig' with {href: url, label: 'Go to payment settings'} %} + {% set url = host ~ '/workspace/' ~ workspace._id ~ '/settings/billing' ~ '?' ~ utmParams %} + {% include '../../components/button.twig' with {href: url, label: 'Настройки оплаты'} %} {% endblock %} diff --git a/workers/email/src/templates/emails/payment-failed/subject.twig b/workers/email/src/templates/emails/payment-failed/subject.twig index 21ab39a5e..c5e49faf6 100644 --- a/workers/email/src/templates/emails/payment-failed/subject.twig +++ b/workers/email/src/templates/emails/payment-failed/subject.twig @@ -1 +1 @@ -Payment failed for {{ workspace.name | escape }} workspace \ No newline at end of file +Оплата тарифа для воркспейса {{ workspace.name | escape }} не прошла \ No newline at end of file diff --git a/workers/email/src/templates/emails/payment-failed/text.twig b/workers/email/src/templates/emails/payment-failed/text.twig index 3cd6087a0..fc2faeeaf 100644 --- a/workers/email/src/templates/emails/payment-failed/text.twig +++ b/workers/email/src/templates/emails/payment-failed/text.twig @@ -1,14 +1,10 @@ -Payment failed for {{ workspace.name | escape }} workspace +Очередная оплата ежемесячного плана для воркспейса «{{ workspace.name | escape }}» не прошла. -Your payment has been failed +Причина: {{ reason }} -The payment attempt for «{{ workspace.name | escape }}» monthly plan was rejected -because of {{ reason }} -Contact support@hawk.so for more details. +Напишите нам на support@hawk.so для получения дополнительной информации. *** -Hawk -Errors tracking system - -Made by CodeX +Хоук +Мониторинг ошибок diff --git a/workers/email/src/templates/emails/payment-success/html.twig b/workers/email/src/templates/emails/payment-success/html.twig index fe7c85f4f..63a11b96e 100644 --- a/workers/email/src/templates/emails/payment-success/html.twig +++ b/workers/email/src/templates/emails/payment-success/html.twig @@ -5,6 +5,7 @@ {% endblock %} {% block content %} + {% set utmParams = 'utm_source=email&utm_medium=transactional&utm_campaign=payment-success' %} @@ -14,7 +15,7 @@ - Payment successful + Платеж выполнен успешно @@ -22,14 +23,14 @@ - The "{{ plan.name }}" plan for "{{ workspace.name | escape }}" workspace has been extended by one month. Thanks for using Hawk. + Тариф «{{ plan.name }}» для воркспейса «{{ workspace.name | escape }}» продлен на один месяц. Ошибки под контролем. - {% set url = host ~ '/workspace/' ~ workspace._id ~ '/settings/billing' %} - {% include '../../components/button.twig' with {href: url, label: 'Go to payment settings'} %} + {% set url = host ~ '/workspace/' ~ workspace._id ~ '/settings/billing' ~ '?' ~ utmParams %} + {% include '../../components/button.twig' with {href: url, label: 'Настройки оплаты'} %} {% endblock %} diff --git a/workers/email/src/templates/emails/payment-success/subject.twig b/workers/email/src/templates/emails/payment-success/subject.twig index 7abc1cfde..1866c1e28 100644 --- a/workers/email/src/templates/emails/payment-success/subject.twig +++ b/workers/email/src/templates/emails/payment-success/subject.twig @@ -1 +1 @@ -Payment successful \ No newline at end of file +Тариф успешно оплачен для воркспейса «{{ workspace.name | escape }}» \ No newline at end of file diff --git a/workers/email/src/templates/emails/payment-success/text.twig b/workers/email/src/templates/emails/payment-success/text.twig index 2742903e6..ab9c43fab 100644 --- a/workers/email/src/templates/emails/payment-success/text.twig +++ b/workers/email/src/templates/emails/payment-success/text.twig @@ -1,15 +1,16 @@ -Payment successful +{% set utmParams = 'utm_source=email&utm_medium=transactional&utm_campaign=payment-success' %} -The "{{ plan.name }}" plan for "{{ workspace.name | escape }}" workspace has been extended by one month. -Thanks for using Hawk. +Платеж выполнен успешно -Go to payment settings : {{ host }}/workspace/{{ workspace._id }}/settings/billing +Тариф «{{ plan.name }}» для воркспейса «{{ workspace.name | escape }}» продлен на один месяц. Ошибки под контролем. + +Перейти к настройкам оплаты: {{ host }}/workspace/{{ workspace._id }}/settings/billing?{{ utmParams }} *** -You received this email because you are currently opted in to receive such alerts via your personal notifications settings. You may adjust your preferences at any time by clicking the link: {{ host }}/account/notifications +Вы получили это письмо, потому что подписаны на подобные уведомления. Вы можете изменить настройки, перейдя по ссылке: {{ host }}/account/notifications -Hawk -Errors tracking system +*** -Made by CodeX +Хоук +Мониторинг ошибок diff --git a/workers/email/src/templates/emails/several-events/html.twig b/workers/email/src/templates/emails/several-events/html.twig index 802f3b0dd..63162677b 100644 --- a/workers/email/src/templates/emails/several-events/html.twig +++ b/workers/email/src/templates/emails/several-events/html.twig @@ -47,13 +47,14 @@ - {% set url = host ~ '/project/' ~ project._id %} + {% set utmParams = 'utm_source=email&utm_medium=transactional&utm_campaign=several-events' %} + {% set url = host ~ '/project/' ~ project._id ~ '?' ~ utmParams %} {% if events.length - eventsToShow > 0 %} {% set hiddenEventsLen = events.length - eventsToShow %} - {% set hiddenEventsMessage = 'and ' ~ hiddenEventsLen ~ ' more…' %} + {% set hiddenEventsMessage = 'и еще ' ~ hiddenEventsLen ~ '…' %} {% include '../../components/button.twig' with {href: url, label: hiddenEventsMessage} %} {% else %} - {% include '../../components/button.twig' with {href: url, label: 'View events'} %} + {% include '../../components/button.twig' with {href: url, label: 'Смотреть события'} %} {% endif %} @@ -65,7 +66,5 @@ {% endblock %} {% block unsubscribeText %} - You received this email because you are currently opted in to receive such alerts via your - project’s notifications settings. You may adjust your preferences at any time by clicking - the link above. + Вы получили это письмо, потому что подписаны на подобные уведомления. Вы можете изменить настройки, перейдя по ссылке выше. {% endblock %} diff --git a/workers/email/src/templates/emails/several-events/subject.twig b/workers/email/src/templates/emails/several-events/subject.twig index 75f975cf1..cde2a64f9 100644 --- a/workers/email/src/templates/emails/several-events/subject.twig +++ b/workers/email/src/templates/emails/several-events/subject.twig @@ -1 +1 @@ -{{ project.name | escape }} — {{ events.length }} new events for the last {{ period | prettyTime }} +{{ project.name | escape }} — {{ events.length }} {{ pluralize_ru(events.length, ['новое событие', 'новых события', 'новых событий']) }} за последние {{ period | prettyTime }} diff --git a/workers/email/src/templates/emails/several-events/text.twig b/workers/email/src/templates/emails/several-events/text.twig index 24ecc9eab..085e46e69 100644 --- a/workers/email/src/templates/emails/several-events/text.twig +++ b/workers/email/src/templates/emails/several-events/text.twig @@ -1,35 +1,31 @@ -You have {{ events.length }} new events on «{{ project.name|escape }}» project for the last {{ period | prettyTime }} +У вас {{ events.length }} {{ pluralize_ru(events.length, ['новое событие', 'новых события', 'новых событий']) }} в проекте «{{ project.name|escape }}» за последние {{ period | prettyTime }} {% set eventsToShow = 5 %} {% for eventData in events | sortEvents | slice(0, eventsToShow) %} -{{ eventData.event.payload.type ?: 'Uncaught Error' }}: {{ eventData.event.payload.title }} +{% if eventData.event.payload.type %}{{ eventData.event.payload.type }}: {% endif %}{{ eventData.event.payload.title | escape }} -In file: {{ eventData.event.payload.backtrace[0].file }} at line {{ eventData.event.payload.backtrace[0].line }}. +В файле: {{ eventData.event.payload.backtrace[0].file }} на строке {{ eventData.event.payload.backtrace[0].line }}. -{{ eventData.newCount }} new and {{ eventData.event.totalCount }} total{{ event.daysRepeated ? ', ' ~ event.daysRepeated ~ 'days repeating' : '' }} {{ event.usersAffected ? ', ' ~ event.userAffected ~ ' users affected' : '' }} +{{ eventData.newCount }} {{ pluralize_ru(eventData.newCount, ['новое', 'новых события', 'новых событий']) }} и {{ eventData.event.totalCount }} {{ pluralize_ru(eventData.event.totalCount, ['раз', 'раза', 'раз']) }}{{ event.daysRepeated ? ', повторяется ' ~ event.daysRepeated ~ ' ' ~ pluralize_ru(event.daysRepeated, ['день', 'дня', 'дней']) : '' }} {{ event.usersAffected ? ', затронуто ' ~ event.userAffected ~ ' ' ~ pluralize_ru(event.userAffected, ['пользователь', 'пользователя', 'пользователей']) : '' }} {% endfor %} {% if events.length - eventsToShow > 0 %} -View other {{ events.length - eventsToShow }} events: {{ host }}/project/{{ project._id }} +{% set utmParams = 'utm_source=email&utm_medium=transactional&utm_campaign=several-events' %} +Смотреть еще {{ events.length - eventsToShow }} событий: {{ host }}/project/{{ project._id }}?{{ utmParams }} {% else %} -View events: {{ host }}/project/{{ project._id }} +Смотреть события: {{ host }}/project/{{ project._id }}?{{ utmParams }} {% endif %} *** -You received this email because you are currently opted in to receive such alerts via your project’s notifications settings. You may adjust your preferences at any time by clicking the link above. To unsubscribe, follow the link: {{ host }}/unsubscribe/{{ project._id }} - -Hawk -Errors tracking system - -Made by CodeX - - - +Вы получили это письмо, потому что подписаны на подобные уведомления. Вы можете изменить настройки, перейдя по ссылке: {{ host }}/unsubscribe/{{ project._id }} +*** +Хоук +Мониторинг ошибок diff --git a/workers/email/src/templates/emails/sign-up/html.twig b/workers/email/src/templates/emails/sign-up/html.twig index f472f12b6..a281ef279 100644 --- a/workers/email/src/templates/emails/sign-up/html.twig +++ b/workers/email/src/templates/emails/sign-up/html.twig @@ -9,21 +9,25 @@ - Your account has been created + Войдите в ваш аккаунт - Use this password to sign in. You can change it later.

- Password: {{ password }} + Добро пожаловать в Хоук!
+
+ Ниже — данные для входа в аккаунт. Используйте этот пароль для первого входа, позже вы сможете изменить его в настройках.
+
+ Пароль: {{ password }}
- {% include '../../components/button.twig' with {href: host ~ '/login', label: 'Sign in'} %} + {% set utmParams = 'utm_source=email&utm_medium=transactional&utm_campaign=sign-up' %} + {% include '../../components/button.twig' with {href: host ~ '/login?emailPrefilled=' ~ email ~ '&' ~ utmParams, label: 'Войти'} %} diff --git a/workers/email/src/templates/emails/sign-up/subject.twig b/workers/email/src/templates/emails/sign-up/subject.twig index b65de85b8..7d766f498 100644 --- a/workers/email/src/templates/emails/sign-up/subject.twig +++ b/workers/email/src/templates/emails/sign-up/subject.twig @@ -1 +1 @@ -Hawk - sign in to your account \ No newline at end of file +Добро пожаловать в Хоук! \ No newline at end of file diff --git a/workers/email/src/templates/emails/sign-up/text.twig b/workers/email/src/templates/emails/sign-up/text.twig index 33414949a..646a1ba3d 100644 --- a/workers/email/src/templates/emails/sign-up/text.twig +++ b/workers/email/src/templates/emails/sign-up/text.twig @@ -1,11 +1,13 @@ -Use this password to sign in. You can change it later. +Добро пожаловать в Хоук! -Password: {{ password }} -Sign in: {{ host ~ '/login' }} +Ниже — данные для входа в аккаунт. Используйте этот пароль для первого входа, позже вы сможете изменить его в настройках. -*** +{% set utmParams = 'utm_source=email&utm_medium=transactional&utm_campaign=sign-up' %} + +Пароль: {{ password }} +Войти: {{ host ~ '/login?emailPrefilled=' ~ endpoint }}&{{ utmParams }} -Hawk -Errors tracking system +*** -Made by CodeX \ No newline at end of file +Хоук +Мониторинг ошибок diff --git a/workers/email/src/templates/emails/workspace-invite/html.twig b/workers/email/src/templates/emails/workspace-invite/html.twig index 8621e61ef..2f39c9e6f 100644 --- a/workers/email/src/templates/emails/workspace-invite/html.twig +++ b/workers/email/src/templates/emails/workspace-invite/html.twig @@ -9,21 +9,24 @@ - Your have an invitation + Присоединяйтесь к воркспейсу - You have been invited to {{workspaceName}} workspace.
- Join to get access to all projects of this workspace. + Вас пригласили в «{{workspaceName}}».
+
+ Чтобы получить доступ ко всем проектам этого воркспейса, нажмите кнопку ниже.
- {% include '../../components/button.twig' with {href: inviteLink, label: 'Join the workspace'} %} + {% set utmParams = 'utm_source=email&utm_medium=transactional&utm_campaign=workspace-invite' %} + {% set sep = ('?' in inviteLink) ? '&' : '?' %} + {% include '../../components/button.twig' with {href: inviteLink ~ sep ~ utmParams, label: 'Присоединиться'} %} diff --git a/workers/email/src/templates/emails/workspace-invite/subject.twig b/workers/email/src/templates/emails/workspace-invite/subject.twig index c8b094795..30322511f 100644 --- a/workers/email/src/templates/emails/workspace-invite/subject.twig +++ b/workers/email/src/templates/emails/workspace-invite/subject.twig @@ -1 +1 @@ -Invitation to {{name}} workspace \ No newline at end of file +Вас пригласили в воркспейс «{{workspaceName}}» \ No newline at end of file diff --git a/workers/email/src/templates/emails/workspace-invite/text.twig b/workers/email/src/templates/emails/workspace-invite/text.twig index 8a55d0df4..87b7499b8 100644 --- a/workers/email/src/templates/emails/workspace-invite/text.twig +++ b/workers/email/src/templates/emails/workspace-invite/text.twig @@ -1,12 +1,15 @@ -Your have an invitation +Присоединяйтесь к воркспейсу -You have been invited to "{{workspaceName}}" workspace. -Join to get access to all projects of this workspace. -{{ inviteLink }} +Вас пригласили в «{{workspaceName}}». -*** +Чтобы получить доступ ко всем проектам этого воркспейса, нажмите ссылку ниже. + +{% set utmParams = 'utm_source=email&utm_medium=transactional&utm_campaign=workspace-invite' %} +{% set sep = ('?' in inviteLink) ? '&' : '?' %} -Hawk -Errors tracking system +{{ inviteLink }}{{ sep }}{{ utmParams }} + +*** -Made by CodeX \ No newline at end of file +Хоук +Мониторинг ошибок diff --git a/workers/paymaster/src/index.ts b/workers/paymaster/src/index.ts index 999f50116..333a39118 100644 --- a/workers/paymaster/src/index.ts +++ b/workers/paymaster/src/index.ts @@ -26,7 +26,7 @@ const DAYS_AFTER_PAYDAY_TO_TRY_PAYING = 3; * List of days left number to notify admins about upcoming payment */ // eslint-disable-next-line @typescript-eslint/no-magic-numbers -const DAYS_LEFT_ALERT = [3, 2, 1, 0]; +const DAYS_LEFT_ALERT = [3, 2, 1]; /** * Days after block to remind admins about blocked workspace @@ -53,10 +53,15 @@ export default class PaymasterWorker extends Worker { */ private workspaces: Collection; + /** + * Collection with plans + */ + private plansCollection: Collection; + /** * List of tariff plans */ - private plans: PlanDBScheme[]; + private plans: PlanDBScheme[] = []; /** * Check if today is a payday for passed timestamp @@ -111,13 +116,9 @@ export default class PaymasterWorker extends Worker { const connection = await this.db.connect(); this.workspaces = connection.collection('workspaces'); - const plansCollection = connection.collection('plans'); - - this.plans = await plansCollection.find({}).toArray(); + this.plansCollection = connection.collection('plans'); - if (this.plans.length === 0) { - throw new Error('Please add tariff plans to the database'); - } + await this.fetchPlans(); await super.start(); } @@ -142,6 +143,48 @@ export default class PaymasterWorker extends Worker { } } + /** + * Fetches tariff plans from database and keeps them cached + */ + private async fetchPlans(): Promise { + if (!this.plansCollection) { + throw new Error('Plans collection is not initialized'); + } + + this.plans = await this.plansCollection.find({}).toArray(); + + if (this.plans.length === 0) { + throw new Error('Please add tariff plans to the database'); + } + } + + /** + * Finds plan by id from cached plans + */ + private findPlanById(planId: WorkspaceDBScheme['tariffPlanId']): PlanDBScheme | undefined { + return this.plans.find((plan) => plan._id.toString() === planId.toString()); + } + + /** + * Returns workspace plan, refreshes cache when plan is missing + */ + private async getWorkspacePlan(workspace: WorkspaceDBScheme): Promise { + let currentPlan = this.findPlanById(workspace.tariffPlanId); + + if (currentPlan) { + return currentPlan; + } + + await this.fetchPlans(); + currentPlan = this.findPlanById(workspace.tariffPlanId); + + if (!currentPlan) { + throw new Error(`[Paymaster] Tariff plan ${workspace.tariffPlanId.toString()} not found for workspace ${workspace._id.toString()} (${workspace.name})`); + } + + return currentPlan; + } + /** * WorkspaceSubscriptionCheckEvent event handler * @@ -180,9 +223,7 @@ export default class PaymasterWorker extends Worker { */ private async processWorkspaceSubscriptionCheck(workspace: WorkspaceDBScheme): Promise<[WorkspaceDBScheme, boolean]> { const date = new Date(); - const currentPlan = this.plans.find( - (plan) => plan._id.toString() === workspace.tariffPlanId.toString() - ); + const currentPlan = await this.getWorkspacePlan(workspace); /** Define readable values */ @@ -226,6 +267,10 @@ export default class PaymasterWorker extends Worker { */ if (!isTimeToPay) { /** + * [USED FOR PREPAID WORKSPACES] + * "Recharge" — to reset limits for the new billing period (month). + * It should be done even for prepaid workspaces that do not need to pay anything today. + * * If it is time to recharge workspace limits, but not time to pay * Start new month - recharge billing period events count and update last charge date */ diff --git a/workers/paymaster/tests/index.test.ts b/workers/paymaster/tests/index.test.ts index 4217802b4..8ad43b4de 100644 --- a/workers/paymaster/tests/index.test.ts +++ b/workers/paymaster/tests/index.test.ts @@ -695,6 +695,104 @@ describe('PaymasterWorker', () => { MockDate.reset(); }); + test('Should refetch plans if workspace tariff appears after worker start', async () => { + /** + * Arrange + */ + const currentDate = new Date('2005-12-22'); + const cachedPlan = createPlanMock({ + monthlyCharge: 100, + isDefault: true, + }); + + await tariffCollection.insertOne(cachedPlan); + + const worker = new PaymasterWorker(); + + await worker.start(); + + const newPlan = createPlanMock({ + monthlyCharge: 0, + isDefault: false, + }); + + await tariffCollection.insertOne(newPlan); + + const workspace = createWorkspaceMock({ + plan: newPlan, + subscriptionId: null, + lastChargeDate: new Date('2005-11-22'), + isBlocked: true, + billingPeriodEventsCount: 10, + }); + + await workspacesCollection.insertOne(workspace); + + MockDate.set(currentDate); + + /** + * Act + */ + await expect(worker.handle(WORKSPACE_SUBSCRIPTION_CHECK)).resolves.not.toThrow(); + + /** + * Assert + */ + const addTaskSpy = jest.spyOn(worker, 'addTask'); + + expect(addTaskSpy).toHaveBeenCalledWith('cron-tasks/limiter', { + type: 'unblock-workspace', + workspaceId: workspace._id.toString(), + }); + + await worker.finish(); + MockDate.reset(); + }); + + test('Should throw an error when workspace plan is still missing after refetch', async () => { + /** + * Arrange + */ + const currentDate = new Date('2005-12-22'); + const cachedPlan = createPlanMock({ + monthlyCharge: 100, + isDefault: true, + }); + + await tariffCollection.insertOne(cachedPlan); + + const worker = new PaymasterWorker(); + + await worker.start(); + + const missingPlan = createPlanMock({ + monthlyCharge: 50, + isDefault: false, + }); + + const workspace = createWorkspaceMock({ + plan: missingPlan, + subscriptionId: null, + lastChargeDate: new Date('2005-11-22'), + isBlocked: false, + billingPeriodEventsCount: 10, + }); + + await workspacesCollection.insertOne(workspace); + + MockDate.set(currentDate); + + /** + * Act + Assert + */ + await expect(worker.handle(WORKSPACE_SUBSCRIPTION_CHECK)).rejects.toThrow( + `[Paymaster] Tariff plan ${missingPlan._id.toString()} not found for workspace ${workspace._id.toString()} (${workspace.name})` + ); + + await worker.finish(); + MockDate.reset(); + }); + afterAll(async () => { await connection.close(); MockDate.reset(); diff --git a/workers/sender/src/index.ts b/workers/sender/src/index.ts index ce1f86b95..7f24c1dc3 100644 --- a/workers/sender/src/index.ts +++ b/workers/sender/src/index.ts @@ -598,6 +598,7 @@ export default abstract class SenderWorker extends Worker { host: process.env.GARAGE_URL, hostOfStatic: process.env.API_STATIC_URL, password, + email: endpoint, }, } as SignUpNotification); } diff --git a/workers/sender/types/template-variables/sign-up.ts b/workers/sender/types/template-variables/sign-up.ts index 21cb06919..a2a982ea3 100644 --- a/workers/sender/types/template-variables/sign-up.ts +++ b/workers/sender/types/template-variables/sign-up.ts @@ -8,7 +8,12 @@ export interface SignUpVariables extends CommonTemplateVariables { /** * Password generated for the user */ - password: string + password: string; + + /** + * Email of the user + */ + email: string; } /**