Add external row quantity foundation
This commit is contained in:
parent
bc2bc7ece5
commit
dac19b093c
30 changed files with 2750 additions and 22 deletions
3
src/db/migrations/0005_stale_gorilla_man.sql
Normal file
3
src/db/migrations/0005_stale_gorilla_man.sql
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
ALTER TABLE `circuit_device_rows` ADD `manual_quantity` integer DEFAULT 0 NOT NULL;
|
||||
--> statement-breakpoint
|
||||
UPDATE `circuit_device_rows` SET `manual_quantity` = `quantity`;
|
||||
2404
src/db/migrations/meta/0005_snapshot.json
Normal file
2404
src/db/migrations/meta/0005_snapshot.json
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -36,6 +36,13 @@
|
|||
"when": 1785684457208,
|
||||
"tag": "0004_illegal_the_stranger",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 5,
|
||||
"version": "6",
|
||||
"when": 1785687503453,
|
||||
"tag": "0005_stale_gorilla_man",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -82,6 +82,14 @@ export class CircuitDeviceRowProjectCommandRepository
|
|||
change.value,
|
||||
])
|
||||
) as CircuitDeviceRowPatchInput;
|
||||
const targetQuantity = patch.quantity ?? current.quantity;
|
||||
const targetManualQuantity =
|
||||
patch.manualQuantity ?? current.manualQuantity;
|
||||
if (targetManualQuantity > targetQuantity) {
|
||||
throw new Error(
|
||||
"Device-row manual quantity must not exceed total quantity."
|
||||
);
|
||||
}
|
||||
if (
|
||||
input.source === "user" &&
|
||||
patch.phaseType !== undefined &&
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import { circuitLists } from "../schema/circuit-lists.js";
|
|||
import { circuits } from "../schema/circuits.js";
|
||||
import {
|
||||
assertCircuitDeviceRowReferencesInProject,
|
||||
toCircuitDeviceRowInsertValues,
|
||||
toCircuitDeviceRowSnapshot,
|
||||
} from "./circuit-device-row-structure.persistence.js";
|
||||
import { executeProjectCommandTransaction } from "./project-command-transaction.persistence.js";
|
||||
|
|
@ -92,7 +93,10 @@ export class CircuitDeviceRowStructureProjectCommandRepository
|
|||
throw new Error("Circuit device-row id already exists.");
|
||||
}
|
||||
|
||||
database.insert(circuitDeviceRows).values(row).run();
|
||||
database
|
||||
.insert(circuitDeviceRows)
|
||||
.values(toCircuitDeviceRowInsertValues(row))
|
||||
.run();
|
||||
const circuitUpdate = database
|
||||
.update(circuits)
|
||||
.set({ isReserve: 0 })
|
||||
|
|
|
|||
|
|
@ -61,6 +61,7 @@ export function toCircuitDeviceRowSnapshot(
|
|||
roomNumberSnapshot: row.roomNumberSnapshot,
|
||||
roomNameSnapshot: row.roomNameSnapshot,
|
||||
quantity: row.quantity,
|
||||
manualQuantity: row.manualQuantity,
|
||||
powerPerUnit: row.powerPerUnit,
|
||||
simultaneityFactor: row.simultaneityFactor,
|
||||
cosPhi: row.cosPhi,
|
||||
|
|
@ -68,3 +69,10 @@ export function toCircuitDeviceRowSnapshot(
|
|||
overriddenFields: row.overriddenFields,
|
||||
};
|
||||
}
|
||||
|
||||
export function toCircuitDeviceRowInsertValues(row: CircuitDeviceRowSnapshot) {
|
||||
return {
|
||||
...row,
|
||||
manualQuantity: row.manualQuantity ?? row.quantity,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ export interface CircuitDeviceRowUpdateInput {
|
|||
roomNumberSnapshot?: string;
|
||||
roomNameSnapshot?: string;
|
||||
quantity: number;
|
||||
manualQuantity?: number;
|
||||
powerPerUnit: number;
|
||||
simultaneityFactor: number;
|
||||
cosPhi?: number;
|
||||
|
|
@ -34,6 +35,7 @@ export interface CircuitDeviceRowPatchInput {
|
|||
roomNumberSnapshot?: string | null;
|
||||
roomNameSnapshot?: string | null;
|
||||
quantity?: number;
|
||||
manualQuantity?: number;
|
||||
powerPerUnit?: number;
|
||||
simultaneityFactor?: number;
|
||||
cosPhi?: number | null;
|
||||
|
|
@ -61,6 +63,7 @@ export function toCircuitDeviceRowUpdateValues(input: CircuitDeviceRowUpdateInpu
|
|||
roomNumberSnapshot: input.roomNumberSnapshot ?? null,
|
||||
roomNameSnapshot: input.roomNameSnapshot ?? null,
|
||||
quantity: input.quantity,
|
||||
manualQuantity: input.manualQuantity ?? input.quantity,
|
||||
powerPerUnit: input.powerPerUnit,
|
||||
simultaneityFactor: input.simultaneityFactor,
|
||||
cosPhi: input.cosPhi ?? null,
|
||||
|
|
@ -90,6 +93,7 @@ export function toCircuitDeviceRowPatchValues(input: CircuitDeviceRowPatchInput)
|
|||
}
|
||||
if (has("roomNameSnapshot")) values.roomNameSnapshot = input.roomNameSnapshot ?? null;
|
||||
if (input.quantity !== undefined) values.quantity = input.quantity;
|
||||
if (input.manualQuantity !== undefined) values.manualQuantity = input.manualQuantity;
|
||||
if (input.powerPerUnit !== undefined) values.powerPerUnit = input.powerPerUnit;
|
||||
if (input.simultaneityFactor !== undefined) {
|
||||
values.simultaneityFactor = input.simultaneityFactor;
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ export class CircuitDeviceRowRepository {
|
|||
roomNumberSnapshot: circuitDeviceRows.roomNumberSnapshot,
|
||||
roomNameSnapshot: circuitDeviceRows.roomNameSnapshot,
|
||||
quantity: circuitDeviceRows.quantity,
|
||||
manualQuantity: circuitDeviceRows.manualQuantity,
|
||||
powerPerUnit: circuitDeviceRows.powerPerUnit,
|
||||
simultaneityFactor: circuitDeviceRows.simultaneityFactor,
|
||||
cosPhi: circuitDeviceRows.cosPhi,
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ import { circuitSections } from "../schema/circuit-sections.js";
|
|||
import { circuits } from "../schema/circuits.js";
|
||||
import { distributionBoardComponentProtectionDevices } from "../schema/distribution-board-component-protection-devices.js";
|
||||
import { distributionBoardComponents } from "../schema/distribution-board-components.js";
|
||||
import { toCircuitDeviceRowInsertValues } from "./circuit-device-row-structure.persistence.js";
|
||||
import { executeProjectCommandTransaction } from "./project-command-transaction.persistence.js";
|
||||
|
||||
export class CircuitGroupSubtreeProjectCommandRepository
|
||||
|
|
@ -212,7 +213,10 @@ export class CircuitGroupSubtreeProjectCommandRepository
|
|||
.run();
|
||||
}
|
||||
if (deviceRows.length > 0) {
|
||||
database.insert(circuitDeviceRows).values(deviceRows).run();
|
||||
database
|
||||
.insert(circuitDeviceRows)
|
||||
.values(deviceRows.map(toCircuitDeviceRowInsertValues))
|
||||
.run();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ import { circuitSections } from "../schema/circuit-sections.js";
|
|||
import { circuits } from "../schema/circuits.js";
|
||||
import {
|
||||
assertCircuitDeviceRowReferencesInProject,
|
||||
toCircuitDeviceRowInsertValues,
|
||||
toCircuitDeviceRowSnapshot,
|
||||
} from "./circuit-device-row-structure.persistence.js";
|
||||
import { executeProjectCommandTransaction } from "./project-command-transaction.persistence.js";
|
||||
|
|
@ -166,7 +167,10 @@ export class CircuitStructureProjectCommandRepository
|
|||
})
|
||||
.run();
|
||||
if (snapshot.deviceRows.length > 0) {
|
||||
database.insert(circuitDeviceRows).values(snapshot.deviceRows).run();
|
||||
database
|
||||
.insert(circuitDeviceRows)
|
||||
.values(snapshot.deviceRows.map(toCircuitDeviceRowInsertValues))
|
||||
.run();
|
||||
}
|
||||
if (snapshot.protectionDevice) {
|
||||
database
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ import { floors } from "../schema/floors.js";
|
|||
import { projectDevices } from "../schema/project-devices.js";
|
||||
import { projects } from "../schema/projects.js";
|
||||
import { rooms } from "../schema/rooms.js";
|
||||
import { toCircuitDeviceRowInsertValues } from "./circuit-device-row-structure.persistence.js";
|
||||
import { executeProjectCommandTransaction } from "./project-command-transaction.persistence.js";
|
||||
|
||||
export class DistributionBoardSubtreeProjectCommandRepository
|
||||
|
|
@ -253,7 +254,10 @@ export class DistributionBoardSubtreeProjectCommandRepository
|
|||
.run();
|
||||
}
|
||||
if (deviceRows.length > 0) {
|
||||
database.insert(circuitDeviceRows).values(deviceRows).run();
|
||||
database
|
||||
.insert(circuitDeviceRows)
|
||||
.values(deviceRows.map(toCircuitDeviceRowInsertValues))
|
||||
.run();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -192,6 +192,7 @@ export function readProjectStateSnapshot(
|
|||
roomNumberSnapshot: circuitDeviceRows.roomNumberSnapshot,
|
||||
roomNameSnapshot: circuitDeviceRows.roomNameSnapshot,
|
||||
quantity: circuitDeviceRows.quantity,
|
||||
manualQuantity: circuitDeviceRows.manualQuantity,
|
||||
powerPerUnit: circuitDeviceRows.powerPerUnit,
|
||||
simultaneityFactor: circuitDeviceRows.simultaneityFactor,
|
||||
cosPhi: circuitDeviceRows.cosPhi,
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ export const circuitDeviceRows = sqliteTable("circuit_device_rows", {
|
|||
roomNumberSnapshot: text("room_number_snapshot"),
|
||||
roomNameSnapshot: text("room_name_snapshot"),
|
||||
quantity: integer("quantity").notNull(),
|
||||
manualQuantity: integer("manual_quantity").notNull().default(0),
|
||||
powerPerUnit: real("power_per_unit").notNull(),
|
||||
simultaneityFactor: real("simultaneity_factor").notNull(),
|
||||
cosPhi: real("cos_phi"),
|
||||
|
|
|
|||
43
src/domain/calculations/circuit-device-row-quantity.ts
Normal file
43
src/domain/calculations/circuit-device-row-quantity.ts
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
export interface ExternalQuantityContribution {
|
||||
effectiveQuantity: number;
|
||||
}
|
||||
|
||||
export function calculateCircuitDeviceRowQuantity(
|
||||
manualQuantity: number,
|
||||
externalObjects: ExternalQuantityContribution[]
|
||||
) {
|
||||
assertNonNegativeFinite(manualQuantity, "Manual quantity");
|
||||
return externalObjects.reduce((quantity, object) => {
|
||||
assertPositiveFinite(object.effectiveQuantity, "External effective quantity");
|
||||
return quantity + object.effectiveQuantity;
|
||||
}, manualQuantity);
|
||||
}
|
||||
|
||||
export function assertCircuitDeviceRowQuantity(input: {
|
||||
quantity: number;
|
||||
manualQuantity: number;
|
||||
externalObjects: ExternalQuantityContribution[];
|
||||
}) {
|
||||
assertNonNegativeFinite(input.quantity, "Materialized quantity");
|
||||
const expected = calculateCircuitDeviceRowQuantity(
|
||||
input.manualQuantity,
|
||||
input.externalObjects
|
||||
);
|
||||
if (input.quantity !== expected) {
|
||||
throw new Error(
|
||||
`Materialized quantity ${input.quantity} does not match manual and external quantity ${expected}.`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function assertNonNegativeFinite(value: number, label: string) {
|
||||
if (!Number.isFinite(value) || value < 0) {
|
||||
throw new Error(`${label} must be a non-negative finite number.`);
|
||||
}
|
||||
}
|
||||
|
||||
function assertPositiveFinite(value: number, label: string) {
|
||||
if (!Number.isFinite(value) || value <= 0) {
|
||||
throw new Error(`${label} must be a positive finite number.`);
|
||||
}
|
||||
}
|
||||
|
|
@ -18,6 +18,7 @@ export interface CircuitDeviceRowUpdateValues {
|
|||
roomNumberSnapshot: string | null;
|
||||
roomNameSnapshot: string | null;
|
||||
quantity: number;
|
||||
manualQuantity: number;
|
||||
powerPerUnit: number;
|
||||
simultaneityFactor: number;
|
||||
cosPhi: number | null;
|
||||
|
|
@ -51,7 +52,18 @@ export function createCircuitDeviceRowUpdateProjectCommand(
|
|||
rowId: string,
|
||||
patch: CircuitDeviceRowUpdatePatch
|
||||
): CircuitDeviceRowUpdateProjectCommand {
|
||||
const changes = Object.entries(patch).map(([field, value]) => ({
|
||||
const normalizedPatch =
|
||||
patch.quantity !== undefined && patch.manualQuantity === undefined
|
||||
? { ...patch, manualQuantity: patch.quantity }
|
||||
: patch;
|
||||
if (
|
||||
normalizedPatch.quantity !== undefined &&
|
||||
normalizedPatch.manualQuantity !== undefined &&
|
||||
normalizedPatch.manualQuantity > normalizedPatch.quantity
|
||||
) {
|
||||
throw new Error("manualQuantity must not exceed quantity.");
|
||||
}
|
||||
const changes = Object.entries(normalizedPatch).map(([field, value]) => ({
|
||||
field: field as CircuitDeviceRowUpdateField,
|
||||
value,
|
||||
})) as CircuitDeviceRowUpdateFieldChange[];
|
||||
|
|
@ -125,6 +137,7 @@ function assertCircuitDeviceRowUpdateFieldValue(
|
|||
}
|
||||
if (
|
||||
field === "quantity" ||
|
||||
field === "manualQuantity" ||
|
||||
field === "powerPerUnit" ||
|
||||
field === "simultaneityFactor"
|
||||
) {
|
||||
|
|
@ -164,6 +177,7 @@ function isCircuitDeviceRowUpdateField(
|
|||
"roomNumberSnapshot",
|
||||
"roomNameSnapshot",
|
||||
"quantity",
|
||||
"manualQuantity",
|
||||
"powerPerUnit",
|
||||
"simultaneityFactor",
|
||||
"cosPhi",
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ export interface CircuitDeviceRowSnapshot {
|
|||
roomNumberSnapshot: string | null;
|
||||
roomNameSnapshot: string | null;
|
||||
quantity: number;
|
||||
manualQuantity?: number;
|
||||
powerPerUnit: number;
|
||||
simultaneityFactor: number;
|
||||
cosPhi: number | null;
|
||||
|
|
@ -57,10 +58,14 @@ export type CircuitDeviceRowStructureProjectCommand =
|
|||
export function createCircuitDeviceRowInsertProjectCommand(
|
||||
row: CircuitDeviceRowSnapshot
|
||||
): CircuitDeviceRowInsertProjectCommand {
|
||||
const normalizedRow = {
|
||||
...row,
|
||||
manualQuantity: row.manualQuantity ?? row.quantity,
|
||||
};
|
||||
const command: CircuitDeviceRowInsertProjectCommand = {
|
||||
schemaVersion: circuitDeviceRowStructureCommandSchemaVersion,
|
||||
type: circuitDeviceRowInsertCommandType,
|
||||
payload: { row },
|
||||
payload: { row: normalizedRow },
|
||||
};
|
||||
assertCircuitDeviceRowInsertProjectCommand(command);
|
||||
return command;
|
||||
|
|
@ -117,7 +122,13 @@ export function assertCircuitDeviceRowInsertProjectCommand(
|
|||
] as const) {
|
||||
assertNullableString(row[field], `row.${field}`);
|
||||
}
|
||||
assertNonNegativeNumber(row.quantity, "row.quantity");
|
||||
const quantity = row.quantity;
|
||||
assertNonNegativeNumber(quantity, "row.quantity");
|
||||
const manualQuantity = row.manualQuantity ?? quantity;
|
||||
assertNonNegativeNumber(manualQuantity, "row.manualQuantity");
|
||||
if (manualQuantity > quantity) {
|
||||
throw new Error("row.manualQuantity must not exceed row.quantity.");
|
||||
}
|
||||
assertNonNegativeNumber(row.powerPerUnit, "row.powerPerUnit");
|
||||
assertNonNegativeNumber(
|
||||
row.simultaneityFactor,
|
||||
|
|
@ -172,7 +183,10 @@ function assertFiniteNumber(
|
|||
}
|
||||
}
|
||||
|
||||
function assertNonNegativeNumber(value: unknown, field: string) {
|
||||
function assertNonNegativeNumber(
|
||||
value: unknown,
|
||||
field: string
|
||||
): asserts value is number {
|
||||
assertFiniteNumber(value, field);
|
||||
if (value < 0) {
|
||||
throw new Error(`${field} must not be negative.`);
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ export interface CircuitDeviceRow {
|
|||
roomNumberSnapshot?: string;
|
||||
roomNameSnapshot?: string;
|
||||
quantity: number;
|
||||
manualQuantity: number;
|
||||
powerPerUnit: number;
|
||||
simultaneityFactor: number;
|
||||
cosPhi?: number;
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ export interface CircuitTreeDeviceRow {
|
|||
roomNumberSnapshot?: string;
|
||||
roomNameSnapshot?: string;
|
||||
quantity: number;
|
||||
manualQuantity: number;
|
||||
powerPerUnit: number;
|
||||
simultaneityFactor: number;
|
||||
cosPhi?: number;
|
||||
|
|
|
|||
|
|
@ -27,8 +27,9 @@ import {
|
|||
type ExternalModelStateSnapshot,
|
||||
} from "../../external-model/domain/external-model-contracts.js";
|
||||
|
||||
export const projectStateSnapshotSchemaVersion = 4 as const;
|
||||
const previousProjectStateSnapshotSchemaVersion = 3 as const;
|
||||
export const projectStateSnapshotSchemaVersion = 5 as const;
|
||||
const previousProjectStateSnapshotSchemaVersion = 4 as const;
|
||||
const externalModelProjectStateSnapshotSchemaVersion = 3 as const;
|
||||
const legacyProjectStateSnapshotSchemaVersion = 2 as const;
|
||||
const baselineProjectStateSnapshotSchemaVersion = 1 as const;
|
||||
|
||||
|
|
@ -103,8 +104,15 @@ const circuitSectionSchema = z
|
|||
}
|
||||
});
|
||||
|
||||
const circuitDeviceRowSchema = z
|
||||
.object({
|
||||
const circuitDeviceRowSchema = z.preprocess(
|
||||
(value) => {
|
||||
if (value === null || typeof value !== "object" || Array.isArray(value)) return value;
|
||||
const row = value as Record<string, unknown>;
|
||||
return Object.prototype.hasOwnProperty.call(row, "manualQuantity")
|
||||
? row
|
||||
: { ...row, manualQuantity: row.quantity };
|
||||
},
|
||||
z.object({
|
||||
id: idSchema,
|
||||
circuitId: idSchema,
|
||||
linkedProjectDeviceId: idSchema.nullable(),
|
||||
|
|
@ -120,13 +128,15 @@ const circuitDeviceRowSchema = z
|
|||
roomNumberSnapshot: nullableStringSchema,
|
||||
roomNameSnapshot: nullableStringSchema,
|
||||
quantity: finiteNumberSchema.nonnegative(),
|
||||
manualQuantity: finiteNumberSchema.nonnegative(),
|
||||
powerPerUnit: finiteNumberSchema.nonnegative(),
|
||||
simultaneityFactor: finiteNumberSchema.nonnegative(),
|
||||
cosPhi: finiteNumberSchema.positive().nullable(),
|
||||
remark: nullableStringSchema,
|
||||
overriddenFields: nullableStringSchema,
|
||||
})
|
||||
.strict();
|
||||
.strict()
|
||||
);
|
||||
|
||||
const circuitSchema = z
|
||||
.object({
|
||||
|
|
@ -453,6 +463,17 @@ const previousProjectStateSnapshotSchema = z
|
|||
project: projectSchema,
|
||||
distributionBoards: z.array(distributionBoardSchema),
|
||||
externalCsvConfiguration: externalCsvConfigurationSnapshotSchema.nullable(),
|
||||
externalModel: externalModelStateSchema,
|
||||
...projectStateSnapshotContents,
|
||||
})
|
||||
.strict();
|
||||
|
||||
const externalModelProjectStateSnapshotSchema = z
|
||||
.object({
|
||||
schemaVersion: z.literal(externalModelProjectStateSnapshotSchemaVersion),
|
||||
project: projectSchema,
|
||||
distributionBoards: z.array(distributionBoardSchema),
|
||||
externalCsvConfiguration: externalCsvConfigurationSnapshotSchema.nullable(),
|
||||
...projectStateSnapshotContents,
|
||||
})
|
||||
.strict();
|
||||
|
|
@ -502,6 +523,13 @@ function upgradePreviousProjectStateSnapshot(value: unknown): unknown {
|
|||
const schemaVersion = (value as { schemaVersion: number }).schemaVersion;
|
||||
if (schemaVersion === previousProjectStateSnapshotSchemaVersion) {
|
||||
const previous = previousProjectStateSnapshotSchema.parse(value);
|
||||
return {
|
||||
...previous,
|
||||
schemaVersion: projectStateSnapshotSchemaVersion,
|
||||
};
|
||||
}
|
||||
if (schemaVersion === externalModelProjectStateSnapshotSchemaVersion) {
|
||||
const previous = externalModelProjectStateSnapshotSchema.parse(value);
|
||||
return {
|
||||
...previous,
|
||||
schemaVersion: projectStateSnapshotSchemaVersion,
|
||||
|
|
@ -765,6 +793,11 @@ function assertProjectStateSnapshotRelations(
|
|||
if (row.roomId !== null) {
|
||||
assertReference(roomIds, row.roomId, "device row room");
|
||||
}
|
||||
if (row.manualQuantity > row.quantity) {
|
||||
throw new Error(
|
||||
"Snapshot device row manual quantity must not exceed total quantity."
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -350,6 +350,7 @@ export interface CircuitTreeDeviceRowDto {
|
|||
roomNumberSnapshot?: string;
|
||||
roomNameSnapshot?: string;
|
||||
quantity: number;
|
||||
manualQuantity: number;
|
||||
powerPerUnit: number;
|
||||
simultaneityFactor: number;
|
||||
cosPhi?: number;
|
||||
|
|
|
|||
|
|
@ -160,6 +160,7 @@ export async function getCircuitTree(req: Request, res: Response) {
|
|||
roomNumberSnapshot: row.roomNumberSnapshot ?? undefined,
|
||||
roomNameSnapshot: row.roomNameSnapshot ?? undefined,
|
||||
quantity: row.quantity,
|
||||
manualQuantity: row.manualQuantity,
|
||||
powerPerUnit: row.powerPerUnit,
|
||||
simultaneityFactor: row.simultaneityFactor,
|
||||
cosPhi: row.cosPhi ?? undefined,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue