diff --git a/src/lib/integrations/facebook/webhook-manager.ts b/src/lib/integrations/facebook/webhook-manager.ts index 9341b2c5..48a05a13 100644 --- a/src/lib/integrations/facebook/webhook-manager.ts +++ b/src/lib/integrations/facebook/webhook-manager.ts @@ -15,6 +15,7 @@ import { createFacebookClient, FacebookAPIError } from './graph-api-client'; import { FACEBOOK_CONFIG } from './constants'; +import * as crypto from 'crypto'; import { decrypt } from './encryption'; import { prisma } from '@/lib/prisma'; @@ -318,7 +319,7 @@ export class WebhookManager { verifyToken: string ): Promise { try { - const challenge = Math.random().toString(36).substring(7); + const challenge = crypto.randomBytes(8).toString('hex'); const url = new URL(callbackUrl); url.searchParams.set('hub.mode', 'subscribe'); url.searchParams.set('hub.challenge', challenge); diff --git a/src/lib/subscription/billing-service.ts b/src/lib/subscription/billing-service.ts index ef65d0c5..69da6bbb 100644 --- a/src/lib/subscription/billing-service.ts +++ b/src/lib/subscription/billing-service.ts @@ -7,6 +7,7 @@ import { prisma } from '@/lib/prisma'; import { getAppUrl } from '@/lib/utils'; +import * as crypto from 'crypto'; import { getRemainingDays, getExpiryDate, isExpiringSoon } from './state-machine'; import { getEffectiveFeatureLimits, getUsageStats } from './feature-enforcer'; import { getPaymentGateway } from './payment-gateway'; @@ -746,13 +747,14 @@ function getRetryDelay(retryCount: number): number { return delays[Math.min(retryCount, delays.length - 1)]; } -async function createInvoiceForPayment( +export async function createInvoiceForPayment( subscriptionId: string, amount: number, periodStart: Date, periodEnd: Date ): Promise { - const invoiceNumber = `INV-${Date.now()}-${Math.random().toString(36).slice(2, 7).toUpperCase()}`; + const randomPart = crypto.randomBytes(4).toString('base64url').toUpperCase().slice(0, 5); + const invoiceNumber = `INV-${Date.now()}-${randomPart}`; await prisma.invoice.create({ data: { diff --git a/src/test/services/billing-service.test.ts b/src/test/services/billing-service.test.ts new file mode 100644 index 00000000..d5f579a4 --- /dev/null +++ b/src/test/services/billing-service.test.ts @@ -0,0 +1,56 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { prisma } from '@/lib/prisma'; +import { createInvoiceForPayment } from '@/lib/subscription/billing-service'; + +// Mock prisma +vi.mock('@/lib/prisma', () => ({ + prisma: { + invoice: { + create: vi.fn().mockResolvedValue({ id: 'inv_123' }), + }, + }, +})); + +describe('BillingService', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + describe('createInvoiceForPayment', () => { + it('should generate an invoice number with the correct format', async () => { + const subscriptionId = 'sub_123'; + const amount = 1000; + const periodStart = new Date(); + const periodEnd = new Date(); + + await createInvoiceForPayment(subscriptionId, amount, periodStart, periodEnd); + + expect(prisma.invoice.create).toHaveBeenCalledTimes(1); + const call = vi.mocked(prisma.invoice.create).mock.calls[0][0]; + const invoiceNumber = call.data.invoiceNumber; + + // Pattern: INV-TIMESTAMP-RANDOM + // INV- (4 chars) + // TIMESTAMP (digits) + // - (1 char) + // RANDOM (5 chars ALPHANUMERIC-ish) + expect(invoiceNumber).toMatch(/^INV-\d+-[0-9A-Z_-]{5}$/); + }); + + it('should generate different invoice numbers for each call', async () => { + const subscriptionId = 'sub_123'; + const amount = 1000; + const periodStart = new Date(); + const periodEnd = new Date(); + + await createInvoiceForPayment(subscriptionId, amount, periodStart, periodEnd); + await createInvoiceForPayment(subscriptionId, amount, periodStart, periodEnd); + + expect(prisma.invoice.create).toHaveBeenCalledTimes(2); + const firstCall = vi.mocked(prisma.invoice.create).mock.calls[0][0]; + const secondCall = vi.mocked(prisma.invoice.create).mock.calls[1][0]; + + expect(firstCall.data.invoiceNumber).not.toBe(secondCall.data.invoiceNumber); + }); + }); +});