Show latest credit ledger in admin
Expose a paginated global ledger timeline and load it automatically so operators can see recent credit activity without first locating a user.
This commit is contained in:
@@ -237,6 +237,33 @@ fun Route.adminApiRoutes(
|
||||
}
|
||||
}
|
||||
|
||||
get("/credits/ledger") {
|
||||
if (
|
||||
call.requireRole(
|
||||
sessionService,
|
||||
setOf(AdminRole.SUPER_ADMIN, AdminRole.SUPPORT),
|
||||
) == null
|
||||
) return@get
|
||||
val limit = call.pageLimit(maximum = 100, default = 100) ?: run {
|
||||
call.respond(HttpStatusCode.BadRequest, AdminErrorResponse("VALIDATION_ERROR"))
|
||||
return@get
|
||||
}
|
||||
try {
|
||||
val page = usersService.latestLedger(
|
||||
limit = limit,
|
||||
cursor = call.request.queryParameters["cursor"],
|
||||
)
|
||||
call.respond(
|
||||
PageResponse(
|
||||
page.items.map(AdminUserLedgerEntryDto::toLedgerResponse),
|
||||
page.nextCursor,
|
||||
),
|
||||
)
|
||||
} catch (_: IllegalArgumentException) {
|
||||
call.respond(HttpStatusCode.BadRequest, AdminErrorResponse("VALIDATION_ERROR"))
|
||||
}
|
||||
}
|
||||
|
||||
post("/credits/grants") {
|
||||
val principal = call.requireMutationPrincipal(config, sessionService) ?: return@post
|
||||
if (principal.role != AdminRole.SUPER_ADMIN) {
|
||||
@@ -667,6 +694,7 @@ private fun AdminUserDetailDto.toUserDetailResponse(): AdminUserDetailResponse =
|
||||
private fun AdminUserLedgerEntryDto.toLedgerResponse(): AdminLedgerResponse =
|
||||
AdminLedgerResponse(
|
||||
entryId = id,
|
||||
userId = userId,
|
||||
type = when (type) {
|
||||
"USAGE_RESERVE" -> "reserve"
|
||||
"USAGE_SETTLE" -> "settle"
|
||||
@@ -864,6 +892,7 @@ private data class AdminUserDetailResponse(
|
||||
@Serializable
|
||||
private data class AdminLedgerResponse(
|
||||
val entryId: String,
|
||||
val userId: String,
|
||||
val type: String,
|
||||
val amount: Long,
|
||||
val balanceAfter: Long,
|
||||
|
||||
@@ -26,6 +26,7 @@ data class AdminUserPageDto(
|
||||
@Serializable
|
||||
data class AdminUserLedgerEntryDto(
|
||||
val id: String,
|
||||
val userId: String,
|
||||
val type: String,
|
||||
val amountDelta: Long,
|
||||
val balanceAfter: Long,
|
||||
|
||||
+28
@@ -47,6 +47,11 @@ interface AdminUsersRepository {
|
||||
limit: Int,
|
||||
cursor: AdminUserLedgerCursor?,
|
||||
): List<AdminUserLedgerEntryDto>
|
||||
|
||||
suspend fun listLatestLedger(
|
||||
limit: Int,
|
||||
cursor: AdminUserLedgerCursor?,
|
||||
): List<AdminUserLedgerEntryDto>
|
||||
}
|
||||
|
||||
class ExposedAdminUsersRepository(
|
||||
@@ -173,6 +178,28 @@ class ExposedAdminUsersRepository(
|
||||
.limit(limit)
|
||||
.map { it.toUserLedgerRow().toDto() }
|
||||
}
|
||||
|
||||
override suspend fun listLatestLedger(
|
||||
limit: Int,
|
||||
cursor: AdminUserLedgerCursor?,
|
||||
): List<AdminUserLedgerEntryDto> = databaseFactory.query {
|
||||
val query = AdminUsersCreditLedgerTable.selectAll()
|
||||
if (cursor != null) {
|
||||
query.where {
|
||||
(AdminUsersCreditLedgerTable.createdAt less cursor.createdAt) or
|
||||
(
|
||||
(AdminUsersCreditLedgerTable.createdAt eq cursor.createdAt) and
|
||||
(AdminUsersCreditLedgerTable.id less cursor.ledgerEntryId.toString())
|
||||
)
|
||||
}
|
||||
}
|
||||
query.orderBy(
|
||||
AdminUsersCreditLedgerTable.createdAt to SortOrder.DESC,
|
||||
AdminUsersCreditLedgerTable.id to SortOrder.DESC,
|
||||
)
|
||||
.limit(limit)
|
||||
.map { it.toUserLedgerRow().toDto() }
|
||||
}
|
||||
}
|
||||
|
||||
private data class UserSupportRows(
|
||||
@@ -324,6 +351,7 @@ private fun ResultRow.toUserLedgerRow() = UserLedgerRow(
|
||||
|
||||
private fun UserLedgerRow.toDto() = AdminUserLedgerEntryDto(
|
||||
id = id.toString(),
|
||||
userId = userId.toString(),
|
||||
type = type.name,
|
||||
amountDelta = amountDelta,
|
||||
balanceAfter = balanceAfter,
|
||||
|
||||
+19
-2
@@ -1,6 +1,7 @@
|
||||
package com.osglab.account.features.admin.users.services
|
||||
|
||||
import com.osglab.account.features.admin.users.models.AdminUserDetailDto
|
||||
import com.osglab.account.features.admin.users.models.AdminUserLedgerEntryDto
|
||||
import com.osglab.account.features.admin.users.models.AdminUserLedgerPageDto
|
||||
import com.osglab.account.features.admin.users.models.AdminUserPageDto
|
||||
import com.osglab.account.features.admin.users.repositories.AdminUserCursor
|
||||
@@ -67,11 +68,27 @@ class AdminUsersService(
|
||||
userId: UUID,
|
||||
limit: Int = 50,
|
||||
cursor: String? = null,
|
||||
): AdminUserLedgerPageDto {
|
||||
return ledgerPage(limit, cursor) { pageSize, decodedCursor ->
|
||||
if (!repository.exists(userId)) throw AdminUserNotFoundException()
|
||||
repository.listLedger(userId, pageSize, decodedCursor)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun latestLedger(
|
||||
limit: Int = 100,
|
||||
cursor: String? = null,
|
||||
): AdminUserLedgerPageDto =
|
||||
ledgerPage(limit, cursor, repository::listLatestLedger)
|
||||
|
||||
private suspend fun ledgerPage(
|
||||
limit: Int,
|
||||
cursor: String?,
|
||||
load: suspend (Int, AdminUserLedgerCursor?) -> List<AdminUserLedgerEntryDto>,
|
||||
): AdminUserLedgerPageDto {
|
||||
require(limit in 1..100) { "Ledger page limit must be between 1 and 100" }
|
||||
val decodedCursor = cursor?.let(AdminUserLedgerCursorCodec::decode)
|
||||
if (!repository.exists(userId)) throw AdminUserNotFoundException()
|
||||
val results = repository.listLedger(userId, limit + 1, decodedCursor)
|
||||
val results = load(limit + 1, decodedCursor)
|
||||
val hasMore = results.size > limit
|
||||
val items = results.take(limit)
|
||||
val nextCursor = if (hasMore) {
|
||||
|
||||
Reference in New Issue
Block a user