Implement billing status notification and enhance user management integration
CI / Backend (build + test) (push) Successful in 1m30s
CI / Frontend (lint + typecheck + build) (push) Successful in 33s

- Added `NotifyBillingStatusChangedAsync` method to `IRealtimeNotifier` for notifying clients about changes in billing status.
- Updated `BillingConfigResumer` to call the new notification method after modifying billing configurations, ensuring users receive real-time updates.
- Enhanced `ListUsersQueryHandler` to include a `BillingPendingReview` property in `UserSummaryDto`, indicating if a user has a pending payment request awaiting confirmation.
- Refactored various command handlers to utilize `AdvisoryLock` for managing concurrent requests, preventing race conditions in billing operations.
- Updated tests to cover new notification behaviors and ensure proper functionality in billing status management.
This commit is contained in:
Leonid Pershin
2026-07-19 23:22:57 +03:00
parent b32756d5bc
commit e19860ba46
49 changed files with 1195 additions and 233 deletions
+15 -5
View File
@@ -8,7 +8,6 @@ import { Badge } from '@/shared/ui/badge'
import { PaidUntilBadge } from '@/features/billing/PaidUntilBadge'
import { listUsers } from '@/features/admin/users/api'
import { UserManageDialog } from '@/features/admin/users/UserManageDialog'
import type { UserSummaryDto } from '@/shared/api/types'
export const Route = createFileRoute('/admin/users')({ component: AdminUsersPage })
@@ -18,12 +17,17 @@ function AdminUsersPage() {
const { t } = useTranslation()
const [search, setSearch] = useState('')
const [page, setPage] = useState(1)
const [managing, setManaging] = useState<UserSummaryDto | null>(null)
// Id, не сам объект — иначе диалог держит "замороженный" снимок пользователя и не видит
// изменения, сделанные им же самим (гифт/блок/смена роли инвалидируют этот запрос, но проп
// диалога от этого не обновится, если хранить готовый объект). Ищем свежую версию в live-данных
// запроса на каждый рендер.
const [managingId, setManagingId] = useState<string | null>(null)
const { data, isLoading, isError, refetch } = useQuery({
queryKey: ['admin-users', page, search],
queryFn: () => listUsers(page, PAGE_SIZE, search || undefined),
})
const managingUser = managingId ? (data?.items.find((u) => u.id === managingId) ?? null) : null
return (
<div className="flex flex-col gap-4">
@@ -77,13 +81,17 @@ function AdminUsersPage() {
</td>
<td className="py-2">
{user.billingEnabled ? (
<PaidUntilBadge paidUntil={user.billingPaidUntil} showDate />
<PaidUntilBadge
paidUntil={user.billingPaidUntil}
pendingReview={user.billingPendingReview}
showDate
/>
) : (
<span className="text-muted-foreground"></span>
)}
</td>
<td className="py-2 text-right">
<Button size="sm" variant="outline" onClick={() => setManaging(user)}>
<Button size="sm" variant="outline" onClick={() => setManagingId(user.id)}>
{t('admin.users.manage')}
</Button>
</td>
@@ -109,7 +117,9 @@ function AdminUsersPage() {
</>
)}
{managing && <UserManageDialog user={managing} open={!!managing} onOpenChange={(open) => !open && setManaging(null)} />}
{managingUser && (
<UserManageDialog user={managingUser} open={!!managingUser} onOpenChange={(open) => !open && setManagingId(null)} />
)}
</div>
)
}