Add ledger, address/contact sort, and tighter schema-graph layout.

Replace manifestarticle.transactedamount with an append-only ledger, add sort on entity address/contact, and allow same-rank FKs with crossing-aware exits in the graph.
This commit is contained in:
seb
2026-08-06 07:39:48 +02:00
parent 3b77a1c946
commit ae549fd022
17 changed files with 2019 additions and 1656 deletions

View File

@@ -178,6 +178,7 @@ BEGIN
city NVARCHAR(255) NULL,
county NVARCHAR(255) NULL,
addresstype NVARCHAR(50) NOT NULL,
sort INT NOT NULL CONSTRAINT DF_entityaddress_sort DEFAULT 0,
CONSTRAINT FK_entityaddress_entity FOREIGN KEY (entity_id) REFERENCES entity(id) ON DELETE CASCADE
);
END
@@ -191,6 +192,7 @@ BEGIN
entity_id INT NOT NULL,
contacttype NVARCHAR(50) NOT NULL,
contact NVARCHAR(255) NOT NULL,
sort INT NOT NULL CONSTRAINT DF_entitycontact_sort DEFAULT 0,
CONSTRAINT FK_entitycontact_entity FOREIGN KEY (entity_id) REFERENCES entity(id) ON DELETE CASCADE
);
END
@@ -520,7 +522,6 @@ BEGIN
article_id INT NULL,
amount FLOAT NOT NULL,
net FLOAT NOT NULL,
transactedamount FLOAT NOT NULL,
comment NVARCHAR(MAX) NULL,
rebate FLOAT NOT NULL CONSTRAINT DF_manifestarticle_rebate DEFAULT 0,
tax_id INT NOT NULL,
@@ -531,6 +532,21 @@ BEGIN
END
`;
export const INIT_LEDGER = `
IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'ledger')
BEGIN
CREATE TABLE ledger (
id INT IDENTITY(1,1) PRIMARY KEY,
manifestarticle_id INT NOT NULL,
transaction_type NVARCHAR(100) NOT NULL,
amount FLOAT NOT NULL,
transaction_text NVARCHAR(MAX) NOT NULL,
prev_hash NVARCHAR(128) NULL,
CONSTRAINT FK_ledger_manifestarticle FOREIGN KEY (manifestarticle_id) REFERENCES manifestarticle(id) ON DELETE NO ACTION
) WITH (LEDGER = ON (APPEND_ONLY = ON));
END
`;
export const INIT_NUMBERS = `
IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'numbers')
BEGIN
@@ -688,6 +704,7 @@ export const SCHEMA_SCRIPTS = [
INIT_PRICE,
INIT_MANIFEST,
INIT_MANIFESTARTICLE,
INIT_LEDGER,
INIT_NUMBERS,
INIT_GET_NEXT_NUMBER_WITH_PREFIX_AND_PADDING,
INIT_COMPANYUSER,

View File

@@ -33,7 +33,6 @@ SELECT
a.sku,
ma.amount,
ma.net,
ma.transactedamount AS transactedAmount,
ma.rebate,
t.rate AS tax,
ma.comment
@@ -79,7 +78,6 @@ INSERT INTO manifestarticle (
article_id,
amount,
net,
transactedamount,
comment,
rebate,
tax_id
@@ -90,7 +88,6 @@ VALUES (
@articleId,
@amount,
@net,
@transactedAmount,
@comment,
@rebate,
@taxId

View File

@@ -8,7 +8,8 @@ export const ADDRESS_COLUMNS = `
zip,
city,
county,
addresstype AS addressType
addresstype AS addressType,
sort
`;
export const LIST_BY_ENTITY = `
@@ -16,7 +17,7 @@ SELECT
${ADDRESS_COLUMNS}
FROM entityaddress
WHERE entity_id = @entityId
ORDER BY id
ORDER BY sort ASC, id ASC
`;
export const GET_BY_ENTITY_AND_ID = `
@@ -26,6 +27,12 @@ FROM entityaddress
WHERE id = @id AND entity_id = @entityId
`;
export const GET_NEXT_SORT = `
SELECT ISNULL(MAX(sort), -1) + 1 AS sort
FROM entityaddress
WHERE entity_id = @entityId
`;
export const CREATE = `
INSERT INTO entityaddress (
entity_id,
@@ -36,7 +43,8 @@ INSERT INTO entityaddress (
zip,
city,
county,
addresstype
addresstype,
sort
)
OUTPUT
INSERTED.id,
@@ -48,7 +56,8 @@ OUTPUT
INSERTED.zip,
INSERTED.city,
INSERTED.county,
INSERTED.addresstype AS addressType
INSERTED.addresstype AS addressType,
INSERTED.sort
VALUES (
@entityId,
@firstName,
@@ -58,7 +67,8 @@ VALUES (
@zip,
@city,
@county,
@addressType
@addressType,
@sort
)
`;
@@ -72,7 +82,8 @@ SET
zip = @zip,
city = @city,
county = @county,
addresstype = @addressType
addresstype = @addressType,
sort = @sort
OUTPUT
INSERTED.id,
INSERTED.entity_id AS entityId,
@@ -83,7 +94,8 @@ OUTPUT
INSERTED.zip,
INSERTED.city,
INSERTED.county,
INSERTED.addresstype AS addressType
INSERTED.addresstype AS addressType,
INSERTED.sort
WHERE id = @id AND entity_id = @entityId
`;

View File

@@ -2,7 +2,8 @@ export const CONTACT_COLUMNS = `
id,
entity_id AS entityId,
contacttype AS contactType,
contact
contact,
sort
`;
export const LIST_BY_ENTITY = `
@@ -10,7 +11,7 @@ SELECT
${CONTACT_COLUMNS}
FROM entitycontact
WHERE entity_id = @entityId
ORDER BY id
ORDER BY sort ASC, id ASC
`;
export const GET_BY_ENTITY_AND_ID = `
@@ -20,24 +21,32 @@ FROM entitycontact
WHERE id = @id AND entity_id = @entityId
`;
export const GET_NEXT_SORT = `
SELECT ISNULL(MAX(sort), -1) + 1 AS sort
FROM entitycontact
WHERE entity_id = @entityId
`;
export const CREATE = `
INSERT INTO entitycontact (entity_id, contacttype, contact)
INSERT INTO entitycontact (entity_id, contacttype, contact, sort)
OUTPUT
INSERTED.id,
INSERTED.entity_id AS entityId,
INSERTED.contacttype AS contactType,
INSERTED.contact
VALUES (@entityId, @contactType, @contact)
INSERTED.contact,
INSERTED.sort
VALUES (@entityId, @contactType, @contact, @sort)
`;
export const UPDATE = `
UPDATE entitycontact
SET contacttype = @contactType, contact = @contact
SET contacttype = @contactType, contact = @contact, sort = @sort
OUTPUT
INSERTED.id,
INSERTED.entity_id AS entityId,
INSERTED.contacttype AS contactType,
INSERTED.contact
INSERTED.contact,
INSERTED.sort
WHERE id = @id AND entity_id = @entityId
`;

View File

@@ -6,6 +6,7 @@ export const TABLES = [
'posterminalcategory',
'posterminal',
'numbers',
'ledger',
'manifestarticle',
'manifest',
'price',

View File

@@ -22,7 +22,6 @@ export function shapeLine(row) {
sku: row.sku ?? null,
amount: row.amount,
net: row.net,
transactedAmount: row.transactedAmount,
rebate: row.rebate,
tax: row.tax,
comment: row.comment ?? null,
@@ -80,10 +79,6 @@ export function normalizeLines(lines) {
const rebate =
entry?.rebate === undefined || entry?.rebate === null ? 0 : entry.rebate;
const tax = entry?.tax;
const transactedAmount =
entry?.transactedAmount === undefined || entry?.transactedAmount === null
? amount
: entry.transactedAmount;
const comment =
entry?.comment === undefined || entry?.comment === null
? null
@@ -93,12 +88,11 @@ export function normalizeLines(lines) {
!Number.isFinite(amount) ||
!Number.isFinite(net) ||
!Number.isFinite(rebate) ||
!Number.isFinite(tax) ||
!Number.isFinite(transactedAmount)
!Number.isFinite(tax)
) {
throw new Error('Invalid lines');
}
return { sku, amount, net, rebate, tax, transactedAmount, comment };
return { sku, amount, net, rebate, tax, comment };
});
}

View File

@@ -57,7 +57,6 @@ export async function insertLines(
.input('articleId', articleId)
.input('amount', line.amount)
.input('net', line.net)
.input('transactedAmount', line.transactedAmount)
.input('comment', line.comment)
.input('rebate', line.rebate)
.input('taxId', taxId)

View File

@@ -1,4 +1,4 @@
import { CREATE } from '../../sql/entityaddress.js';
import { CREATE, GET_NEXT_SORT } from '../../sql/entityaddress.js';
import resolveCompanyAccess from '../companyuser/resolveCompanyAccess.js';
import resolveEntityAccess from '../entity/resolveEntityAccess.js';
@@ -14,6 +14,7 @@ function shape(row) {
city: row.city ?? null,
county: row.county ?? null,
addressType: row.addressType,
sort: row.sort,
};
}
@@ -38,6 +39,17 @@ export default function create(pool) {
await access(googleAuthId, companyId);
await entityAccess(companyId, entityId);
let sort = input.sort;
if (sort === undefined || sort === null) {
const next = await pool
.request()
.input('entityId', entityId)
.query(GET_NEXT_SORT);
sort = next.recordset[0].sort;
} else if (!Number.isFinite(sort)) {
throw new Error('Invalid sort');
}
const result = await pool
.request()
.input('entityId', entityId)
@@ -49,6 +61,7 @@ export default function create(pool) {
.input('city', optionalString(input.city))
.input('county', optionalString(input.county))
.input('addressType', addressType)
.input('sort', sort)
.query(CREATE);
return shape(result.recordset[0]);

View File

@@ -14,6 +14,7 @@ function shape(row) {
city: row.city ?? null,
county: row.county ?? null,
addressType: row.addressType,
sort: row.sort,
};
}

View File

@@ -14,6 +14,7 @@ function shape(row) {
city: row.city ?? null,
county: row.county ?? null,
addressType: row.addressType,
sort: row.sort,
};
}
@@ -81,6 +82,14 @@ export default function update(pool) {
}
}
let sort = current.sort;
if (Object.prototype.hasOwnProperty.call(patch, 'sort')) {
if (!Number.isFinite(patch.sort)) {
throw new Error('Invalid sort');
}
sort = patch.sort;
}
const result = await pool
.request()
.input('id', id)
@@ -93,6 +102,7 @@ export default function update(pool) {
.input('city', city)
.input('county', county)
.input('addressType', addressType)
.input('sort', sort)
.query(UPDATE);
const row = result.recordset[0];

View File

@@ -1,4 +1,4 @@
import { CREATE } from '../../sql/entitycontact.js';
import { CREATE, GET_NEXT_SORT } from '../../sql/entitycontact.js';
import resolveCompanyAccess from '../companyuser/resolveCompanyAccess.js';
import resolveEntityAccess from '../entity/resolveEntityAccess.js';
@@ -8,6 +8,7 @@ function shape(row) {
entityId: row.entityId,
contactType: row.contactType,
contact: row.contact,
sort: row.sort,
};
}
@@ -27,11 +28,23 @@ export default function create(pool) {
await access(googleAuthId, companyId);
await entityAccess(companyId, entityId);
let sort = input.sort;
if (sort === undefined || sort === null) {
const next = await pool
.request()
.input('entityId', entityId)
.query(GET_NEXT_SORT);
sort = next.recordset[0].sort;
} else if (!Number.isFinite(sort)) {
throw new Error('Invalid sort');
}
const result = await pool
.request()
.input('entityId', entityId)
.input('contactType', contactType)
.input('contact', contact)
.input('sort', sort)
.query(CREATE);
return shape(result.recordset[0]);

View File

@@ -8,6 +8,7 @@ function shape(row) {
entityId: row.entityId,
contactType: row.contactType,
contact: row.contact,
sort: row.sort,
};
}

View File

@@ -8,6 +8,7 @@ function shape(row) {
entityId: row.entityId,
contactType: row.contactType,
contact: row.contact,
sort: row.sort,
};
}
@@ -50,12 +51,21 @@ export default function update(pool) {
throw new Error('Contact required');
}
let sort = current.sort;
if (Object.prototype.hasOwnProperty.call(patch, 'sort')) {
if (!Number.isFinite(patch.sort)) {
throw new Error('Invalid sort');
}
sort = patch.sort;
}
const result = await pool
.request()
.input('id', id)
.input('entityId', entityId)
.input('contactType', contactType)
.input('contact', contact)
.input('sort', sort)
.query(UPDATE);
const row = result.recordset[0];