2026-06-23 06:30:38 +00:00
import { describe , it , expect } from "vitest" ;
import { renderQuotePdf } from "./render" ;
import type { Letterhead } from "@/lib/letterhead" ;
import type { Customer , Quote , QuoteItem } from "@/lib/types" ;
/ * *
* Smoke tests for the PDF renderer . We don ' t render visually — we
* assert the binary is a well - formed PDF ( magic bytes , sane size ) so
* any regression in the render pipeline ( style typo breaking JSX ,
* dep misconfig , etc . ) trips the test even without eyeballing output .
* /
const baseLetterhead : Letterhead = {
id : "lh1" ,
collectionId : "pbc_1010367695" ,
collectionName : "letterhead_settings" ,
created : "2026-06-22T20:31:27.165Z" ,
updated : "2026-06-22T20:31:27.165Z" ,
display_name : "CNL Mining Supplies" ,
address : "47 Station Street\nCarletonville, Gauteng 2499" ,
phone : "+27 18 786 2897" ,
email : "quotes@wfsales.co.za" ,
vat_number : "4840229449" ,
registration_number : "2014/004024/07" ,
bank_name : "Standard Bank - Carletonville" ,
bank_account_number : "Current Account\n022196552" ,
bank_branch_code : "016141" ,
terms :
"Terms and Conditions:\n1. This quotation is valid for 30 days from the date of issue.\n2. Prices are quoted excluding VAT unless otherwise stated.\n3. Lead time will be confirmed upon receipt of order." ,
default_markup_percent : 30 ,
footer_text : "" ,
logo : "" ,
is_active : true ,
company : "CNL Mining Supplies (Pty) Ltd" ,
accent_color : "#738224" ,
} ;
const baseCustomer : Customer = {
id : "cust1" ,
collectionId : "pbc_customers" ,
collectionName : "customers" ,
created : "2026-06-22T00:00:00Z" ,
updated : "2026-06-22T00:00:00Z" ,
customer : "Cash Sales" ,
email : "james@microntec.co.za" ,
alternative_email : "" ,
buyer_email : "" ,
number : "0615829016" ,
company : "" ,
dateadded : "" ,
lastvisiteddate : "" ,
rep : "" ,
2026-06-29 11:06:01 +00:00
coordinates : { lon : 0 , lat : 0 } ,
2026-06-23 06:30:38 +00:00
description : "" ,
} ;
const baseQuote : Quote = {
id : "q1" ,
collectionId : "pbc_quotes" ,
collectionName : "quotes" ,
created : "2026-06-22T13:40:00Z" ,
updated : "2026-06-22T13:40:00Z" ,
quote_number : "Q009638" ,
quote_reference : "" ,
external_number : "" ,
rfq_number : "" ,
markup : 0 ,
customer : "cust1" ,
company : "" ,
claimed_by : "" ,
description : "" ,
status : "new" ,
approved_by : "" ,
notes : "" ,
email_sender : "" ,
email_subject : "" ,
email_body : "" ,
has_attachments : false ,
company_letterhead : "CNL" ,
email_delivered : false ,
email_delivered_at : "" ,
email_received_at : "" ,
email_opened : false ,
email_opened_at : "" ,
email_tracking_id : "" ,
has_new_reply : false ,
last_reply_at : "" ,
email_failed_at : "" ,
pdf_generated_at : "" ,
email_message_id : "" ,
feat: add quote templates, improved email, new statuses
Add complete quote template functionality, enhanced outbound email sending, new quote statuses, and supporting infrastructure:
- Add new quote statuses (template, sending, mail_failed) with tailored UI styling for status pills
- Implement full template workflows: save real quotes as templates, create new quotes from existing templates
- Add support for custom quote dates and extended validity periods on generated PDF documents
- Build out improved email pipeline with persisted compose fields and retry functionality for failed sends
- Add attachment direction tracking to categorize inbound, outbound, and generated quote attachments
- Fix tooltip component to properly handle Radix `asChild` prop to avoid React DOM attribute errors
- Add comprehensive test coverage, schema migrations, and update sample test and production data
2026-06-24 20:57:01 +00:00
// Backdating / template / follow-up fields — empty defaults so the
// PDF falls back to `created` for the date and the package default
// (30 days) for validity.
quote_date : "" ,
validity_days : null ,
is_template : false ,
template_source_id : "" ,
generated_from_template_id : "" ,
follow_up_due : "" ,
email_cc : "" ,
email_bcc : "" ,
email_subject_outbound : "" ,
email_message : "" ,
attach_auth_letters : true ,
// Outbound-send tracking — empty defaults. Tests that exercise
// the failure banner seed these explicitly.
email_outbound_message_id : "" ,
email_outbound_attachment_ids : "" ,
email_error : "" ,
sending_started_at : "" ,
2026-06-23 06:30:38 +00:00
} ;
const baseItems : QuoteItem [ ] = [
{
id : "qi1" ,
collectionId : "pbc_quote_items" ,
collectionName : "quote_items" ,
created : "2026-06-22T13:40:00Z" ,
updated : "2026-06-22T13:40:00Z" ,
quote : "q1" ,
description : "JEICS30 SLUGGER M2-AL HSS" ,
quote_number : "Q009638" ,
quote_reference : "" ,
quantity : 1 ,
supplier : "" ,
cost_price : 0 ,
selling_price : 648.7 ,
notes : "" ,
is_processed : true ,
markup : 0 ,
quote_pdf_id : "" ,
user_edited : true ,
} ,
{
id : "qi2" ,
collectionId : "pbc_quote_items" ,
collectionName : "quote_items" ,
created : "2026-06-22T13:40:00Z" ,
updated : "2026-06-22T13:40:00Z" ,
quote : "q1" ,
description : "JEIPILOT 1" ,
quote_number : "Q009638" ,
quote_reference : "" ,
quantity : 1 ,
supplier : "" ,
cost_price : 0 ,
selling_price : 88.36 ,
notes : "" ,
is_processed : true ,
markup : 0 ,
quote_pdf_id : "" ,
user_edited : true ,
} ,
] ;
describe ( "renderQuotePdf" , ( ) = > {
it ( "produces a Buffer with a PDF magic-number header" , async ( ) = > {
const buf = await renderQuotePdf ( {
quote : baseQuote ,
items : baseItems ,
customer : baseCustomer ,
letterhead : baseLetterhead ,
logoDataUri : null ,
} ) ;
expect ( Buffer . isBuffer ( buf ) ) . toBe ( true ) ;
// Every PDF starts with the literal `%PDF-` (5 bytes).
expect ( buf . subarray ( 0 , 5 ) . toString ( "ascii" ) ) . toBe ( "%PDF-" ) ;
} , 30 _000 ) ;
it ( "produces a non-trivial buffer (> 1 KB) for a simple quote" , async ( ) = > {
const buf = await renderQuotePdf ( {
quote : baseQuote ,
items : baseItems ,
customer : baseCustomer ,
letterhead : baseLetterhead ,
logoDataUri : null ,
} ) ;
expect ( buf . length ) . toBeGreaterThan ( 1024 ) ;
} , 30 _000 ) ;
it ( "handles missing logo gracefully (logoDataUri=null)" , async ( ) = > {
const buf = await renderQuotePdf ( {
quote : baseQuote ,
items : baseItems ,
customer : baseCustomer ,
letterhead : { . . . baseLetterhead , logo : "" } ,
logoDataUri : null ,
} ) ;
expect ( buf . subarray ( 0 , 5 ) . toString ( "ascii" ) ) . toBe ( "%PDF-" ) ;
} , 30 _000 ) ;
it ( "renders CNL and Isherwood without throwing (different accent colors)" , async ( ) = > {
const cnl = await renderQuotePdf ( {
quote : baseQuote ,
items : baseItems ,
customer : baseCustomer ,
letterhead : baseLetterhead , // accent #738224
logoDataUri : null ,
} ) ;
const isherwood = await renderQuotePdf ( {
quote : baseQuote ,
items : baseItems ,
customer : baseCustomer ,
letterhead : { . . . baseLetterhead , display_name : "ISHERWOOD MINING SUPPLIES" , accent_color : "#7ed957" } ,
logoDataUri : null ,
} ) ;
expect ( cnl . subarray ( 0 , 5 ) . toString ( "ascii" ) ) . toBe ( "%PDF-" ) ;
expect ( isherwood . subarray ( 0 , 5 ) . toString ( "ascii" ) ) . toBe ( "%PDF-" ) ;
// Different accent color → different generated PDF body length.
// (not a strict equality — accent only affects a small region — but
// the two PDFs at least both produce valid output)
} , 30 _000 ) ;
feat: add quote templates, improved email, new statuses
Add complete quote template functionality, enhanced outbound email sending, new quote statuses, and supporting infrastructure:
- Add new quote statuses (template, sending, mail_failed) with tailored UI styling for status pills
- Implement full template workflows: save real quotes as templates, create new quotes from existing templates
- Add support for custom quote dates and extended validity periods on generated PDF documents
- Build out improved email pipeline with persisted compose fields and retry functionality for failed sends
- Add attachment direction tracking to categorize inbound, outbound, and generated quote attachments
- Fix tooltip component to properly handle Radix `asChild` prop to avoid React DOM attribute errors
- Add comprehensive test coverage, schema migrations, and update sample test and production data
2026-06-24 20:57:01 +00:00
it ( "renders with backdated quote_date + extended validity_days without throwing" , async ( ) = > {
// Office scenario: backdate to 1 March 2026, valid for 2 months.
// We can't easily grep for "1 March 2026" in the PDF byte stream
// because @react-pdf compresses text into FlateDecode streams —
// so we verify the format helper produces the right dates and
// trust the rest from the unit tests in format.test.ts.
const { effectiveQuoteDate , effectiveValidityDays , addDays } = await import ( "./format" ) ;
const d = effectiveQuoteDate ( { quote_date : "2026-03-01T00:00:00.000Z" , created : baseQuote.created } ) ;
expect ( d ) . toBe ( "2026-03-01T00:00:00.000Z" ) ;
expect ( effectiveValidityDays ( { validity_days : 60 } ) ) . toBe ( 60 ) ;
expect ( addDays ( d , effectiveValidityDays ( { validity_days : 60 } ) ) ) . toBe ( "2026-04-30T00:00:00.000Z" ) ;
const buf = await renderQuotePdf ( {
quote : {
. . . baseQuote ,
quote_date : "2026-03-01T00:00:00.000Z" ,
validity_days : 60 ,
} ,
items : baseItems ,
customer : baseCustomer ,
letterhead : baseLetterhead ,
logoDataUri : null ,
} ) ;
expect ( buf . subarray ( 0 , 5 ) . toString ( "ascii" ) ) . toBe ( "%PDF-" ) ;
expect ( buf . length ) . toBeGreaterThan ( 1024 ) ;
} , 30 _000 ) ;
2026-06-23 06:30:38 +00:00
} ) ;