template-saas/apps/backend/dist/modules/billing/services/billing.service.js
rckrdmrd 26f0e52ca7 feat: Initial commit - template-saas
Template base para proyectos SaaS multi-tenant.

Estructura inicial:
- apps/backend (NestJS API)
- apps/frontend (React/Vite)
- apps/database (PostgreSQL DDL)
- docs/ (Documentación)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-07 04:41:24 -06:00

252 lines
11 KiB
JavaScript

"use strict";
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
var __param = (this && this.__param) || function (paramIndex, decorator) {
return function (target, key) { decorator(target, key, paramIndex); }
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.BillingService = void 0;
const common_1 = require("@nestjs/common");
const typeorm_1 = require("@nestjs/typeorm");
const typeorm_2 = require("typeorm");
const subscription_entity_1 = require("../entities/subscription.entity");
const invoice_entity_1 = require("../entities/invoice.entity");
const payment_method_entity_1 = require("../entities/payment-method.entity");
let BillingService = class BillingService {
constructor(subscriptionRepo, invoiceRepo, paymentMethodRepo) {
this.subscriptionRepo = subscriptionRepo;
this.invoiceRepo = invoiceRepo;
this.paymentMethodRepo = paymentMethodRepo;
}
async createSubscription(dto) {
const now = new Date();
const periodEnd = new Date(now);
periodEnd.setMonth(periodEnd.getMonth() + 1);
const subscription = this.subscriptionRepo.create({
tenant_id: dto.tenant_id,
plan_id: dto.plan_id,
status: dto.trial_end ? subscription_entity_1.SubscriptionStatus.TRIAL : subscription_entity_1.SubscriptionStatus.ACTIVE,
current_period_start: now,
current_period_end: periodEnd,
trial_end: dto.trial_end ? new Date(dto.trial_end) : null,
payment_provider: dto.payment_provider,
});
return this.subscriptionRepo.save(subscription);
}
async getSubscription(tenantId) {
return this.subscriptionRepo.findOne({
where: { tenant_id: tenantId },
order: { created_at: 'DESC' },
});
}
async updateSubscription(tenantId, dto) {
const subscription = await this.getSubscription(tenantId);
if (!subscription) {
throw new common_1.NotFoundException('Subscription not found');
}
Object.assign(subscription, dto);
return this.subscriptionRepo.save(subscription);
}
async cancelSubscription(tenantId, dto) {
const subscription = await this.getSubscription(tenantId);
if (!subscription) {
throw new common_1.NotFoundException('Subscription not found');
}
subscription.cancelled_at = new Date();
if (dto.immediately) {
subscription.status = subscription_entity_1.SubscriptionStatus.CANCELLED;
}
if (dto.reason) {
subscription.metadata = {
...subscription.metadata,
cancellation_reason: dto.reason,
};
}
return this.subscriptionRepo.save(subscription);
}
async changePlan(tenantId, newPlanId) {
const subscription = await this.getSubscription(tenantId);
if (!subscription) {
throw new common_1.NotFoundException('Subscription not found');
}
subscription.plan_id = newPlanId;
subscription.metadata = {
...subscription.metadata,
plan_changed_at: new Date().toISOString(),
};
return this.subscriptionRepo.save(subscription);
}
async renewSubscription(tenantId) {
const subscription = await this.getSubscription(tenantId);
if (!subscription) {
throw new common_1.NotFoundException('Subscription not found');
}
const now = new Date();
const newPeriodEnd = new Date(subscription.current_period_end);
newPeriodEnd.setMonth(newPeriodEnd.getMonth() + 1);
subscription.current_period_start = subscription.current_period_end;
subscription.current_period_end = newPeriodEnd;
subscription.status = subscription_entity_1.SubscriptionStatus.ACTIVE;
return this.subscriptionRepo.save(subscription);
}
async createInvoice(tenantId, subscriptionId, lineItems) {
const invoiceNumber = await this.generateInvoiceNumber();
const items = lineItems.map((item) => ({
...item,
amount: item.quantity * item.unit_price,
}));
const subtotal = items.reduce((sum, item) => sum + item.amount, 0);
const tax = subtotal * 0.16;
const total = subtotal + tax;
const dueDate = new Date();
dueDate.setDate(dueDate.getDate() + 15);
const invoice = this.invoiceRepo.create({
tenant_id: tenantId,
subscription_id: subscriptionId,
invoice_number: invoiceNumber,
status: invoice_entity_1.InvoiceStatus.OPEN,
subtotal,
tax,
total,
due_date: dueDate,
line_items: items,
});
return this.invoiceRepo.save(invoice);
}
async getInvoices(tenantId, options) {
const page = options?.page || 1;
const limit = options?.limit || 10;
const [data, total] = await this.invoiceRepo.findAndCount({
where: { tenant_id: tenantId },
order: { created_at: 'DESC' },
skip: (page - 1) * limit,
take: limit,
});
return { data, total, page, limit };
}
async getInvoice(invoiceId, tenantId) {
const invoice = await this.invoiceRepo.findOne({
where: { id: invoiceId, tenant_id: tenantId },
});
if (!invoice) {
throw new common_1.NotFoundException('Invoice not found');
}
return invoice;
}
async markInvoicePaid(invoiceId, tenantId) {
const invoice = await this.getInvoice(invoiceId, tenantId);
invoice.status = invoice_entity_1.InvoiceStatus.PAID;
invoice.paid_at = new Date();
return this.invoiceRepo.save(invoice);
}
async voidInvoice(invoiceId, tenantId) {
const invoice = await this.getInvoice(invoiceId, tenantId);
if (invoice.status === invoice_entity_1.InvoiceStatus.PAID) {
throw new common_1.BadRequestException('Cannot void a paid invoice');
}
invoice.status = invoice_entity_1.InvoiceStatus.VOID;
return this.invoiceRepo.save(invoice);
}
async generateInvoiceNumber() {
const year = new Date().getFullYear();
const month = String(new Date().getMonth() + 1).padStart(2, '0');
const count = await this.invoiceRepo.count();
const sequence = String(count + 1).padStart(6, '0');
return `INV-${year}${month}-${sequence}`;
}
async addPaymentMethod(tenantId, dto) {
if (dto.is_default) {
await this.paymentMethodRepo.update({ tenant_id: tenantId, is_default: true }, { is_default: false });
}
const paymentMethod = this.paymentMethodRepo.create({
tenant_id: tenantId,
...dto,
});
return this.paymentMethodRepo.save(paymentMethod);
}
async getPaymentMethods(tenantId) {
return this.paymentMethodRepo.find({
where: { tenant_id: tenantId, is_active: true },
order: { is_default: 'DESC', created_at: 'DESC' },
});
}
async getDefaultPaymentMethod(tenantId) {
return this.paymentMethodRepo.findOne({
where: { tenant_id: tenantId, is_default: true, is_active: true },
});
}
async setDefaultPaymentMethod(paymentMethodId, tenantId) {
const paymentMethod = await this.paymentMethodRepo.findOne({
where: { id: paymentMethodId, tenant_id: tenantId },
});
if (!paymentMethod) {
throw new common_1.NotFoundException('Payment method not found');
}
await this.paymentMethodRepo.update({ tenant_id: tenantId, is_default: true }, { is_default: false });
paymentMethod.is_default = true;
return this.paymentMethodRepo.save(paymentMethod);
}
async removePaymentMethod(paymentMethodId, tenantId) {
const paymentMethod = await this.paymentMethodRepo.findOne({
where: { id: paymentMethodId, tenant_id: tenantId },
});
if (!paymentMethod) {
throw new common_1.NotFoundException('Payment method not found');
}
if (paymentMethod.is_default) {
throw new common_1.BadRequestException('Cannot remove default payment method');
}
paymentMethod.is_active = false;
await this.paymentMethodRepo.save(paymentMethod);
}
async getBillingSummary(tenantId) {
const subscription = await this.getSubscription(tenantId);
const defaultPaymentMethod = await this.getDefaultPaymentMethod(tenantId);
const pendingInvoices = await this.invoiceRepo.find({
where: { tenant_id: tenantId, status: invoice_entity_1.InvoiceStatus.OPEN },
});
const totalDue = pendingInvoices.reduce((sum, inv) => sum + Number(inv.total), 0);
return {
subscription,
defaultPaymentMethod,
pendingInvoices: pendingInvoices.length,
totalDue,
};
}
async checkSubscriptionStatus(tenantId) {
const subscription = await this.getSubscription(tenantId);
if (!subscription) {
return { isActive: false, daysRemaining: 0, status: subscription_entity_1.SubscriptionStatus.EXPIRED };
}
const now = new Date();
const periodEnd = new Date(subscription.current_period_end);
const daysRemaining = Math.ceil((periodEnd.getTime() - now.getTime()) / (1000 * 60 * 60 * 24));
const isActive = [
subscription_entity_1.SubscriptionStatus.ACTIVE,
subscription_entity_1.SubscriptionStatus.TRIAL,
].includes(subscription.status);
return {
isActive,
daysRemaining: Math.max(0, daysRemaining),
status: subscription.status,
};
}
};
exports.BillingService = BillingService;
exports.BillingService = BillingService = __decorate([
(0, common_1.Injectable)(),
__param(0, (0, typeorm_1.InjectRepository)(subscription_entity_1.Subscription)),
__param(1, (0, typeorm_1.InjectRepository)(invoice_entity_1.Invoice)),
__param(2, (0, typeorm_1.InjectRepository)(payment_method_entity_1.PaymentMethod)),
__metadata("design:paramtypes", [typeorm_2.Repository,
typeorm_2.Repository,
typeorm_2.Repository])
], BillingService);
//# sourceMappingURL=billing.service.js.map