Architektur: Craftvia-Domänenmodell, Verträge und Team-Schnitte

- Migration 0002_craftvia_domain: 27 Fachtabellen inkl. RLS (enable_tenant_rls)
- TENANT_MODELS (db.ts, backup/topology.ts) um alle Fachmodelle ergänzt
- moduleGuard liefert DB-autoritative Rechte; ServiceCtx für Domänen-Services
- Verträge: Statusmaschine, Events, Nummernkreise, Sichtbarkeits-Scopes,
  Job-Queues + Worker, KI-Provider-Interfaces, Sync-Envelope
- docs/craftvia/ARCHITEKTUR.md mit Lanes, Ownership und DoD

Gate: tsc, lint, build, 22/22 Tests grün.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-14 11:49:21 +02:00
co-authored by Claude Opus 5
parent 1701db0a62
commit bf4456718e
21 changed files with 2670 additions and 12 deletions
+41
View File
@@ -0,0 +1,41 @@
import type { TenantDb } from "@/server/db";
export type NumberKey = "customer" | "work_order" | "emergency" | "report";
const DEFAULT_PREFIX: Record<NumberKey, string> = {
customer: "K-",
work_order: "A-",
emergency: "N-",
report: "B-",
};
/**
* Allocate the next number of a tenant sequence, e.g. "A-00042".
* The increment is a single UPDATE … RETURNING (atomic); the very first call per key
* creates the row and retries once if a concurrent call created it first.
*/
export async function nextNumber(db: TenantDb, tenantId: string, key: NumberKey): Promise<string> {
for (let attempt = 0; attempt < 3; attempt++) {
const existing = await db.numberSequence.findFirst({ where: { tenantId, key }, select: { id: true } });
if (existing) {
const seq = await db.numberSequence.update({
where: { id: existing.id },
data: { nextValue: { increment: 1 } },
});
return format(seq.prefix, seq.nextValue - 1, seq.padding);
}
try {
const seq = await db.numberSequence.create({
data: { tenantId, key, prefix: DEFAULT_PREFIX[key], nextValue: 2 },
});
return format(seq.prefix, 1, seq.padding);
} catch (err) {
if ((err as { code?: string }).code !== "P2002") throw err; // unique race → retry via update path
}
}
throw new Error(`could not allocate number for ${key}`);
}
function format(prefix: string, value: number, padding: number): string {
return `${prefix}${String(value).padStart(padding, "0")}`;
}