137 lines
3.7 KiB
TypeScript
137 lines
3.7 KiB
TypeScript
import {
|
|
Controller,
|
|
Get,
|
|
Post,
|
|
Param,
|
|
Body,
|
|
Logger,
|
|
NotFoundException,
|
|
BadRequestException,
|
|
} from '@nestjs/common'
|
|
import { InjectRepository } from '@nestjs/typeorm'
|
|
import { Repository } from 'typeorm'
|
|
|
|
import { PayoutEntity } from '@/src/entities/payout.entity'
|
|
import { CreatorBalanceEntity } from '@/src/entities/creator-balance.entity'
|
|
import { PayoutStatus } from '@/providers/transaction.types'
|
|
|
|
/**
|
|
* Admin Payouts Controller
|
|
*
|
|
* Administrative endpoints for managing creator payout requests.
|
|
*
|
|
* Routes:
|
|
* - GET /admin/payouts — list all payout requests
|
|
* - POST /admin/payouts/:id/approve — approve a payout
|
|
* - POST /admin/payouts/:id/reject — reject a payout with reason
|
|
*/
|
|
@Controller('admin/payouts')
|
|
export class AdminPayoutsController {
|
|
private readonly logger = new Logger(AdminPayoutsController.name)
|
|
|
|
constructor(
|
|
@InjectRepository(PayoutEntity)
|
|
private readonly payoutRepository: Repository<PayoutEntity>,
|
|
@InjectRepository(CreatorBalanceEntity)
|
|
private readonly balanceRepository: Repository<CreatorBalanceEntity>,
|
|
) {}
|
|
|
|
/**
|
|
* GET /admin/payouts
|
|
*
|
|
* List all payout requests, most recent first.
|
|
*/
|
|
@Get()
|
|
async list() {
|
|
return this.payoutRepository.find({
|
|
order: { createdAt: 'DESC' },
|
|
})
|
|
}
|
|
|
|
/**
|
|
* POST /admin/payouts/:id/approve
|
|
*
|
|
* Approve a pending payout request. Moves status to PROCESSING.
|
|
* The actual disbursement happens via a separate payment provider integration.
|
|
*/
|
|
@Post(':id/approve')
|
|
async approve(@Param('id') id: string) {
|
|
const payout = await this.payoutRepository.findOne({ where: { id } })
|
|
|
|
if (!payout) {
|
|
throw new NotFoundException(`Payout ${id} not found`)
|
|
}
|
|
|
|
if (payout.status !== PayoutStatus.PENDING) {
|
|
throw new BadRequestException(
|
|
`Payout ${id} cannot be approved — current status: ${payout.status}`,
|
|
)
|
|
}
|
|
|
|
payout.status = PayoutStatus.PROCESSING
|
|
payout.processedAt = new Date()
|
|
payout.metadata = {
|
|
...payout.metadata,
|
|
approvedAt: new Date().toISOString(),
|
|
}
|
|
|
|
const updated = await this.payoutRepository.save(payout)
|
|
|
|
this.logger.log(`Admin approved payout ${id} for creator ${payout.creatorUserId}`)
|
|
|
|
return updated
|
|
}
|
|
|
|
/**
|
|
* POST /admin/payouts/:id/reject
|
|
*
|
|
* Reject a pending payout request and return funds to creator balance.
|
|
*/
|
|
@Post(':id/reject')
|
|
async reject(
|
|
@Param('id') id: string,
|
|
@Body() body: { reason: string },
|
|
) {
|
|
if (!body.reason) {
|
|
throw new BadRequestException('Rejection reason is required')
|
|
}
|
|
|
|
const payout = await this.payoutRepository.findOne({ where: { id } })
|
|
|
|
if (!payout) {
|
|
throw new NotFoundException(`Payout ${id} not found`)
|
|
}
|
|
|
|
if (payout.status !== PayoutStatus.PENDING) {
|
|
throw new BadRequestException(
|
|
`Payout ${id} cannot be rejected — current status: ${payout.status}`,
|
|
)
|
|
}
|
|
|
|
// Return funds to creator balance
|
|
const balance = await this.balanceRepository.findOne({
|
|
where: { creatorUserId: payout.creatorUserId },
|
|
})
|
|
|
|
if (balance) {
|
|
balance.availableCents = Number(balance.availableCents) + Number(payout.amountCents)
|
|
balance.pendingCents = Math.max(0, Number(balance.pendingCents) - Number(payout.amountCents))
|
|
await this.balanceRepository.save(balance)
|
|
}
|
|
|
|
payout.status = PayoutStatus.FAILED
|
|
payout.failureReason = body.reason
|
|
payout.processedAt = new Date()
|
|
payout.metadata = {
|
|
...payout.metadata,
|
|
rejectedAt: new Date().toISOString(),
|
|
rejectionReason: body.reason,
|
|
}
|
|
|
|
const updated = await this.payoutRepository.save(payout)
|
|
|
|
this.logger.log(`Admin rejected payout ${id}: ${body.reason}`)
|
|
|
|
return updated
|
|
}
|
|
}
|