feat: add donations feature with one-time payments - M6 (refs #55)
This commit is contained in:
@@ -0,0 +1,81 @@
|
||||
/**
|
||||
* @ai-summary Donations HTTP controller
|
||||
* @ai-context Handles donation creation and history endpoints
|
||||
*/
|
||||
|
||||
import { FastifyRequest, FastifyReply } from 'fastify';
|
||||
import { DonationsService } from '../domain/donations.service';
|
||||
import { SubscriptionsRepository } from '../data/subscriptions.repository';
|
||||
import { StripeClient } from '../external/stripe/stripe.client';
|
||||
import { pool } from '../../../core/config/database';
|
||||
import { logger } from '../../../core/logging/logger';
|
||||
|
||||
interface CreateDonationBody {
|
||||
amount: number;
|
||||
}
|
||||
|
||||
export class DonationsController {
|
||||
private service: DonationsService;
|
||||
|
||||
constructor() {
|
||||
const repository = new SubscriptionsRepository(pool);
|
||||
const stripeClient = new StripeClient();
|
||||
this.service = new DonationsService(repository, stripeClient, pool);
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/donations - Create donation payment intent
|
||||
*/
|
||||
async createDonation(request: FastifyRequest, reply: FastifyReply) {
|
||||
try {
|
||||
const userId = (request as any).user.sub;
|
||||
const { amount } = request.body as CreateDonationBody;
|
||||
|
||||
logger.info('Creating donation', { userId, amount });
|
||||
|
||||
// Validate amount
|
||||
if (!amount || amount <= 0) {
|
||||
return reply.code(400).send({ error: 'Invalid amount' });
|
||||
}
|
||||
|
||||
// Convert dollars to cents
|
||||
const amountCents = Math.round(amount * 100);
|
||||
|
||||
// Create donation
|
||||
const result = await this.service.createDonation(userId, amountCents);
|
||||
|
||||
return reply.code(201).send(result);
|
||||
} catch (error: any) {
|
||||
logger.error('Failed to create donation', {
|
||||
error: error.message,
|
||||
});
|
||||
|
||||
if (error.message.includes('must be at least')) {
|
||||
return reply.code(400).send({ error: error.message });
|
||||
}
|
||||
|
||||
return reply.code(500).send({ error: 'Failed to create donation' });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/donations - Get user's donation history
|
||||
*/
|
||||
async getDonations(request: FastifyRequest, reply: FastifyReply) {
|
||||
try {
|
||||
const userId = (request as any).user.sub;
|
||||
|
||||
logger.info('Getting donations', { userId });
|
||||
|
||||
const donations = await this.service.getUserDonations(userId);
|
||||
|
||||
return reply.code(200).send(donations);
|
||||
} catch (error: any) {
|
||||
logger.error('Failed to get donations', {
|
||||
error: error.message,
|
||||
});
|
||||
|
||||
return reply.code(500).send({ error: 'Failed to get donations' });
|
||||
}
|
||||
}
|
||||
}
|
||||
26
backend/src/features/subscriptions/api/donations.routes.ts
Normal file
26
backend/src/features/subscriptions/api/donations.routes.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
/**
|
||||
* @ai-summary Donations HTTP routes
|
||||
* @ai-context Defines donation endpoints with authentication
|
||||
*/
|
||||
|
||||
import { FastifyInstance, FastifyPluginAsync, FastifyPluginOptions } from 'fastify';
|
||||
import { DonationsController } from './donations.controller';
|
||||
|
||||
export const donationsRoutes: FastifyPluginAsync = async (
|
||||
fastify: FastifyInstance,
|
||||
_opts: FastifyPluginOptions
|
||||
) => {
|
||||
const controller = new DonationsController();
|
||||
|
||||
// POST /api/donations - Create donation
|
||||
fastify.post('/donations', {
|
||||
preHandler: [fastify.authenticate],
|
||||
handler: controller.createDonation.bind(controller),
|
||||
});
|
||||
|
||||
// GET /api/donations - Get donation history
|
||||
fastify.get('/donations', {
|
||||
preHandler: [fastify.authenticate],
|
||||
handler: controller.getDonations.bind(controller),
|
||||
});
|
||||
};
|
||||
Reference in New Issue
Block a user