diff --git a/app/api/_lib/polar/services/polar-webhook-processor.service.ts b/app/api/_lib/polar/services/polar-webhook-processor.service.ts index 9af9bede..7b488c8c 100644 --- a/app/api/_lib/polar/services/polar-webhook-processor.service.ts +++ b/app/api/_lib/polar/services/polar-webhook-processor.service.ts @@ -16,7 +16,9 @@ import { UserRepository } from '@/app/api/_lib/polar/repositories/user.repository'; import { SubscriptionRepository } from '@/app/api/_lib/polar/repositories/subscription.repository'; +import { PolarService } from '@/app/api/_lib/polar/services/polar.service'; import { PlanType, SubscriptionProcessResult } from '@/app/api/_lib/polar/types'; +import { extractPlanPrice } from '@/app/api/_lib/polar/utils/price-extractor.util'; /** * Servicio para procesar datos de webhooks de Polar @@ -34,9 +36,28 @@ export class PolarWebhookProcessorService { */ async processSubscriptionWithData(subscriptionId: string, polarData: any): Promise { try { - const userId = polarData.customerExternalId || polarData.customer?.externalId; + // Manejar tanto el campo antiguo como el nuevo + const userId = + polarData.customerExternalId || + polarData.customer?.externalId || + polarData.customer?.external_customer_id || + polarData.customer_external_id || + polarData.external_customer_id; - if (!userId) { + // Si no encontramos el external_id, intentar obtenerlo del customer_id + let finalUserId = userId; + if (!finalUserId && polarData.customer_id) { + console.log(`external_id not found in webhook payload, fetching from customer_id: ${polarData.customer_id}`); + try { + const polarService = new PolarService(process.env.POLAR_ACCESS_TOKEN || ''); + const customer = await polarService.getCustomer(polarData.customer_id); + finalUserId = customer?.externalId || ''; + } catch (error) { + console.error(`Error fetching customer ${polarData.customer_id}:`, error); + } + } + + if (!finalUserId) { return { success: false, userId: '', @@ -47,14 +68,14 @@ export class PolarWebhookProcessorService { // Determinar acción basada en el estado de la suscripción if (this.isSubscriptionActiveFromData(polarData)) { - return await this.activateSubscriptionWithData(userId, polarData); + return await this.activateSubscriptionWithData(finalUserId, polarData); } else if (this.isSubscriptionCanceledFromData(polarData)) { - return await this.cancelSubscriptionWithData(userId, polarData); + return await this.cancelSubscriptionWithData(finalUserId, polarData); } return { success: true, - userId, + userId: finalUserId, plan: PlanType.FREE, message: 'Subscription processed successfully', }; @@ -110,7 +131,7 @@ export class PolarWebhookProcessorService { const nextPaymentDate = polarData.currentPeriodEnd ? new Date(polarData.currentPeriodEnd).toISOString() : undefined; - const planPrice = polarData.amount ? polarData.amount / 100 : undefined; + const planPrice = extractPlanPrice(polarData); await this.subscriptionRepository.update(userId, { nextPaymentDate, @@ -235,7 +256,7 @@ export class PolarWebhookProcessorService { */ private extractSubscriptionDataFromPolar(userId: string, polarData: any, plan: PlanType): any { const nextPaymentDate = polarData.currentPeriodEnd ? new Date(polarData.currentPeriodEnd).toISOString() : undefined; - const planPrice = polarData.amount ? polarData.amount / 100 : undefined; // Convertir de centavos a dólares + const planPrice = extractPlanPrice(polarData); // Convertir de centavos a dólares return { id: userId, diff --git a/app/api/_lib/polar/services/polar.service.ts b/app/api/_lib/polar/services/polar.service.ts index fc1d640e..08bd6c60 100644 --- a/app/api/_lib/polar/services/polar.service.ts +++ b/app/api/_lib/polar/services/polar.service.ts @@ -140,11 +140,19 @@ export class PolarService { * Mapea la respuesta de Polar a nuestro tipo PolarSubscription */ private mapToPolarSubscription(polarResponse: any): PolarSubscription { + // Manejar tanto el campo antiguo como el nuevo + const customerExternalId = + polarResponse.customer?.externalId || + polarResponse.customer?.external_customer_id || + polarResponse.customer_external_id || + polarResponse.external_customer_id || + ''; + return { id: polarResponse.id, status: polarResponse.status as SubscriptionStatus, customerId: polarResponse.customer?.id || '', - customerExternalId: polarResponse.customer?.externalId || '', + customerExternalId, productId: polarResponse.productId || '', amount: polarResponse.amount || 0, currentPeriodEnd: polarResponse.currentPeriodEnd ? new Date(polarResponse.currentPeriodEnd) : new Date(), diff --git a/app/api/_lib/polar/services/subscription.service.ts b/app/api/_lib/polar/services/subscription.service.ts index c566506f..dab10b49 100644 --- a/app/api/_lib/polar/services/subscription.service.ts +++ b/app/api/_lib/polar/services/subscription.service.ts @@ -18,6 +18,7 @@ import { UserRepository } from '@/app/api/_lib/polar/repositories/user.repositor import { SubscriptionRepository } from '@/app/api/_lib/polar/repositories/subscription.repository'; import { PolarService } from '@/app/api/_lib/polar/services/polar.service'; import { PlanType, SubscriptionProcessResult, ProductConfig, ProductPlanMapping } from '@/app/api/_lib/polar/types'; +import { extractPlanPrice } from '@/app/api/_lib/polar/utils/price-extractor.util'; /** * Servicio de aplicación para lógica de negocio de suscripciones @@ -123,7 +124,7 @@ export class SubscriptionService { const nextPaymentDate = polarSubscription?.currentPeriodEnd ? new Date(polarSubscription.currentPeriodEnd).toISOString() : undefined; - const planPrice = polarSubscription?.amount ? polarSubscription.amount / 100 : undefined; + const planPrice = extractPlanPrice(polarSubscription); await this.subscriptionRepository.update(userId, { nextPaymentDate, @@ -147,7 +148,7 @@ export class SubscriptionService { const nextPaymentDate = polarSubscription?.currentPeriodEnd ? new Date(polarSubscription.currentPeriodEnd).toISOString() : undefined; - const planPrice = polarSubscription?.amount ? polarSubscription.amount / 100 : undefined; + const planPrice = extractPlanPrice(polarSubscription); await this.subscriptionRepository.upsert({ id: userId, diff --git a/app/api/_lib/polar/utils/price-extractor.util.ts b/app/api/_lib/polar/utils/price-extractor.util.ts new file mode 100644 index 00000000..6ee87ca9 --- /dev/null +++ b/app/api/_lib/polar/utils/price-extractor.util.ts @@ -0,0 +1,34 @@ +/** + * Utilidad para extraer precios de los payloads de Polar + * Centraliza la lógica de extracción de precios para evitar duplicación + */ + +/** + * Extrae el precio del plan desde los datos de Polar + * Busca en múltiples ubicaciones basándose en los payloads reales de Polar + */ +export function extractPlanPrice(polarData: any): number | undefined { + // Basándose en los payloads reales de Polar: + // - subscription.updated: price.price_amount + // - order.paid: product_price.price_amount + // - subscription.revoked: amount + + const priceSources = [ + polarData?.amount, // Campo directo amount (subscription.revoked) + polarData?.price?.price_amount, // Dentro del objeto price (subscription.updated) + polarData?.product_price?.price_amount, // Dentro de product_price (order.paid) + polarData?.prices?.[0]?.price_amount, // Primer elemento del array prices + ]; + + // Buscar el primer valor válido (no null, undefined, o 0) + for (const price of priceSources) { + if (price && price > 0) { + const finalPrice = price / 100; // Convertir de centavos a dólares + console.log(`Found plan price: $${finalPrice} from source: ${price}`); + return finalPrice; + } + } + + console.warn(`No valid price found in Polar data. Sources checked:`, priceSources); + return undefined; +} diff --git a/app/store/hooks/useSubscriptionLogic.ts b/app/store/hooks/useSubscriptionLogic.ts index 83371858..04308fcf 100644 --- a/app/store/hooks/useSubscriptionLogic.ts +++ b/app/store/hooks/useSubscriptionLogic.ts @@ -58,10 +58,7 @@ export function useSubscriptionLogic(userId?: string): UseSubscriptionLogicResul subscription.subscriptionId && !subscription.subscriptionId.startsWith('trial-') ); - // Verificar que tiene planPrice mayor a 0 - const hasValidPrice = (subscription.planPrice ?? 0) > 0; - - return hasValidSubscriptionId && hasValidPrice; + return hasValidSubscriptionId; }, [subscription]); // Determinar si es plan pagado basado en el precio del plan diff --git a/jest.setup.ts b/jest.setup.ts index 191e0416..c2f52b21 100644 --- a/jest.setup.ts +++ b/jest.setup.ts @@ -6,6 +6,19 @@ process.env.DEV_CACHE_ENABLED = 'true'; global.console.warn = jest.fn(); +// Polyfill para Request y Response (necesario para el SDK de Polar) +global.Request = + global.Request || + class Request { + constructor(_input: string | Request, _init?: RequestInit) {} + }; + +global.Response = + global.Response || + class Response { + constructor(_body?: BodyInit | null, _init?: ResponseInit) {} + }; + Object.defineProperty(window, 'matchMedia', { writable: true, value: jest.fn().mockImplementation((query) => ({ diff --git a/middlewares/store-access/store.ts b/middlewares/store-access/store.ts index a78e5fa7..d155c1cd 100644 --- a/middlewares/store-access/store.ts +++ b/middlewares/store-access/store.ts @@ -60,6 +60,7 @@ export async function handleStoreMiddleware(request: NextRequest, response: Next return authResponse; // Si hay redirección de auth, retornarla } + // Obtener la sesión que ya fue validada en handleAuthenticationMiddleware const session = await getSession(request, response); const userId = (session as AuthSession).tokens?.idToken?.payload?.['cognito:username'];