"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.addPeriod = addPeriod; exports.getPlans = getPlans; exports.getProviders = getProviders; exports.getPlanFeatures = getPlanFeatures; exports.getSubscription = getSubscription; exports.getInvoices = getInvoices; exports.getEvents = getEvents; exports.getEventsBySubscriptionId = getEventsBySubscriptionId; exports.getEntitlement = getEntitlement; exports.startTrial = startTrial; exports.handleAmanpayWebhook = handleAmanpayWebhook; exports.handlePaypalWebhook = handlePaypalWebhook; exports.checkout = checkout; exports.capturePaypal = capturePaypal; exports.changePlan = changePlan; exports.cancel = cancel; exports.resume = resume; exports.reactivate = reactivate; exports.runTrialExpirationJob = runTrialExpirationJob; exports.runPaymentPendingTimeoutJob = runPaymentPendingTimeoutJob; exports.runPastDueTimeoutJob = runPastDueTimeoutJob; exports.runSuspensionTimeoutJob = runSuspensionTimeoutJob; exports.runPeriodEndCancellationJob = runPeriodEndCancellationJob; exports.adminExtendTrial = adminExtendTrial; exports.adminExtendGracePeriod = adminExtendGracePeriod; exports.adminCancelSubscription = adminCancelSubscription; exports.adminReactivateSubscription = adminReactivateSubscription; exports.adminSuspendSubscription = adminSuspendSubscription; const types_1 = require("@rentaldrivego/types"); const prisma_1 = require("../../lib/prisma"); const errors_1 = require("../../http/errors"); const amanpay = __importStar(require("../../services/amanpayService")); const paypal = __importStar(require("../../services/paypalService")); const repo = __importStar(require("./subscription.repo")); const subscription_policy_1 = require("./subscription.policy"); const webhookIdempotency_1 = require("../../security/webhookIdempotency"); // ─── Helpers ────────────────────────────────────────────────── function addPeriod(date, period) { const d = new Date(date); period === 'ANNUAL' ? d.setFullYear(d.getFullYear() + 1) : d.setMonth(d.getMonth() + 1); return d; } // ─── Plans & providers ──────────────────────────────────────── async function getPlans() { const configs = await prisma_1.prisma.pricingConfig.findMany(); if (configs.length === 0) return types_1.PLAN_PRICES; const result = {}; for (const c of configs) { if (!result[c.plan]) result[c.plan] = {}; result[c.plan][c.billingPeriod] = { MAD: c.amount }; } return result; } function getProviders() { return { amanpay: amanpay.isConfigured(), paypal: paypal.isConfigured() }; } function getPlanFeatures() { return prisma_1.prisma.planFeature.findMany({ orderBy: [{ plan: 'asc' }, { sortOrder: 'asc' }, { createdAt: 'asc' }], }); } // ─── Reads ──────────────────────────────────────────────────── function getSubscription(companyId) { return repo.findByCompany(companyId); } function getInvoices(companyId) { return repo.findInvoices(companyId); } function getEvents(companyId) { return repo.findByCompany(companyId).then((sub) => sub ? repo.getEvents(sub.id) : []); } function getEventsBySubscriptionId(subscriptionId) { return repo.getEvents(subscriptionId); } // ─── Entitlements ───────────────────────────────────────────── async function getEntitlement(companyId) { const sub = await repo.findByCompany(companyId); const status = sub?.status ?? 'EXPIRED'; const accessLevel = (0, subscription_policy_1.getAccessLevel)(status); return { companyId, subscriptionId: sub?.id ?? null, subscriptionStatus: status, accessLevel, plan: sub?.plan ?? null, currentPeriodEnd: sub?.currentPeriodEnd ?? null, }; } // ─── Trial management ───────────────────────────────────────── async function startTrial(companyId, plan, billingPeriod, currency) { if (subscription_policy_1.SUBSCRIPTION_POLICY.trial.oneTrialPerCompany) { const existing = await repo.findByCompany(companyId); if (existing?.trialUsed) { throw new errors_1.ValidationError('This company has already used its free trial'); } } const trialEndAt = new Date(Date.now() + subscription_policy_1.SUBSCRIPTION_POLICY.trial.durationDays * 24 * 60 * 60 * 1000); const sub = await repo.startTrial(companyId, plan, billingPeriod, currency, trialEndAt); await repo.createEvent({ subscriptionId: sub.id, companyId, eventType: 'trial.started', payload: { plan, billingPeriod, trialEndAt }, }); return sub; } // ─── Payment success (shared by all providers) ─────────────── async function handlePaymentSuccess(subscriptionId, invoiceId) { const sub = await repo.findById(subscriptionId); if (!sub) return; await repo.markInvoicePaid(invoiceId); await repo.createPaymentAttempt({ invoiceId, subscriptionId, status: 'succeeded', }); const periodEnd = addPeriod(new Date(), sub.billingPeriod); await repo.activateSubscription(subscriptionId, periodEnd); await repo.createEvent({ subscriptionId, companyId: sub.companyId, eventType: sub.status === 'TRIALING' ? 'trial.converted' : 'subscription.activated', source: 'webhook', payload: { invoiceId, periodEnd }, }); } // ─── Payment failure ────────────────────────────────────────── async function handlePaymentFailure(subscriptionId, invoiceId, failureCode, failureMessage) { const sub = await repo.findById(subscriptionId); if (!sub) return; await repo.markInvoiceFailed(invoiceId); await repo.createPaymentAttempt({ invoiceId, subscriptionId, status: 'failed', failureCode, failureMessage, }); await repo.incrementRetryCount(sub.id); if (sub.status !== 'PAYMENT_PENDING') { await repo.setPaymentPending(sub.id); await repo.createEvent({ subscriptionId: sub.id, companyId: sub.companyId, eventType: 'subscription.payment_pending', source: 'webhook', payload: { invoiceId, failureCode }, }); } } // ─── Webhook handlers ───────────────────────────────────────── async function applyAmanpayWebhook(event) { const transactionId = event.transaction_id ?? event.id; const status = event.status?.toUpperCase(); if (status === 'PAID' || status === 'SUCCEEDED') { const invoice = await repo.findInvoiceByAmanpay(transactionId); if (!invoice || invoice.status === 'PAID') return; // idempotent await handlePaymentSuccess(invoice.subscriptionId, invoice.id); } else if (status === 'FAILED' || status === 'DECLINED') { const invoice = await repo.findInvoiceByAmanpay(transactionId); if (!invoice || invoice.status === 'PAID') return; await handlePaymentFailure(invoice.subscriptionId, invoice.id, status, event.failure_reason); } } async function applyPaypalWebhook(event) { if (event.event_type === 'PAYMENT.CAPTURE.COMPLETED') { const captureId = event.resource?.id; const invoice = await repo.findInvoiceByPaypal(captureId); if (!invoice || invoice.status === 'PAID') return; // idempotent await handlePaymentSuccess(invoice.subscriptionId, invoice.id); } else if (event.event_type === 'PAYMENT.CAPTURE.DENIED') { const captureId = event.resource?.id; const invoice = await repo.findInvoiceByPaypal(captureId); if (!invoice || invoice.status === 'PAID') return; await handlePaymentFailure(invoice.subscriptionId, invoice.id, 'capture_denied'); } } async function handleAmanpayWebhook(event, rawBody = JSON.stringify(event)) { return (0, webhookIdempotency_1.processWebhookOnce)({ provider: 'amanpay:subscriptions', providerEventId: (0, webhookIdempotency_1.getWebhookEventId)('amanpay', event), eventType: String(event.status ?? 'unknown'), rawBody, handle: () => applyAmanpayWebhook(event), }); } async function handlePaypalWebhook(event, rawBody = JSON.stringify(event)) { return (0, webhookIdempotency_1.processWebhookOnce)({ provider: 'paypal:subscriptions', providerEventId: (0, webhookIdempotency_1.getWebhookEventId)('paypal', event), eventType: String(event.event_type ?? 'unknown'), rawBody, handle: () => applyPaypalWebhook(event), }); } // ─── Checkout ───────────────────────────────────────────────── async function checkout(companyId, body) { const dbPrice = await prisma_1.prisma.pricingConfig.findUnique({ where: { plan_billingPeriod: { plan: body.plan, billingPeriod: body.billingPeriod } }, }); const amount = dbPrice?.amount ?? types_1.PLAN_PRICES[body.plan]?.[body.billingPeriod]?.[body.currency]; if (!amount) throw new errors_1.ValidationError('Invalid plan or billing period'); const company = await prisma_1.prisma.company.findUniqueOrThrow({ where: { id: companyId } }); const subscription = await repo.findOrCreateSubscription(companyId, body.plan, body.billingPeriod, body.currency); const orderId = `sub-${companyId}-${Date.now()}`; const description = `${body.plan} plan — ${body.billingPeriod}`; const webhookBase = process.env.API_URL ?? 'http://localhost:4000'; let checkoutUrl; let amanpayTransactionId = null; let paypalCaptureId = null; if (body.provider === 'AMANPAY') { if (!amanpay.isConfigured()) throw new errors_1.ValidationError('AmanPay is not configured on this platform'); const result = await amanpay.createCheckout({ amount, currency: body.currency, orderId, description, customerEmail: company.email, customerName: company.name, successUrl: body.successUrl, failureUrl: body.failureUrl, webhookUrl: `${webhookBase}/api/v1/subscriptions/webhooks/amanpay`, }); checkoutUrl = result.checkoutUrl; amanpayTransactionId = result.transactionId; } else { if (!paypal.isConfigured()) throw new errors_1.ValidationError('PayPal is not configured on this platform'); const result = await paypal.createOrder({ amount, currency: body.currency, orderId, description, returnUrl: body.successUrl, cancelUrl: body.failureUrl, }); checkoutUrl = result.approveUrl; paypalCaptureId = result.orderId; } const dueAt = new Date(Date.now() + subscription_policy_1.SUBSCRIPTION_POLICY.payment.paymentPendingTimeoutDays * 24 * 60 * 60 * 1000); const invoice = await repo.createInvoice({ companyId, subscriptionId: subscription.id, amount, currency: body.currency, paymentProvider: body.provider, amanpayTransactionId, paypalCaptureId, dueAt, }); return { invoice, checkoutUrl }; } async function capturePaypal(companyId, paypalOrderId) { const invoice = await repo.findInvoiceByPaypalForCompany(paypalOrderId, companyId); if (invoice.status === 'PAID') return { success: true }; // idempotent const capture = await paypal.captureOrder(paypalOrderId); const captureId = capture.purchase_units?.[0]?.payments?.captures?.[0]?.id ?? paypalOrderId; await repo.updateInvoicePaypal(invoice.id, captureId); const periodEnd = addPeriod(new Date(), invoice.subscription.billingPeriod); await repo.activateSubscription(invoice.subscriptionId, periodEnd); await repo.createEvent({ subscriptionId: invoice.subscriptionId, companyId, eventType: 'subscription.activated', source: 'user', payload: { invoiceId: invoice.id }, }); return { success: true }; } // ─── Plan changes ───────────────────────────────────────────── function changePlan(companyId, data) { return repo.updatePlan(companyId, data); } // ─── Cancellation ───────────────────────────────────────────── async function cancel(companyId, mode = 'period_end', reason) { const sub = await repo.findByCompany(companyId); if (!sub) throw new errors_1.ValidationError('No subscription found'); const updated = await repo.setCancelled(sub.id, mode === 'immediate'); await repo.createEvent({ subscriptionId: sub.id, companyId, eventType: 'subscription.canceled', source: 'user', payload: { mode, reason: reason ?? null }, }); return updated; } async function resume(companyId) { const sub = await repo.findByCompany(companyId); if (!sub) throw new errors_1.ValidationError('No subscription found'); const updated = await repo.setCancelAtPeriodEnd(companyId, false); await repo.createEvent({ subscriptionId: sub.id, companyId, eventType: 'subscription.resumed', source: 'user', payload: {}, }); return updated; } // ─── Reactivation ──────────────────────────────────────────── async function reactivate(companyId, body) { const sub = await repo.findByCompany(companyId); if (!sub) throw new errors_1.ValidationError('No subscription found'); const allowedStatuses = ['CANCELLED', 'EXPIRED', 'SUSPENDED', 'PAST_DUE', 'PAYMENT_PENDING']; if (!allowedStatuses.includes(sub.status)) { throw new errors_1.ValidationError(`Cannot reactivate a subscription with status ${sub.status}`); } await repo.setPaymentPending(sub.id); await repo.createEvent({ subscriptionId: sub.id, companyId, eventType: 'subscription.reactivated', source: 'user', payload: { plan: body.plan, billingPeriod: body.billingPeriod }, }); return checkout(companyId, body); } // ─── Scheduled job actions ──────────────────────────────────── async function runTrialExpirationJob() { const expired = await repo.findTrialsEndedWithoutConversion(); for (const sub of expired) { await repo.setExpired(sub.id); await repo.createEvent({ subscriptionId: sub.id, companyId: sub.companyId, eventType: 'trial.expired', payload: { trialEndAt: sub.trialEndAt }, }); } return expired.length; } async function runPaymentPendingTimeoutJob() { const cutoff = new Date(Date.now() - subscription_policy_1.SUBSCRIPTION_POLICY.payment.paymentPendingTimeoutDays * 24 * 60 * 60 * 1000); const subs = await repo.findPaymentPendingTimedOut(cutoff); for (const sub of subs) { await repo.setPastDue(sub.id); await repo.createEvent({ subscriptionId: sub.id, companyId: sub.companyId, eventType: 'subscription.past_due', payload: { paymentPendingSince: sub.paymentPendingSince }, }); } return subs.length; } async function runPastDueTimeoutJob() { const cutoff = new Date(Date.now() - subscription_policy_1.SUBSCRIPTION_POLICY.pastDue.timeoutDays * 24 * 60 * 60 * 1000); const subs = await repo.findPastDueTimedOut(cutoff); for (const sub of subs) { await repo.setSuspended(sub.id); await repo.createEvent({ subscriptionId: sub.id, companyId: sub.companyId, eventType: 'subscription.suspended', payload: { pastDueSince: sub.pastDueSince }, }); } return subs.length; } async function runSuspensionTimeoutJob() { const cutoff = new Date(Date.now() - subscription_policy_1.SUBSCRIPTION_POLICY.suspension.cancelAfterDays * 24 * 60 * 60 * 1000); const subs = await repo.findSuspendedTimedOut(cutoff); for (const sub of subs) { await repo.setCancelled(sub.id, true); await repo.createEvent({ subscriptionId: sub.id, companyId: sub.companyId, eventType: 'subscription.canceled', payload: { reason: 'suspension_timeout', suspendedAt: sub.suspendedAt }, }); } return subs.length; } async function runPeriodEndCancellationJob() { const subs = await repo.findSubscriptionsToExpireAtPeriodEnd(); for (const sub of subs) { await repo.setCancelled(sub.id, true); await repo.createEvent({ subscriptionId: sub.id, companyId: sub.companyId, eventType: 'subscription.canceled', payload: { reason: 'period_end', currentPeriodEnd: sub.currentPeriodEnd }, }); } return subs.length; } // ─── Admin overrides ────────────────────────────────────────── async function adminExtendTrial(subscriptionId, extraDays, adminId, reason) { const sub = await repo.findById(subscriptionId); if (!sub) throw new errors_1.ValidationError('Subscription not found'); const currentEnd = sub.trialEndAt ?? new Date(); const newEnd = new Date(currentEnd.getTime() + extraDays * 24 * 60 * 60 * 1000); await prisma_1.prisma.subscription.update({ where: { id: subscriptionId }, data: { trialEndAt: newEnd }, }); await repo.createEvent({ subscriptionId, companyId: sub.companyId, eventType: 'admin.override_created', source: 'admin', payload: { action: 'extend_trial', extraDays, newEnd, adminId, reason }, }); return { trialEndAt: newEnd }; } async function adminExtendGracePeriod(subscriptionId, extraDays, adminId, reason) { const sub = await repo.findById(subscriptionId); if (!sub) throw new errors_1.ValidationError('Subscription not found'); await repo.setPaymentPending(sub.id); await repo.createEvent({ subscriptionId, companyId: sub.companyId, eventType: 'admin.override_created', source: 'admin', payload: { action: 'extend_grace_period', extraDays, adminId, reason }, }); return { success: true }; } async function adminCancelSubscription(subscriptionId, adminId, reason) { const sub = await repo.findById(subscriptionId); if (!sub) throw new errors_1.ValidationError('Subscription not found'); await repo.setCancelled(sub.id, true); await repo.createEvent({ subscriptionId, companyId: sub.companyId, eventType: 'subscription.canceled', source: 'admin', payload: { adminId, reason }, }); return { success: true }; } async function adminReactivateSubscription(subscriptionId, adminId, reason) { const sub = await repo.findById(subscriptionId); if (!sub) throw new errors_1.ValidationError('Subscription not found'); const periodEnd = addPeriod(new Date(), sub.billingPeriod); await repo.activateSubscription(subscriptionId, periodEnd); await repo.createEvent({ subscriptionId, companyId: sub.companyId, eventType: 'subscription.reactivated', source: 'admin', payload: { adminId, reason, periodEnd }, }); return { success: true }; } async function adminSuspendSubscription(subscriptionId, adminId, reason) { const sub = await repo.findById(subscriptionId); if (!sub) throw new errors_1.ValidationError('Subscription not found'); await repo.setSuspended(sub.id); await repo.createEvent({ subscriptionId, companyId: sub.companyId, eventType: 'subscription.suspended', source: 'admin', payload: { adminId, reason }, }); return { success: true }; } //# sourceMappingURL=subscription.service.js.map