import assert from "node:assert/strict"; import { readFileSync } from "node:fs"; import test from "node:test"; import vm from "node:vm"; import { buildOrderLines, catalog } from "../stripe.mjs"; import { consumerFields, licenseCoverage, normalizeSystemConfiguration, publicWizardCatalog, recommendLicenses, systemDeviceTypes } from "../portal-domain.mjs"; import { buildSymconPackage } from "../symcon-package.mjs"; const configuration = () => ({ name: "Testsystem", location: "Teststandort", devices: { buffer_storage: { quantity: 0 }, heat_pump: { quantity: 1, items: [{ name: "WP", properties: { Kontaktart: 1, Kontakt1VariableID: 10001, Kontakt2VariableID: 10002, Nennleistung: 4200, Rueckmeldungsart: 1, IstleistungVariableID: 10003, Kontakt2Invertiert: true, Mindestlaufzeit: 1500 } }] } } }); test("heat-pump order includes separate licenses and setup at existing prices", () => { const result = buildOrderLines("manager_standard", { heat_pump: 2 }, { modules: { heat_pump: 2 } }, "manager_standard"); assert.equal(result.length, 2); assert.deepEqual(result.map(line => [line.catalogKey, line.lineType, line.quantity, line.unitAmount, line.entitlement]), [ ["heat_pump", "license", 2, 2095, true], ["heat_pump", "setup", 2, 2725, false] ]); }); test("heat-pump checkout respects catalog availability and quantities", () => { const disabled = { ...catalog, heat_pump: { ...catalog.heat_pump, available: false } }; assert.throws(() => buildOrderLines("manager_standard", { heat_pump: 1 }, {}, "manager_standard", disabled), /catalog_item_unavailable/); for (const quantity of [-1, 0.5, 1000]) { assert.throws(() => buildOrderLines("manager_standard", { heat_pump: quantity }), /invalid_quantity/); } }); test("wizard publishes two-contact heat-pump fields without duplicates", () => { const fields = publicWizardCatalog().consumerFields.heat_pump; assert.equal(new Set(consumerFields.heat_pump.map(field => field.key)).size, consumerFields.heat_pump.length); for (const key of ["Kontaktart", "Kontakt1VariableID", "Kontakt2VariableID", "Rueckmeldungsart", "IstleistungVariableID", "MaximaleSperrzeit24h"]) { assert.ok(fields.some(field => field.key === key), key); } assert.equal(systemDeviceTypes.heat_pump.moduleId, "{A31C9274-54F7-4BD2-9804-7AF23E86C4D1}"); }); test("heat-pump settings survive normalize, save and recommendation", () => { const result = normalizeSystemConfiguration(configuration()); assert.deepEqual(normalizeSystemConfiguration(result), result); const properties = result.devices.heat_pump.items[0].properties; assert.equal(properties.Kontaktart, 1); assert.equal(properties.Kontakt1VariableID, 10001); assert.equal(properties.Kontakt2VariableID, 10002); assert.equal(properties.IstleistungVariableID, 10003); assert.equal(properties.Rueckmeldungsart, 1); assert.equal(properties.Kontakt2Invertiert, true); assert.equal(properties.Mindestlaufzeit, 1500); assert.equal(properties.MaximaleSperrzeit, 7200); assert.equal(recommendLicenses(result).modules.heat_pump, 1); }); test("older configurations gain no heat-pump device and retain consumers", () => { const input = configuration(); delete input.devices.heat_pump; input.devices.consumer_single = { quantity: 1, items: [{ name: "Bestand", properties: { Nennleistung: 2200 } }] }; const result = normalizeSystemConfiguration(input); assert.deepEqual(result.devices.heat_pump, { quantity: 0, items: [] }); assert.equal(result.devices.consumer_single.items[0].properties.Nennleistung, 2200); }); test("legacy coverage reserves single-consumer licenses without double counting", () => { assert.equal(licenseCoverage({ heat_pump: 2, consumer_single: 1 }, { consumer_single: 3 }).heat_pump, 2); assert.equal(licenseCoverage({ heat_pump: 2, consumer_single: 2 }, { consumer_single: 3 }).heat_pump, 1); assert.equal(licenseCoverage({ heat_pump: 2, consumer_single: 3 }, { consumer_single: 3 }).heat_pump, 0); assert.equal(licenseCoverage({ heat_pump: 2 }, { heat_pump: 1, consumer_single: 3 }).heat_pump, 2); const source = readFileSync(new URL("../public/app.js", import.meta.url), "utf8"); const functionSource = source.match(/ function licenseCoverage\(requested, owned\) \{[\s\S]*?\n \}/)[0]; const browserCoverage = vm.runInNewContext(`(${functionSource})`); for (let requested = 0; requested < 5; requested++) { for (let consumers = 0; consumers < 5; consumers++) { const demand = { heat_pump: requested, consumer_single: consumers }; const owned = { heat_pump: 1, consumer_single: 3 }; assert.equal(JSON.stringify(browserCoverage(demand, owned)), JSON.stringify(licenseCoverage(demand, owned))); } } }); test("package contains heat-pump configuration and preflight before writes", () => { const archive = buildSymconPackage(configuration()); const files = {}; let offset = 0; while (archive.readUInt32LE(offset) === 0x04034b50) { const length = archive.readUInt32LE(offset + 18); const nameLength = archive.readUInt16LE(offset + 26); const extraLength = archive.readUInt16LE(offset + 28); const name = archive.subarray(offset + 30, offset + 30 + nameLength).toString(); const start = offset + 30 + nameLength + extraLength; files[name] = archive.subarray(start, start + length).toString(); offset = start + length; } const bootstrap = files["bootstrap.php"]; assert.match(bootstrap, /A31C9274-54F7-4BD2-9804-7AF23E86C4D1/); assert.ok(bootstrap.indexOf("// Validate heat-pump bindings") < bootstrap.indexOf("$root = enxCategory")); assert.match(bootstrap, /\$contact1 === \$contact2/); assert.match(bootstrap, /Typ der Betriebsrueckmeldung/); assert.match(bootstrap, /IPS_SetProperty\(\$instance, \(string\) \$property, \$value\)/); }); test("header has both links, sidebar has neither, and wizard has ten steps", () => { const html = readFileSync(new URL("../public/index.html", import.meta.url), "utf8"); const header = html.match(/
([\s\S]*?)<\/header>/)[1]; const sidebar = html.match(/