import { CARGO_TYPE_SUBTREE_SQL, bookingContainerCountSql, bookingContainerVgmSql, bookingContentMatchSql, bookingContentSql, bookingHasContainerTypeSql, bookingRequestedCargoSql, bookingRequestedContainerCountSql, } from './booking-content.sql'; describe('bookingContentSql', () => { const sql = bookingContentSql('b'); it('prefers the container lines, since container bookings carry no description', () => { expect(sql.indexOf('freight.booking_container')).toBeLessThan( sql.indexOf('freight.cargo_types'), ); expect(sql).toContain('freight.container_types'); expect(sql).toContain('bc.deleted_at IS NULL'); }); it('falls back to commodity, then to the free-text description', () => { expect(sql.indexOf('cgt.cargo_type_name')).toBeLessThan( sql.indexOf('b.cargo_free_text'), ); }); // An empty string is not a missing value to COALESCE — without NULLIF a blank // description would win over the commodity behind it. it('treats an empty string as absent at every level', () => { expect(sql.match(/NULLIF/g)).toHaveLength(3); }); it('rewrites every reference when embedded under another alias', () => { expect(bookingContentSql('bk')).not.toMatch(/\bb\.(cargo|id)/); }); }); describe('CARGO_TYPE_SUBTREE_SQL', () => { // The filter offers groups, not just leaves, so picking "Bulk" has to reach // commodities at any depth beneath it — two levels today, more tomorrow. it('walks the tree recursively rather than one level of children', () => { expect(CARGO_TYPE_SUBTREE_SQL).toContain('WITH RECURSIVE'); expect(CARGO_TYPE_SUBTREE_SQL).toContain('c.parent_group_id = sub.id'); }); it('includes the picked node itself, so a leaf still matches exactly', () => { expect(CARGO_TYPE_SUBTREE_SQL).toContain('WHERE id = :cargoTypeId'); }); }); describe('bookingContentMatchSql', () => { const sql = bookingContentMatchSql('b'); it('searches all three places content can live', () => { expect(sql).toContain('b.cargo_free_text ILIKE :cargoText'); expect(sql).toContain('cgt.cargo_type_name ILIKE :cargoText'); expect(sql).toContain('cnt.code ILIKE :cargoText'); }); // Anything but OR would make the text box match nothing for whole freight // types — a container booking has no commodity, a bulk one has no container. it('ORs them, and stays one parenthesised term for andWhere', () => { expect(sql).not.toContain(' AND :cargoText'); expect(sql.startsWith('(')).toBe(true); expect(sql.trimEnd().endsWith(')')).toBe(true); }); }); describe('bookingContainerCountSql', () => { // booking_container is one row per LINE carrying a quantity, so counting rows // would report a 54-container booking as 1. it('sums the line quantities rather than counting lines', () => { expect(bookingContainerCountSql('b')).toContain('SUM(bc.quantity)'); expect(bookingContainerCountSql('b')).not.toContain('COUNT('); }); it('counts every type by default and one type when scoped', () => { expect(bookingContainerCountSql('b')).not.toContain('container_type_id'); expect(bookingContainerCountSql('b', true)).toContain( 'bc.container_type_id = :containerTypeId', ); }); it('is 0, never NULL, so a bound comparison still decides', () => { expect(bookingContainerCountSql('b')).toContain('COALESCE(SUM(bc.quantity), 0)'); }); it('ignores soft-deleted lines', () => { expect(bookingContainerCountSql('b')).toContain('bc.deleted_at IS NULL'); expect(bookingHasContainerTypeSql('b')).toContain('bc.deleted_at IS NULL'); }); it('rewrites the booking reference under another alias', () => { expect(bookingContainerCountSql('bk')).toContain('bc.booking_id = bk.id'); expect(bookingHasContainerTypeSql('bk')).toContain('bc.booking_id = bk.id'); }); }); describe('bookingContainerVgmSql', () => { // The whole point: b.cargo_total_weight_vgm is 0 for portal container // bookings, so the weight has to come off the lines. it('reads the lines, never the booking-level column', () => { const sql = bookingContainerVgmSql('b'); expect(sql).toContain('SUM(bc.total_vgm_tons)'); expect(sql).not.toContain('cargo_total_weight_vgm'); expect(sql).toContain('bc.deleted_at IS NULL'); }); }); describe('requested (shipment-request) cargo', () => { const cargo = bookingRequestedCargoSql('b'); const count = bookingRequestedContainerCountSql('b'); it('reads the request, never the booking or its container lines', () => { for (const sql of [cargo, count]) { expect(sql).toContain('freight.booking_requests br'); expect(sql).toContain('br.created_booking_id = b.id'); expect(sql).not.toContain('freight.booking_container'); } }); // requested_lines is a free-form jsonb column; jsonb_array_elements throws on // a non-array, which would 500 the whole list for one malformed row. it('survives a requested_lines with no container array', () => { for (const sql of [cargo, count]) { expect(sql).toContain("jsonb_typeof(br.requested_lines->'containers') = 'array'"); expect(sql).toContain("ELSE '[]'::jsonb"); } }); it('renders the bulk shape too, not only containers', () => { expect(cargo).toContain("'bulk'->>'cargoWeightTons'"); expect(cargo).toContain("'bulk'->>'itemCount'"); }); it('counts 0 rather than NULL when no request exists', () => { expect(count).toContain("COALESCE(SUM((l->>'quantity')::int), 0)"); }); it('ignores soft-deleted requests', () => { expect(cargo).toContain('br.deleted_at IS NULL'); expect(count).toContain('br.deleted_at IS NULL'); }); });