Use Zammad IDs for organization links

This commit is contained in:
2026-08-11 14:47:11 +02:00
parent dcc115a82c
commit a760f423a4
2 changed files with 120 additions and 44 deletions
+39 -39
View File
@@ -132,17 +132,17 @@ async function setAppSetting(key: string, value: string) {
); );
} }
async function getOrganizationById( async function getOrganizationByZammadId(
client: { query: (text: string, params?: unknown[]) => Promise<{ rows: OrganizationRow[]; rowCount: number | null }> }, client: { query: (text: string, params?: unknown[]) => Promise<{ rows: OrganizationRow[]; rowCount: number | null }> },
organizationId: number zammadId: number
) { ) {
const result = await client.query( const result = await client.query(
` `
SELECT id, zammad_id, name, synced_at SELECT zammad_id::text AS id, zammad_id::text AS zammad_id, name, synced_at
FROM organizations FROM organizations
WHERE id = $1; WHERE zammad_id = $1;
`, `,
[organizationId] [zammadId]
); );
return result.rows[0] ?? null; return result.rows[0] ?? null;
@@ -332,7 +332,7 @@ async function ensureRecurringSessionsForUserPeriod(userId: string, startIso: st
rb.valid_until::text, rb.valid_until::text,
rb.start_time::text rb.start_time::text
FROM recurring_billings rb FROM recurring_billings rb
JOIN organizations o ON o.id = rb.organization_id JOIN organizations o ON o.zammad_id = rb.organization_id
WHERE rb.user_id = $1 WHERE rb.user_id = $1
AND rb.valid_from < $3::date AND rb.valid_from < $3::date
AND (rb.valid_until IS NULL OR rb.valid_until >= $2::date); AND (rb.valid_until IS NULL OR rb.valid_until >= $2::date);
@@ -558,7 +558,7 @@ async function getPeriodOverview(config: PeriodConfig, period: ParsedPeriod, use
NULL::timestamptz AS closed_at NULL::timestamptz AS closed_at
FROM tickets t FROM tickets t
JOIN sessions s ON s.ticket_id = t.id JOIN sessions s ON s.ticket_id = t.id
LEFT JOIN organizations o ON o.id = t.organization_id LEFT JOIN organizations o ON o.zammad_id = t.organization_id
WHERE s.started_at >= $1::timestamptz AND s.started_at < $2::timestamptz WHERE s.started_at >= $1::timestamptz AND s.started_at < $2::timestamptz
AND s.user_id = $3 AND s.user_id = $3
GROUP BY GROUP BY
@@ -587,7 +587,7 @@ async function getPeriodOverview(config: PeriodConfig, period: ParsedPeriod, use
s.rounded_minutes s.rounded_minutes
FROM sessions s FROM sessions s
JOIN tickets t ON t.id = s.ticket_id JOIN tickets t ON t.id = s.ticket_id
LEFT JOIN organizations so ON so.id = s.organization_id LEFT JOIN organizations so ON so.zammad_id = s.organization_id
WHERE s.started_at >= $1::timestamptz WHERE s.started_at >= $1::timestamptz
AND s.started_at < $2::timestamptz AND s.started_at < $2::timestamptz
AND s.user_id = $3 AND s.user_id = $3
@@ -681,8 +681,8 @@ async function getStatisticsOverview(period: ParsedPeriod, userId: string) {
s.recurring_billing_id s.recurring_billing_id
FROM sessions s FROM sessions s
JOIN tickets t ON t.id = s.ticket_id JOIN tickets t ON t.id = s.ticket_id
LEFT JOIN organizations so ON so.id = s.organization_id LEFT JOIN organizations so ON so.zammad_id = s.organization_id
LEFT JOIN organizations ot ON ot.id = t.organization_id LEFT JOIN organizations ot ON ot.zammad_id = t.organization_id
WHERE s.started_at >= $1::timestamptz WHERE s.started_at >= $1::timestamptz
AND s.started_at < $2::timestamptz AND s.started_at < $2::timestamptz
AND s.user_id = $3 AND s.user_id = $3
@@ -904,8 +904,8 @@ async function getStatisticsOverview(period: ParsedPeriod, userId: string) {
s.rounded_minutes s.rounded_minutes
FROM sessions s FROM sessions s
JOIN tickets t ON t.id = s.ticket_id JOIN tickets t ON t.id = s.ticket_id
LEFT JOIN organizations so ON so.id = s.organization_id LEFT JOIN organizations so ON so.zammad_id = s.organization_id
LEFT JOIN organizations o ON o.id = t.organization_id LEFT JOIN organizations o ON o.zammad_id = t.organization_id
WHERE s.started_at >= $1::timestamptz WHERE s.started_at >= $1::timestamptz
AND s.started_at < $2::timestamptz AND s.started_at < $2::timestamptz
AND s.user_id = $3 AND s.user_id = $3
@@ -1066,7 +1066,7 @@ async function getPeriodTicket(config: PeriodConfig, period: ParsedPeriod, ticke
COALESCE(period_sessions.open_count, 0)::int AS ticket_open_count, COALESCE(period_sessions.open_count, 0)::int AS ticket_open_count,
COALESCE(period_sessions.manual_session_count, 0)::int AS ticket_manual_session_count COALESCE(period_sessions.manual_session_count, 0)::int AS ticket_manual_session_count
FROM tickets t FROM tickets t
LEFT JOIN organizations o ON o.id = t.organization_id LEFT JOIN organizations o ON o.zammad_id = t.organization_id
${periodClosureJoin} ${periodClosureJoin}
LEFT JOIN LATERAL ( LEFT JOIN LATERAL (
SELECT SELECT
@@ -1096,13 +1096,13 @@ async function getPeriodTicket(config: PeriodConfig, period: ParsedPeriod, ticke
( (
SELECT name SELECT name
FROM organizations FROM organizations
WHERE id = sessions.organization_id WHERE zammad_id = sessions.organization_id
) AS organization_name, ) AS organization_name,
COALESCE( COALESCE(
( (
SELECT name SELECT name
FROM organizations FROM organizations
WHERE id = sessions.organization_id WHERE zammad_id = sessions.organization_id
), ),
customer_name customer_name
) AS customer_name, ) AS customer_name,
@@ -1473,7 +1473,7 @@ app.get("/api/organizations", async (req, res) => {
const result = await query<OrganizationRow>( const result = await query<OrganizationRow>(
` `
SELECT id, zammad_id, name, synced_at SELECT zammad_id::text AS id, zammad_id::text AS zammad_id, name, synced_at
FROM organizations FROM organizations
${where.length > 0 ? `WHERE ${where.join(" AND ")}` : ""} ${where.length > 0 ? `WHERE ${where.join(" AND ")}` : ""}
ORDER BY lower(name) ASC ORDER BY lower(name) ASC
@@ -1596,30 +1596,30 @@ app.post("/api/admin/zammad/organizations/sync", requireAdmin, async (req, res)
{ {
const staleResult = const staleResult =
zammadIds.length > 0 zammadIds.length > 0
? await client.query<{ id: string }>( ? await client.query<{ zammad_id: string }>(
` `
SELECT id SELECT zammad_id::text AS zammad_id
FROM organizations FROM organizations
WHERE NOT (zammad_id = ANY($1::bigint[])); WHERE NOT (zammad_id = ANY($1::bigint[]));
`, `,
[zammadIds] [zammadIds]
) )
: await client.query<{ id: string }>( : await client.query<{ zammad_id: string }>(
` `
SELECT id SELECT zammad_id::text AS zammad_id
FROM organizations; FROM organizations;
` `
); );
const staleIds = staleResult.rows.map((row) => row.id); const staleZammadIds = staleResult.rows.map((row) => row.zammad_id);
if (staleIds.length > 0) { if (staleZammadIds.length > 0) {
const ticketResult = await client.query( const ticketResult = await client.query(
` `
UPDATE tickets UPDATE tickets
SET organization_id = NULL SET organization_id = NULL
WHERE organization_id = ANY($1::bigint[]); WHERE organization_id = ANY($1::bigint[]);
`, `,
[staleIds] [staleZammadIds]
); );
const sessionResult = await client.query( const sessionResult = await client.query(
` `
@@ -1627,14 +1627,14 @@ app.post("/api/admin/zammad/organizations/sync", requireAdmin, async (req, res)
SET organization_id = NULL SET organization_id = NULL
WHERE organization_id = ANY($1::bigint[]); WHERE organization_id = ANY($1::bigint[]);
`, `,
[staleIds] [staleZammadIds]
); );
const deleteResult = await client.query( const deleteResult = await client.query(
` `
DELETE FROM organizations DELETE FROM organizations
WHERE id = ANY($1::bigint[]); WHERE zammad_id = ANY($1::bigint[]);
`, `,
[staleIds] [staleZammadIds]
); );
unlinkedTickets = ticketResult.rowCount ?? 0; unlinkedTickets = ticketResult.rowCount ?? 0;
@@ -1980,8 +1980,8 @@ app.get("/api/export/months/:month", requireUser, async (req, res) => {
s.activity s.activity
FROM sessions s FROM sessions s
JOIN tickets t ON t.id = s.ticket_id JOIN tickets t ON t.id = s.ticket_id
LEFT JOIN organizations so ON so.id = s.organization_id LEFT JOIN organizations so ON so.zammad_id = s.organization_id
LEFT JOIN organizations ot ON ot.id = t.organization_id LEFT JOIN organizations ot ON ot.zammad_id = t.organization_id
LEFT JOIN ticket_day_billings tdb LEFT JOIN ticket_day_billings tdb
ON tdb.ticket_id = s.ticket_id ON tdb.ticket_id = s.ticket_id
AND tdb.user_id = s.user_id AND tdb.user_id = s.user_id
@@ -2149,7 +2149,7 @@ async function recurringBillingResponse(userId: string) {
) AS slots ) AS slots
FROM recurring_billings rb FROM recurring_billings rb
JOIN users owner ON owner.id = rb.user_id JOIN users owner ON owner.id = rb.user_id
JOIN organizations o ON o.id = rb.organization_id JOIN organizations o ON o.zammad_id = rb.organization_id
LEFT JOIN recurring_billing_slots rbs ON rbs.recurring_billing_id = rb.id LEFT JOIN recurring_billing_slots rbs ON rbs.recurring_billing_id = rb.id
WHERE rb.user_id = $1 WHERE rb.user_id = $1
GROUP BY rb.id, owner.username, owner.display_name, o.name GROUP BY rb.id, owner.username, owner.display_name, o.name
@@ -2170,7 +2170,7 @@ app.post("/api/recurring-billings", requireUser, async (req, res) => {
const payload = parseRecurringBillingPayload(req.body); const payload = parseRecurringBillingPayload(req.body);
const created = await withTransaction(async (client) => { const created = await withTransaction(async (client) => {
const organization = await getOrganizationById(client, payload.organizationId); const organization = await getOrganizationByZammadId(client, payload.organizationId);
if (!organization) { if (!organization) {
return "organization-not-found" as const; return "organization-not-found" as const;
@@ -2272,9 +2272,9 @@ app.patch("/api/recurring-billings/:billingId", requireUser, async (req, res) =>
` `
SELECT id SELECT id
FROM recurring_billings FROM recurring_billings
WHERE s.id = $1 WHERE id = $1
AND s.user_id = $2 AND user_id = $2
FOR UPDATE OF s; FOR UPDATE;
`, `,
[billingId, userId] [billingId, userId]
); );
@@ -2283,7 +2283,7 @@ app.patch("/api/recurring-billings/:billingId", requireUser, async (req, res) =>
return { status: "not-found" as const }; return { status: "not-found" as const };
} }
const organization = await getOrganizationById(client, payload.organizationId); const organization = await getOrganizationByZammadId(client, payload.organizationId);
if (!organization) { if (!organization) {
return { status: "organization-not-found" as const }; return { status: "organization-not-found" as const };
@@ -2579,7 +2579,7 @@ app.post("/api/sessions", requireUser, async (req, res) => {
throw badRequest("organizationId is required"); throw badRequest("organizationId is required");
} }
const organization = await getOrganizationById(client, effectiveOrganizationId); const organization = await getOrganizationByZammadId(client, effectiveOrganizationId);
if (!organization) { if (!organization) {
throw badRequest("organizationId must reference an existing organization"); throw badRequest("organizationId must reference an existing organization");
@@ -2670,7 +2670,7 @@ app.get("/api/tickets/lookup", requireUser, async (req, res) => {
COALESCE(o.name, t.customer_name) AS customer_name, COALESCE(o.name, t.customer_name) AS customer_name,
t.work_type t.work_type
FROM tickets t FROM tickets t
LEFT JOIN organizations o ON o.id = t.organization_id LEFT JOIN organizations o ON o.zammad_id = t.organization_id
WHERE t.ticket_number = $1; WHERE t.ticket_number = $1;
`, `,
[ticketNumber] [ticketNumber]
@@ -2709,7 +2709,7 @@ app.get("/api/tickets/search", requireUser, async (req, res) => {
COALESCE(SUM(s.rounded_minutes), 0)::int AS total_minutes COALESCE(SUM(s.rounded_minutes), 0)::int AS total_minutes
FROM tickets t FROM tickets t
JOIN sessions s ON s.ticket_id = t.id JOIN sessions s ON s.ticket_id = t.id
LEFT JOIN organizations o ON o.id = t.organization_id LEFT JOIN organizations o ON o.zammad_id = t.organization_id
WHERE s.user_id = $1 WHERE s.user_id = $1
AND ( AND (
t.ticket_number ILIKE $2 t.ticket_number ILIKE $2
@@ -2745,7 +2745,7 @@ app.patch("/api/tickets/:ticketId", requireUser, async (req, res) => {
try { try {
const result = await withTransaction(async (client) => { const result = await withTransaction(async (client) => {
const organization = await getOrganizationById(client, organizationId); const organization = await getOrganizationByZammadId(client, organizationId);
if (!organization) { if (!organization) {
return "organization-not-found" as const; return "organization-not-found" as const;
@@ -3232,7 +3232,7 @@ app.patch("/api/sessions/:sessionId/details", requireUser, async (req, res) => {
const roundedMinutes = Math.max(1, Math.round(durationSeconds / 60)); const roundedMinutes = Math.max(1, Math.round(durationSeconds / 60));
const updated = await withTransaction(async (client) => { const updated = await withTransaction(async (client) => {
const organization = await getOrganizationById(client, organizationId); const organization = await getOrganizationByZammadId(client, organizationId);
if (!organization) { if (!organization) {
return "organization-not-found" as const; return "organization-not-found" as const;
+81 -5
View File
@@ -97,7 +97,7 @@ export async function migrate() {
CREATE TABLE IF NOT EXISTS tickets ( CREATE TABLE IF NOT EXISTS tickets (
id BIGSERIAL PRIMARY KEY, id BIGSERIAL PRIMARY KEY,
ticket_number TEXT NOT NULL UNIQUE, ticket_number TEXT NOT NULL UNIQUE,
organization_id BIGINT REFERENCES organizations(id), organization_id BIGINT REFERENCES organizations(zammad_id),
customer_name TEXT, customer_name TEXT,
work_type TEXT, work_type TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(), created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
@@ -107,7 +107,7 @@ export async function migrate() {
`); `);
await query("ALTER TABLE tickets ADD COLUMN IF NOT EXISTS customer_name TEXT;"); await query("ALTER TABLE tickets ADD COLUMN IF NOT EXISTS customer_name TEXT;");
await query("ALTER TABLE tickets ADD COLUMN IF NOT EXISTS organization_id BIGINT REFERENCES organizations(id);"); await query("ALTER TABLE tickets ADD COLUMN IF NOT EXISTS organization_id BIGINT REFERENCES organizations(zammad_id);");
await query("ALTER TABLE tickets ADD COLUMN IF NOT EXISTS work_type TEXT;"); await query("ALTER TABLE tickets ADD COLUMN IF NOT EXISTS work_type TEXT;");
await query(` await query(`
ALTER TABLE tickets ALTER TABLE tickets
@@ -130,7 +130,7 @@ export async function migrate() {
CREATE TABLE IF NOT EXISTS sessions ( CREATE TABLE IF NOT EXISTS sessions (
id BIGSERIAL PRIMARY KEY, id BIGSERIAL PRIMARY KEY,
ticket_id BIGINT NOT NULL REFERENCES tickets(id) ON DELETE CASCADE, ticket_id BIGINT NOT NULL REFERENCES tickets(id) ON DELETE CASCADE,
organization_id BIGINT REFERENCES organizations(id), organization_id BIGINT REFERENCES organizations(zammad_id),
customer_name TEXT NOT NULL, customer_name TEXT NOT NULL,
activity TEXT NOT NULL, activity TEXT NOT NULL,
work_type TEXT NOT NULL, work_type TEXT NOT NULL,
@@ -151,7 +151,7 @@ export async function migrate() {
`); `);
await query("ALTER TABLE sessions ADD COLUMN IF NOT EXISTS user_id BIGINT REFERENCES users(id);"); await query("ALTER TABLE sessions ADD COLUMN IF NOT EXISTS user_id BIGINT REFERENCES users(id);");
await query("ALTER TABLE sessions ADD COLUMN IF NOT EXISTS organization_id BIGINT REFERENCES organizations(id);"); await query("ALTER TABLE sessions ADD COLUMN IF NOT EXISTS organization_id BIGINT REFERENCES organizations(zammad_id);");
await query("ALTER TABLE sessions ADD COLUMN IF NOT EXISTS recurring_billing_id BIGINT;"); await query("ALTER TABLE sessions ADD COLUMN IF NOT EXISTS recurring_billing_id BIGINT;");
await query("ALTER TABLE sessions ADD COLUMN IF NOT EXISTS recurring_billing_slot_id BIGINT;"); await query("ALTER TABLE sessions ADD COLUMN IF NOT EXISTS recurring_billing_slot_id BIGINT;");
await query("ALTER TABLE sessions ADD COLUMN IF NOT EXISTS recurring_occurrence_date DATE;"); await query("ALTER TABLE sessions ADD COLUMN IF NOT EXISTS recurring_occurrence_date DATE;");
@@ -173,7 +173,7 @@ export async function migrate() {
id BIGSERIAL PRIMARY KEY, id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE, user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
ticket_number TEXT, ticket_number TEXT,
organization_id BIGINT NOT NULL REFERENCES organizations(id), organization_id BIGINT NOT NULL REFERENCES organizations(zammad_id),
activity TEXT NOT NULL, activity TEXT NOT NULL,
work_type TEXT NOT NULL, work_type TEXT NOT NULL,
recurrence_type TEXT NOT NULL, recurrence_type TEXT NOT NULL,
@@ -191,6 +191,82 @@ export async function migrate() {
); );
`); `);
await query(`
ALTER TABLE tickets DROP CONSTRAINT IF EXISTS tickets_organization_id_fkey;
ALTER TABLE sessions DROP CONSTRAINT IF EXISTS sessions_organization_id_fkey;
ALTER TABLE recurring_billings DROP CONSTRAINT IF EXISTS recurring_billings_organization_id_fkey;
`);
await query(`
UPDATE tickets t
SET organization_id = o.zammad_id
FROM organizations o
WHERE t.organization_id = o.id
AND NOT EXISTS (
SELECT 1
FROM organizations current_org
WHERE current_org.zammad_id = t.organization_id
);
`);
await query(`
UPDATE sessions s
SET organization_id = o.zammad_id
FROM organizations o
WHERE s.organization_id = o.id
AND NOT EXISTS (
SELECT 1
FROM organizations current_org
WHERE current_org.zammad_id = s.organization_id
);
`);
await query(`
UPDATE recurring_billings rb
SET organization_id = o.zammad_id
FROM organizations o
WHERE rb.organization_id = o.id
AND NOT EXISTS (
SELECT 1
FROM organizations current_org
WHERE current_org.zammad_id = rb.organization_id
);
`);
await query(`
UPDATE tickets
SET organization_id = NULL
WHERE organization_id IS NOT NULL
AND NOT EXISTS (
SELECT 1
FROM organizations o
WHERE o.zammad_id = tickets.organization_id
);
`);
await query(`
UPDATE sessions
SET organization_id = NULL
WHERE organization_id IS NOT NULL
AND NOT EXISTS (
SELECT 1
FROM organizations o
WHERE o.zammad_id = sessions.organization_id
);
`);
await query(`
ALTER TABLE tickets
ADD CONSTRAINT tickets_organization_id_fkey
FOREIGN KEY (organization_id) REFERENCES organizations(zammad_id);
ALTER TABLE sessions
ADD CONSTRAINT sessions_organization_id_fkey
FOREIGN KEY (organization_id) REFERENCES organizations(zammad_id);
ALTER TABLE recurring_billings
ADD CONSTRAINT recurring_billings_organization_id_fkey
FOREIGN KEY (organization_id) REFERENCES organizations(zammad_id);
`);
await query("ALTER TABLE recurring_billings ALTER COLUMN ticket_number DROP NOT NULL;"); await query("ALTER TABLE recurring_billings ALTER COLUMN ticket_number DROP NOT NULL;");
await query("ALTER TABLE recurring_billings DROP CONSTRAINT IF EXISTS recurring_billings_ticket_number_format;"); await query("ALTER TABLE recurring_billings DROP CONSTRAINT IF EXISTS recurring_billings_ticket_number_format;");
await query("UPDATE recurring_billings SET recurrence_type = 'every_n_weeks' WHERE recurrence_type = 'every_n_days';"); await query("UPDATE recurring_billings SET recurrence_type = 'every_n_weeks' WHERE recurrence_type = 'every_n_days';");