food-market/tests/food-market.UnitTests/NegativeStockNotifyQuietHoursTests.cs
nns 29a951a6ff
Some checks are pending
Auto-tag / Create date-tag (push) Waiting to run
CI / Backend (.NET 8) (push) Waiting to run
CI / Web (React + Vite) (push) Waiting to run
CI / POS (WPF, Windows) (push) Waiting to run
Docker API / Build + push API (push) Waiting to run
Docker API / Deploy API on stage (push) Blocked by required conditions
Docker Web / Build + push Web (push) Waiting to run
Docker Web / Deploy Web on stage (push) Blocked by required conditions
feat(notifications): центр уведомлений + агрегация/эскалация минусов + каналы
Персистентные уведомления (таблица notifications, Phase41) + REST
/api/notifications (список/счётчик/read/read-all) → центр уведомлений в вебе
(колокольчик, API-backed) и мобильном API.

Фоновый джоб NegativeStockNotificationJob (cron */5): агрегирует новые минусы
в ОДНО уведомление на склад (дебаунс по NotifiedAt + интервал орг., по умолч.
15 мин), уважает тихие часы (Asia/Almaty); эскалация просроченных (>N часов,
по умолч. 24) владельцу/админам. Получатели — админы, владелец, кладовщики
(право InventoryEdit). Каналы: SignalR (живой колокольчик), Telegram (владельцу),
push-заглушка.

Настройки организации: NegStockNotify{Enabled,IntervalMinutes,QuietFrom/ToHour,
EscalateHours} + GET/PUT /api/organization/settings + секция в настройках веба.
Блок «Просроченные минусы» на дашборде.

Тесты: unit тихих часов 3/3; integration — джоб создаёт уведомление админу,
дебаунс, read-all 3/3 (вся группа NegativeStockTests зелёная).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-09 02:56:59 +05:00

47 lines
2.4 KiB
C#
Raw Permalink 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.

using System;
using foodmarket.Api.Background;
using FluentAssertions;
using Xunit;
namespace foodmarket.UnitTests;
/// <summary>Тихие часы агрегированной рассылки уведомлений об отрицательных
/// остатках. Время считается в зоне Asia/Almaty (UTC+5). Интервал может
/// переходить через полночь; null/равные границы = тихих часов нет.</summary>
public class NegativeStockNotifyQuietHoursTests
{
// 03:00 UTC = 08:00 Алматы.
private static readonly DateTime At08Almaty = new(2026, 10, 9, 3, 0, 0, DateTimeKind.Utc);
// 18:00 UTC = 23:00 Алматы.
private static readonly DateTime At23Almaty = new(2026, 10, 9, 18, 0, 0, DateTimeKind.Utc);
// 21:00 UTC = 02:00 Алматы (следующих суток).
private static readonly DateTime At02Almaty = new(2026, 10, 9, 21, 0, 0, DateTimeKind.Utc);
[Fact]
public void No_quiet_hours_when_null_or_equal()
{
NegativeStockNotificationJob.InQuietHours(At23Almaty, null, 8).Should().BeFalse();
NegativeStockNotificationJob.InQuietHours(At23Almaty, 22, null).Should().BeFalse();
NegativeStockNotificationJob.InQuietHours(At23Almaty, 0, 0).Should().BeFalse();
}
[Fact]
public void Overnight_window_22_to_8_covers_late_night_and_early_morning()
{
// Тихие часы 22:00–08:00 (через полночь).
NegativeStockNotificationJob.InQuietHours(At23Almaty, 22, 8).Should().BeTrue(); // 23:00 — тихо
NegativeStockNotificationJob.InQuietHours(At02Almaty, 22, 8).Should().BeTrue(); // 02:00 — тихо
NegativeStockNotificationJob.InQuietHours(At08Almaty, 22, 8).Should().BeFalse(); // 08:00 — уже можно
}
[Fact]
public void Daytime_window_9_to_18_is_same_day_range()
{
NegativeStockNotificationJob.InQuietHours(At08Almaty, 9, 18).Should().BeFalse(); // 08:00 — вне окна
NegativeStockNotificationJob.InQuietHours(At23Almaty, 9, 18).Should().BeFalse(); // 23:00 — вне окна
// 13:00 Алматы = 08:00 UTC… проверим середину окна: 10:00 UTC = 15:00 Алматы.
var at15 = new DateTime(2026, 10, 9, 10, 0, 0, DateTimeKind.Utc);
NegativeStockNotificationJob.InQuietHours(at15, 9, 18).Should().BeTrue();
}
}