platform-codebase/features/payments/backend-api/admin/admin-payouts.controller.ts

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
}
}