import { generateOrderNotificationApiRoute } from '../src/ecommerce/ecommerce-api-routes-generator' import { UIDLEcommerceSettings } from '@teleporthq/teleport-types' // The order-notification endpoint is now a thin shell that // resolves the canonical order-shaped token payload from the // request body, renders the configured subject + body templates, // and delegates dispatch to the shared utils/ecommerce/email-sender // module. Provider switching, environment-variable lookup, and // HTTPS / SMTP wiring all live in the sender (see // `email-sender-and-low-stock-alert.test.ts`); this file pins the // endpoint-level contract — template literals carried verbatim // into the emitted code, fallback paths, diagnostic logging. const baseSettings = (overrides: any = {}): UIDLEcommerceSettings => ({ cashOnDelivery: true, deliveryEnabled: true, storePickupEnabled: false, guestCheckout: true, stockManagement: true, orderNotifications: true, deliveryConfig: null, stockManagementConfig: null, paymentProviders: [], orderNotificationConfig: { provider: 'postmark', fromEmail: 'orders@example.com', fromName: 'Example Store', notificationEmails: ['merchant@example.com'], subject: 'New order {{orderNumber}} — {{totalAmount}}', body: '
Order {{orderNumber}} for {{customerName}}
', ...overrides, }, }) describe('generateOrderNotificationApiRoute — endpoint shell', () => { const route = generateOrderNotificationApiRoute(baseSettings()) it('delegates dispatch to the shared email-sender module', () => { expect(route).toContain("require('../../../utils/ecommerce/email-sender')") expect(route).toContain('sender.sendNotificationEmail(notificationEmails, subject, html, {') // …and describes the send for the sent-email ledger, then lands the row // before replying. expect(route).toContain("emailType: 'order-notification'") expect(route).toContain('await sender.settleSentEmailLog()') }) it('renders subject + body via sender.renderTemplate (not inline)', () => { expect(route).toContain('sender.renderTemplate(') // The inline renderTemplate function was removed — assert it // is NOT defined locally in the endpoint anymore. expect(route).not.toMatch(/function\s+renderTemplate/) }) it("bakes the merchant's subject + body templates verbatim into the source", () => { // The merchant edits the templates in the GUI; the generator // emits them as string literals so the runtime only needs to // do token substitution. expect(route).toContain('New order {{orderNumber}} — {{totalAmount}}') expect(route).toContain('Order {{orderNumber}} for {{customerName}}
') }) it('injects the configured notificationEmails list as a JSON array', () => { expect(route).toContain('["merchant@example.com"]') }) it('logs a diagnostic line on the no-recipients skip path', () => { const empty = generateOrderNotificationApiRoute(baseSettings({ notificationEmails: [] })) expect(empty).toContain( "console.log('[order-notification] skipped: no notification emails configured')" ) expect(empty).toContain('no_notification_emails') }) it('logs a diagnostic line on uncaught handler errors', () => { expect(route).toContain("console.error('[order-notification] handler error: '") }) }) describe('generateOrderNotificationApiRoute — token payload', () => { const route = generateOrderNotificationApiRoute(baseSettings()) it('builds the canonical token payload from the request body', () => { for (const tok of [ 'orderNumber: resolvedOrderNumber', 'customerName: customerName', 'customerEmail: customerEmail', 'currency: currency', 'paymentMethod: paymentMethod', 'fulfillmentMethod: fulfillmentMethod', 'itemsCount: itemsCount', 'orderDate: formattedDate', // shippingAddress is HTML-escaped + newline-converted before // being put into the token payload, so the property assignment // uses the rendered variable (not the raw request body field). 'shippingAddress: shippingAddressHtml', // itemsList is the new HTML-rendered token derived from items[] 'itemsList: itemsListHtml', ]) { expect(route).toContain(tok) } }) it('falls back to orderId when orderNumber is missing', () => { expect(route).toContain('orderNumber || orderId || ') }) it('formats totalAmount with two decimals when numeric', () => { expect(route).toContain('totalAmount.toFixed(2)') }) }) describe('generateOrderNotificationApiRoute — itemsList rendering + auto-injection', () => { const route = generateOrderNotificationApiRoute( baseSettings({ // A merchant-configured body that has {{itemsCount}} but NO {{itemsList}}. // The endpoint should auto-inject the list since the merchant clearly // wants item details (they reference itemsCount) but forgot to add the // list placeholder. body: 'Order {{orderNumber}}, Items: {{itemsCount}}
', }) ) it('escapes HTML in product name, sku, and shipping address fields', () => { expect(route).toContain('function htmlEscape(v)') expect(route).toContain('&') expect(route).toContain('<') expect(route).toContain('>') expect(route).toContain('"') }) it('builds anTest {{orderNumber}}: items {{itemsCount}}
', }) ) // Extract every relevant helper + the auto-inject snippet by // scanning for distinguishing markers; build a tiny shim that // re-runs the same logic against a fixture payload. const HTML_ESC = 'function htmlEscape(v) {' const FORMAT_MONEY = 'function formatMoney(n) {' expect(route).toContain(HTML_ESC) expect(route).toContain(FORMAT_MONEY) // Fixture passes 2 items including one with an XSS attempt in name. // The output HTML must be escaped. const ITEMS = [ { name: '', sku: 'X1', quantity: 2, unitPrice: 9.99 }, { name: 'Normal product', quantity: 1, unitPrice: 49 }, ] // Sanity: the route's template-literal string IS the full handler // source, so we can verify the escaped HTML would be present in // the listHtml output by re-running formatMoney + htmlEscape in // an eval'd shim. const shim = ` ${route.match(/function htmlEscape\(v\) \{[\s\S]*?\}/)![0]} ${route.match(/function formatMoney\(n\) \{[\s\S]*?\}/)![0]} return htmlEscape('