Marketplace móvil para negocios locales mexicanos. Estructura inicial: - apps/backend (NestJS API) - apps/frontend (React Web) - apps/mobile (Expo/React Native) - apps/mcp-server (Claude MCP Server) - apps/whatsapp-service (WhatsApp Business API) - 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>
220 lines
9.8 KiB
JavaScript
220 lines
9.8 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); }
|
|
};
|
|
var BillingService_1;
|
|
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 stripe_service_1 = require("./stripe.service");
|
|
const subscription_entity_1 = require("../subscriptions/entities/subscription.entity");
|
|
const plan_entity_1 = require("../subscriptions/entities/plan.entity");
|
|
const token_balance_entity_1 = require("../subscriptions/entities/token-balance.entity");
|
|
const token_usage_entity_1 = require("../subscriptions/entities/token-usage.entity");
|
|
let BillingService = BillingService_1 = class BillingService {
|
|
constructor(stripeService, subscriptionRepo, planRepo, tokenBalanceRepo, tokenUsageRepo) {
|
|
this.stripeService = stripeService;
|
|
this.subscriptionRepo = subscriptionRepo;
|
|
this.planRepo = planRepo;
|
|
this.tokenBalanceRepo = tokenBalanceRepo;
|
|
this.tokenUsageRepo = tokenUsageRepo;
|
|
this.logger = new common_1.Logger(BillingService_1.name);
|
|
this.tokenPackages = [
|
|
{ code: 'tokens_1000', name: '1,000 Tokens', tokens: 1000, priceMxn: 29 },
|
|
{ code: 'tokens_3000', name: '3,000 Tokens', tokens: 3000, priceMxn: 69 },
|
|
{ code: 'tokens_8000', name: '8,000 Tokens', tokens: 8000, priceMxn: 149 },
|
|
{ code: 'tokens_20000', name: '20,000 Tokens', tokens: 20000, priceMxn: 299 },
|
|
];
|
|
}
|
|
async getPlans() {
|
|
return this.planRepo.find({
|
|
where: { status: 'active' },
|
|
order: { priceMonthly: 'ASC' },
|
|
});
|
|
}
|
|
getTokenPackages() {
|
|
return this.tokenPackages;
|
|
}
|
|
async createSubscriptionCheckout(tenantId, planCode, successUrl, cancelUrl) {
|
|
const plan = await this.planRepo.findOne({ where: { code: planCode, status: 'active' } });
|
|
if (!plan || !plan.stripePriceIdMonthly) {
|
|
throw new common_1.NotFoundException('Plan no encontrado');
|
|
}
|
|
const subscription = await this.subscriptionRepo.findOne({ where: { tenantId } });
|
|
if (!subscription?.stripeCustomerId) {
|
|
throw new common_1.BadRequestException('Cliente de Stripe no configurado');
|
|
}
|
|
const session = await this.stripeService.createCheckoutSession({
|
|
customerId: subscription.stripeCustomerId,
|
|
priceId: plan.stripePriceIdMonthly,
|
|
mode: 'subscription',
|
|
successUrl,
|
|
cancelUrl,
|
|
metadata: { tenantId, planCode },
|
|
});
|
|
return { checkoutUrl: session.url };
|
|
}
|
|
async createTokenPurchaseCheckout(tenantId, packageCode, successUrl, cancelUrl) {
|
|
const tokenPackage = this.tokenPackages.find((p) => p.code === packageCode);
|
|
if (!tokenPackage) {
|
|
throw new common_1.NotFoundException('Paquete de tokens no encontrado');
|
|
}
|
|
const subscription = await this.subscriptionRepo.findOne({ where: { tenantId } });
|
|
if (!subscription?.stripeCustomerId) {
|
|
throw new common_1.BadRequestException('Cliente de Stripe no configurado');
|
|
}
|
|
const paymentIntent = await this.stripeService.createPaymentIntent({
|
|
amount: tokenPackage.priceMxn * 100,
|
|
customerId: subscription.stripeCustomerId,
|
|
metadata: {
|
|
tenantId,
|
|
packageCode,
|
|
tokens: tokenPackage.tokens.toString(),
|
|
},
|
|
});
|
|
return { checkoutUrl: paymentIntent.client_secret };
|
|
}
|
|
async createPortalSession(tenantId, returnUrl) {
|
|
const subscription = await this.subscriptionRepo.findOne({ where: { tenantId } });
|
|
if (!subscription?.stripeCustomerId) {
|
|
throw new common_1.BadRequestException('Cliente de Stripe no configurado');
|
|
}
|
|
const session = await this.stripeService.createPortalSession({
|
|
customerId: subscription.stripeCustomerId,
|
|
returnUrl,
|
|
});
|
|
return { portalUrl: session.url };
|
|
}
|
|
async handleSubscriptionCreated(stripeSubscriptionId, stripeCustomerId, stripePriceId) {
|
|
let plan = await this.planRepo.findOne({ where: { stripePriceIdMonthly: stripePriceId } });
|
|
if (!plan) {
|
|
plan = await this.planRepo.findOne({ where: { stripePriceIdYearly: stripePriceId } });
|
|
}
|
|
if (!plan) {
|
|
this.logger.warn(`Plan not found for price: ${stripePriceId}`);
|
|
return;
|
|
}
|
|
const subscription = await this.subscriptionRepo.findOne({
|
|
where: { stripeCustomerId },
|
|
});
|
|
if (subscription) {
|
|
subscription.planId = plan.id;
|
|
subscription.stripeSubscriptionId = stripeSubscriptionId;
|
|
subscription.status = subscription_entity_1.SubscriptionStatus.ACTIVE;
|
|
subscription.currentPeriodStart = new Date();
|
|
subscription.currentPeriodEnd = new Date(Date.now() + 30 * 24 * 60 * 60 * 1000);
|
|
await this.subscriptionRepo.save(subscription);
|
|
if (plan.includedTokens > 0) {
|
|
await this.addTokensToBalance(subscription.tenantId, plan.includedTokens, 'subscription');
|
|
}
|
|
this.logger.log(`Subscription activated for tenant: ${subscription.tenantId}`);
|
|
}
|
|
}
|
|
async handleSubscriptionCancelled(stripeSubscriptionId) {
|
|
const subscription = await this.subscriptionRepo.findOne({
|
|
where: { stripeSubscriptionId },
|
|
});
|
|
if (subscription) {
|
|
subscription.status = subscription_entity_1.SubscriptionStatus.CANCELLED;
|
|
subscription.cancelledAt = new Date();
|
|
await this.subscriptionRepo.save(subscription);
|
|
this.logger.log(`Subscription cancelled for tenant: ${subscription.tenantId}`);
|
|
}
|
|
}
|
|
async handleTokenPurchase(tenantId, packageCode, tokens) {
|
|
await this.addTokensToBalance(tenantId, tokens, 'purchase');
|
|
this.logger.log(`Added ${tokens} tokens to tenant: ${tenantId}`);
|
|
}
|
|
async getTokenBalance(tenantId) {
|
|
return this.tokenBalanceRepo.findOne({ where: { tenantId } });
|
|
}
|
|
async addTokensToBalance(tenantId, tokens, source) {
|
|
let balance = await this.tokenBalanceRepo.findOne({ where: { tenantId } });
|
|
if (!balance) {
|
|
balance = this.tokenBalanceRepo.create({
|
|
tenantId,
|
|
usedTokens: 0,
|
|
availableTokens: 0,
|
|
});
|
|
}
|
|
balance.availableTokens += tokens;
|
|
return this.tokenBalanceRepo.save(balance);
|
|
}
|
|
async consumeTokens(tenantId, tokens, action, description) {
|
|
const balance = await this.tokenBalanceRepo.findOne({ where: { tenantId } });
|
|
if (!balance || balance.availableTokens < tokens) {
|
|
return false;
|
|
}
|
|
balance.usedTokens += tokens;
|
|
balance.availableTokens -= tokens;
|
|
await this.tokenBalanceRepo.save(balance);
|
|
const usage = this.tokenUsageRepo.create({
|
|
tenantId,
|
|
tokensUsed: tokens,
|
|
action,
|
|
description,
|
|
});
|
|
await this.tokenUsageRepo.save(usage);
|
|
return true;
|
|
}
|
|
async getTokenUsageHistory(tenantId, limit = 50) {
|
|
return this.tokenUsageRepo.find({
|
|
where: { tenantId },
|
|
order: { createdAt: 'DESC' },
|
|
take: limit,
|
|
});
|
|
}
|
|
async getBillingSummary(tenantId) {
|
|
const subscription = await this.subscriptionRepo.findOne({
|
|
where: { tenantId },
|
|
relations: ['plan'],
|
|
});
|
|
const tokenBalance = await this.getTokenBalance(tenantId);
|
|
let invoices = [];
|
|
if (subscription?.stripeCustomerId) {
|
|
try {
|
|
invoices = await this.stripeService.listInvoices(subscription.stripeCustomerId, 5);
|
|
}
|
|
catch (error) {
|
|
this.logger.warn('Could not fetch invoices from Stripe');
|
|
}
|
|
}
|
|
return {
|
|
subscription,
|
|
plan: subscription?.plan || null,
|
|
tokenBalance,
|
|
invoices: invoices.map((inv) => ({
|
|
id: inv.id,
|
|
amount: inv.amount_paid / 100,
|
|
status: inv.status,
|
|
date: new Date(inv.created * 1000),
|
|
pdfUrl: inv.invoice_pdf,
|
|
})),
|
|
};
|
|
}
|
|
};
|
|
exports.BillingService = BillingService;
|
|
exports.BillingService = BillingService = BillingService_1 = __decorate([
|
|
(0, common_1.Injectable)(),
|
|
__param(1, (0, typeorm_1.InjectRepository)(subscription_entity_1.Subscription)),
|
|
__param(2, (0, typeorm_1.InjectRepository)(plan_entity_1.Plan)),
|
|
__param(3, (0, typeorm_1.InjectRepository)(token_balance_entity_1.TokenBalance)),
|
|
__param(4, (0, typeorm_1.InjectRepository)(token_usage_entity_1.TokenUsage)),
|
|
__metadata("design:paramtypes", [stripe_service_1.StripeService,
|
|
typeorm_2.Repository,
|
|
typeorm_2.Repository,
|
|
typeorm_2.Repository,
|
|
typeorm_2.Repository])
|
|
], BillingService);
|
|
//# sourceMappingURL=billing.service.js.map
|