Some checks are pending
Bidi Control Character Guard / bidi-control-guard (push) Waiting to run
Circular Dependency Check / Check for new circular dependencies (push) Waiting to run
Citus Migration Smoke / Combined migrations on single-node Citus (push) Waiting to run
E2E Fresh Install Tests / fresh-install-e2e (push) Waiting to run
ext-v2 guardrails / Run ext-v2 guard and ESLint (push) Waiting to run
Integration Tests / Check for relevant changes (push) Waiting to run
Integration Tests / ${{ (github.event_name == 'schedule' || github.event.inputs.suite == 'full') && 'Full integration suite' || 'Tier-1 integration subset' }} (push) Blocked by required conditions
Mobile checks / Mobile lint + typecheck (push) Waiting to run
Mobile checks / Mobile unit tests (push) Waiting to run
Mobile checks / Mobile dependency audit (report) (push) Waiting to run
Mobile checks / Mobile reproducibility checks (push) Waiting to run
Secrets guard (env backups) / Ensure no tracked env backup files (push) Waiting to run
Temporal Readiness / fast-readiness (push) Waiting to run
Temporal Readiness / docker-parity (push) Waiting to run
TypeScript Type Check / Nx affected typecheck (push) Waiting to run
Unit Tests / Skipped-test budget (push) Waiting to run
Unit Tests / Nx affected unit tests (push) Waiting to run
Unit Tests / Server unit coverage (informational) (push) Waiting to run
Validate Tenant Management Schema / Check for relevant changes (push) Waiting to run
Validate Tenant Management Schema / Validate Tenant Management Schema (push) Blocked by required conditions
EE Workflows Build Guard / ee-workflows-build-guard (push) Waiting to run
Excluded: .git, node_modules, secrets/, compose.env, assemblyscript tgz Source: /opt/alga-psa on psa.joliet.tech
165 lines
5.7 KiB
TypeScript
165 lines
5.7 KiB
TypeScript
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
|
|
const TENANT_ID = '22222222-2222-4222-8222-222222222222';
|
|
const QUOTE_ID = '33333333-3333-4333-8333-333333333333';
|
|
const USER_ID = '11111111-1111-4111-8111-111111111111';
|
|
|
|
const createTenantKnex = vi.fn();
|
|
const mapDbQuoteToViewModel = vi.fn();
|
|
const resolveQuoteTemplateAst = vi.fn();
|
|
const quoteGetById = vi.fn();
|
|
const uploadMock = vi.fn();
|
|
const createFileStoreMock = vi.fn();
|
|
const getBrowserMock = vi.fn();
|
|
const releaseBrowserMock = vi.fn();
|
|
|
|
vi.mock('@alga-psa/db', () => ({
|
|
createTenantKnex: (...args: any[]) => createTenantKnex(...args),
|
|
runWithTenant: async (_tenant: string, fn: () => Promise<unknown>) => fn(),
|
|
}));
|
|
|
|
vi.mock('../../src/lib/adapters/quoteAdapters', () => ({
|
|
mapDbQuoteToViewModel: (...args: any[]) => mapDbQuoteToViewModel(...args),
|
|
}));
|
|
|
|
vi.mock('../../src/lib/quote-template-ast/templateSelection', () => ({
|
|
resolveQuoteTemplateAst: (...args: any[]) => resolveQuoteTemplateAst(...args),
|
|
}));
|
|
|
|
vi.mock('../../src/models/quote', () => ({
|
|
default: {
|
|
getById: (...args: any[]) => quoteGetById(...args),
|
|
},
|
|
}));
|
|
|
|
vi.mock('@alga-psa/storage', () => ({
|
|
StorageProviderFactory: {
|
|
createProvider: async () => ({
|
|
upload: (...args: any[]) => uploadMock(...args),
|
|
}),
|
|
},
|
|
generateStoragePath: (...parts: string[]) => parts.join('/'),
|
|
FileStoreModel: {
|
|
create: (...args: any[]) => createFileStoreMock(...args),
|
|
findById: vi.fn().mockResolvedValue(null),
|
|
},
|
|
}));
|
|
|
|
vi.mock('../../src/services/browserPoolService', () => ({
|
|
browserPoolService: {
|
|
getBrowser: (...args: any[]) => getBrowserMock(...args),
|
|
releaseBrowser: (...args: any[]) => releaseBrowserMock(...args),
|
|
},
|
|
}));
|
|
|
|
import { createPDFGenerationService } from '../../src/services/pdfGenerationService';
|
|
import { getStandardQuoteTemplateAstByCode } from '../../src/lib/quote-template-ast/standardTemplates';
|
|
|
|
describe('quotePdfGenerationService', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
|
|
const pageMock = {
|
|
setContent: vi.fn().mockResolvedValue(undefined),
|
|
pdf: vi.fn().mockResolvedValue(Buffer.from('%PDF-quote-test')),
|
|
close: vi.fn().mockResolvedValue(undefined),
|
|
};
|
|
const browserMock = {
|
|
newPage: vi.fn().mockResolvedValue(pageMock),
|
|
};
|
|
|
|
getBrowserMock.mockResolvedValue(browserMock);
|
|
releaseBrowserMock.mockResolvedValue(undefined);
|
|
createTenantKnex.mockResolvedValue({ knex: { scope: 'knex' }, tenant: TENANT_ID });
|
|
mapDbQuoteToViewModel.mockResolvedValue({
|
|
quote_id: QUOTE_ID,
|
|
quote_number: 'Q-0042',
|
|
title: 'Proposal',
|
|
description: 'Managed services',
|
|
scope_of_work: 'Managed services',
|
|
quote_date: '2026-03-13T00:00:00.000Z',
|
|
valid_until: '2026-03-20T00:00:00.000Z',
|
|
status: 'draft',
|
|
version: 1,
|
|
po_number: null,
|
|
currency_code: 'USD',
|
|
subtotal: 1000,
|
|
discount_total: 0,
|
|
tax: 0,
|
|
total_amount: 1000,
|
|
terms_and_conditions: 'Net 30',
|
|
client_notes: null,
|
|
client_id: 'client-1',
|
|
contact_id: null,
|
|
client: { name: 'Client', address: null, email: null, phone: null, logo_url: null },
|
|
contact: null,
|
|
tenant: { name: 'Tenant', address: null, email: null, phone: null, logo_url: null },
|
|
line_items: [
|
|
{
|
|
quote_item_id: 'item-1',
|
|
service_id: null,
|
|
service_name: null,
|
|
service_sku: null,
|
|
billing_method: 'fixed',
|
|
description: 'Managed services',
|
|
quantity: 1,
|
|
unit_price: 1000,
|
|
total_price: 1000,
|
|
tax_amount: 0,
|
|
net_amount: 1000,
|
|
unit_of_measure: null,
|
|
phase: null,
|
|
is_optional: false,
|
|
is_selected: true,
|
|
is_recurring: false,
|
|
billing_frequency: null,
|
|
is_discount: false,
|
|
discount_type: null,
|
|
discount_percentage: null,
|
|
applies_to_item_id: null,
|
|
applies_to_service_id: null,
|
|
tax_region: null,
|
|
tax_rate: null,
|
|
},
|
|
],
|
|
phases: [],
|
|
});
|
|
resolveQuoteTemplateAst.mockResolvedValue({
|
|
templateAst: getStandardQuoteTemplateAstByCode('standard-quote-default'),
|
|
source: 'standard-fallback',
|
|
standardCode: 'standard-quote-default',
|
|
});
|
|
quoteGetById.mockResolvedValue({ quote_id: QUOTE_ID, quote_number: 'Q-0042' });
|
|
uploadMock.mockResolvedValue({ path: 'stored/pdfs/Q-0042.pdf' });
|
|
createFileStoreMock.mockResolvedValue({ file_id: 'file-1', storage_path: 'stored/pdfs/Q-0042.pdf' });
|
|
});
|
|
|
|
it('T083: generates a valid PDF buffer from quote data', async () => {
|
|
const service = createPDFGenerationService(TENANT_ID);
|
|
const pdf = await service.generatePDF({ quoteId: QUOTE_ID, userId: USER_ID });
|
|
|
|
expect(Buffer.isBuffer(pdf)).toBe(true);
|
|
expect(pdf.toString('utf8')).toContain('%PDF-quote-test');
|
|
const browser = await getBrowserMock.mock.results[0].value;
|
|
const page = await browser.newPage.mock.results[0].value;
|
|
expect(page.setContent).toHaveBeenCalledWith(expect.stringContaining('<!doctype html>'), { waitUntil: 'load' });
|
|
});
|
|
|
|
it('T084: stores generated file in file storage and returns file_id', async () => {
|
|
const service = createPDFGenerationService(TENANT_ID);
|
|
const result = await service.generateAndStore({ quoteId: QUOTE_ID, quoteNumber: 'Q-0042', userId: USER_ID });
|
|
|
|
expect(uploadMock).toHaveBeenCalledWith(expect.any(Buffer), '22222222-2222-4222-8222-222222222222/pdfs/Q-0042.pdf', {
|
|
mime_type: 'application/pdf',
|
|
});
|
|
expect(createFileStoreMock).toHaveBeenCalledWith(
|
|
expect.anything(),
|
|
expect.objectContaining({
|
|
original_name: 'Q-0042.pdf',
|
|
uploaded_by_id: USER_ID,
|
|
})
|
|
);
|
|
expect(result).toMatchObject({ file_id: 'file-1' });
|
|
});
|
|
});
|