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
136 lines
5.9 KiB
TypeScript
136 lines
5.9 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
||
import { TEMPLATE_AST_VERSION } from '@alga-psa/types';
|
||
|
||
import {
|
||
QUOTE_TEMPLATE_VALUE_BINDINGS,
|
||
QUOTE_TEMPLATE_COLLECTION_BINDINGS,
|
||
buildQuoteTemplateBindings,
|
||
} from '../../src/lib/quote-template-ast/bindings';
|
||
|
||
import {
|
||
STANDARD_QUOTE_TEMPLATE_ASTS,
|
||
getStandardQuoteTemplateAstByCode,
|
||
} from '../../src/lib/quote-template-ast/standardTemplates';
|
||
|
||
// ── bindings ─────────────────────────────────────────────────────────
|
||
describe('quote-template-ast – bindings', () => {
|
||
it('T230: value bindings include all expected quote fields', () => {
|
||
const requiredIds = [
|
||
'quoteNumber', 'quoteDate', 'validUntil', 'status', 'title', 'scope',
|
||
'subtotal', 'discountTotal', 'tax', 'total',
|
||
'termsAndConditions', 'clientNotes', 'version',
|
||
'clientName', 'clientAddress', 'contactName',
|
||
'tenantName', 'tenantAddress', 'tenantLogo',
|
||
];
|
||
|
||
for (const id of requiredIds) {
|
||
expect(QUOTE_TEMPLATE_VALUE_BINDINGS).toHaveProperty(id);
|
||
expect(QUOTE_TEMPLATE_VALUE_BINDINGS[id].kind).toBe('value');
|
||
expect(QUOTE_TEMPLATE_VALUE_BINDINGS[id].path).toBeTruthy();
|
||
}
|
||
});
|
||
|
||
it('T231: collection bindings include lineItems and phases', () => {
|
||
expect(QUOTE_TEMPLATE_COLLECTION_BINDINGS.lineItems).toBeDefined();
|
||
expect(QUOTE_TEMPLATE_COLLECTION_BINDINGS.lineItems.kind).toBe('collection');
|
||
expect(QUOTE_TEMPLATE_COLLECTION_BINDINGS.lineItems.path).toBe('line_items');
|
||
|
||
expect(QUOTE_TEMPLATE_COLLECTION_BINDINGS.phases).toBeDefined();
|
||
expect(QUOTE_TEMPLATE_COLLECTION_BINDINGS.phases.kind).toBe('collection');
|
||
expect(QUOTE_TEMPLATE_COLLECTION_BINDINGS.phases.path).toBe('phases');
|
||
});
|
||
|
||
it('T232: buildQuoteTemplateBindings returns values and collections', () => {
|
||
const bindings = buildQuoteTemplateBindings();
|
||
expect(bindings.values).toBeDefined();
|
||
expect(bindings.collections).toBeDefined();
|
||
expect(Object.keys(bindings.values!).length).toBeGreaterThan(0);
|
||
expect(Object.keys(bindings.collections!)).toEqual(
|
||
Object.keys(QUOTE_TEMPLATE_COLLECTION_BINDINGS),
|
||
);
|
||
});
|
||
|
||
it('T233: value bindings provide fallbacks for display fields', () => {
|
||
expect(QUOTE_TEMPLATE_VALUE_BINDINGS.clientName.fallback).toBe('Client');
|
||
expect(QUOTE_TEMPLATE_VALUE_BINDINGS.tenantName.fallback).toBe('Your Company');
|
||
expect(QUOTE_TEMPLATE_VALUE_BINDINGS.scope.fallback).toBe('');
|
||
expect(QUOTE_TEMPLATE_VALUE_BINDINGS.termsAndConditions.fallback).toBe('');
|
||
expect(QUOTE_TEMPLATE_VALUE_BINDINGS.clientNotes.fallback).toBe('');
|
||
});
|
||
|
||
it('T234: poNumber binding has no path collision with quoteNumber', () => {
|
||
expect(QUOTE_TEMPLATE_VALUE_BINDINGS.poNumber.path).toBe('po_number');
|
||
expect(QUOTE_TEMPLATE_VALUE_BINDINGS.quoteNumber.path).toBe('quote_number');
|
||
});
|
||
});
|
||
|
||
// ── standardTemplates ────────────────────────────────────────────────
|
||
describe('quote-template-ast – standardTemplates', () => {
|
||
it('T235: exposes standard-quote-default and standard-quote-detailed', () => {
|
||
expect(STANDARD_QUOTE_TEMPLATE_ASTS).toHaveProperty('standard-quote-default');
|
||
expect(STANDARD_QUOTE_TEMPLATE_ASTS).toHaveProperty('standard-quote-detailed');
|
||
});
|
||
|
||
it('T236: each standard template has the correct AST structure', () => {
|
||
for (const [code, ast] of Object.entries(STANDARD_QUOTE_TEMPLATE_ASTS)) {
|
||
expect(ast.kind).toBe('invoice-template-ast');
|
||
expect(ast.version).toBe(TEMPLATE_AST_VERSION);
|
||
expect(ast.metadata?.templateName).toBeTruthy();
|
||
expect(ast.bindings).toBeDefined();
|
||
expect(ast.layout).toBeDefined();
|
||
expect(ast.layout.type).toBe('document');
|
||
expect(ast.layout.children?.length).toBeGreaterThan(0);
|
||
}
|
||
});
|
||
|
||
it('T237: standard-quote-default includes line items table and totals', () => {
|
||
const ast = STANDARD_QUOTE_TEMPLATE_ASTS['standard-quote-default'];
|
||
const nodeIds = collectNodeIds(ast.layout);
|
||
expect(nodeIds).toContain('line-items');
|
||
expect(nodeIds).toContain('totals');
|
||
expect(nodeIds).toContain('quote-number');
|
||
expect(nodeIds).toContain('signature-block');
|
||
});
|
||
|
||
it('T238: standard-quote-detailed includes phase summary and version field', () => {
|
||
const ast = STANDARD_QUOTE_TEMPLATE_ASTS['standard-quote-detailed'];
|
||
const nodeIds = collectNodeIds(ast.layout);
|
||
expect(nodeIds).toContain('phase-summary');
|
||
expect(nodeIds).toContain('line-items-detailed');
|
||
expect(nodeIds).toContain('version');
|
||
});
|
||
|
||
it('T239: getStandardQuoteTemplateAstByCode returns a clone', () => {
|
||
const a = getStandardQuoteTemplateAstByCode('standard-quote-default');
|
||
const b = getStandardQuoteTemplateAstByCode('standard-quote-default');
|
||
expect(a).not.toBe(b);
|
||
expect(a).toEqual(b);
|
||
});
|
||
|
||
it('T240: getStandardQuoteTemplateAstByCode returns null for unknown code', () => {
|
||
const result = getStandardQuoteTemplateAstByCode('nonexistent-template');
|
||
expect(result).toBeNull();
|
||
});
|
||
|
||
it('T241: standard templates use quote template bindings', () => {
|
||
for (const ast of Object.values(STANDARD_QUOTE_TEMPLATE_ASTS)) {
|
||
const bindings = ast.bindings!;
|
||
expect(bindings.values?.quoteNumber).toBeDefined();
|
||
expect(bindings.values?.subtotal).toBeDefined();
|
||
expect(bindings.collections?.lineItems).toBeDefined();
|
||
}
|
||
});
|
||
});
|
||
|
||
// ── helpers ──────────────────────────────────────────────────────────
|
||
function collectNodeIds(node: any): string[] {
|
||
const ids: string[] = [];
|
||
if (node.id) ids.push(node.id);
|
||
if (Array.isArray(node.children)) {
|
||
for (const child of node.children) {
|
||
ids.push(...collectNodeIds(child));
|
||
}
|
||
}
|
||
return ids;
|
||
}
|