Files
cross-financial-management/manager/prisma/schema.prisma
T
Joshua OosthuizenandClaude Sonnet 5 4acbc03b35 Initial commit: multi-company financial management system
Root company app (src/, client/) — GL foundation, AR, and AP:
- Segmented Chart of Accounts (Sage 300-style), double-entry PostingService with
  three-layer balance enforcement (app validation, CHECK constraints, deferred
  DB trigger), gap-free document numbering, RBAC with branch restrictions.
- Accounts Receivable: Customers, Invoices, Credit Notes, Receipts with
  allocation, all posting through PostingService.
- Accounts Payable: Suppliers, Supplier Invoices, Debit Notes, Payments with
  allocation, mirroring AR's posting pattern.
- Tax Codes shared between AR and AP.
- Company-app login is a token handoff from the Account Manager (RS256,
  single-use, company-scoped) rather than a local password.
- Frontend reskinned to the approved mockup: navy sidebar, dashboard, tree-view
  Chart of Accounts, Customer/Supplier 360 pages.

Account Manager (manager/, manager/client/) — central login gateway:
- Password + TOTP 2FA + Google/Microsoft OAuth, company picker, and the RS256
  handoff token issuance that the company app verifies.

Covered by unit + integration tests against a real Postgres database (GL
balance invariants, AR/AP posting correctness and allocation limits, the full
handoff exchange including replay/cross-company/expiry/forgery rejection).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tq2DbEvzHzs13jDFhLaX44
2026-09-03 13:27:19 +02:00

130 lines
3.9 KiB
Plaintext

// Central Account Manager — a platform-wide identity store (Person) and company registry,
// fully separate from any single company's own isolated database and User table.
// See the implementation plan, "Phase A — Account Manager: foundation & handoff".
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model Person {
id Int @id @default(autoincrement())
email String @unique
fullName String
isActive Boolean @default(true)
isOperator Boolean @default(false)
credential PasswordCredential?
twoFactor TwoFactorCredential?
oauthAccounts OAuthAccount[]
memberships PersonCompanyMembership[]
auditLogs ProvisioningAuditLog[]
createdAt DateTime @default(now())
}
model PasswordCredential {
id Int @id @default(autoincrement())
personId Int @unique
person Person @relation(fields: [personId], references: [id])
passwordHash String
updatedAt DateTime @updatedAt
}
model TwoFactorCredential {
id Int @id @default(autoincrement())
personId Int @unique
person Person @relation(fields: [personId], references: [id])
totpSecret String
backupCodes String[] // hashed, one-time use
isEnabled Boolean @default(false)
enabledAt DateTime?
}
enum OAuthProvider {
GOOGLE
MICROSOFT
}
model OAuthAccount {
id Int @id @default(autoincrement())
personId Int
person Person @relation(fields: [personId], references: [id])
provider OAuthProvider
providerAccountId String
email String
createdAt DateTime @default(now())
@@unique([provider, providerAccountId])
}
enum CompanyStatus {
PROVISIONING
ACTIVE
STOPPED
FAILED
DECOMMISSIONED
}
model Company {
id Int @id @default(autoincrement())
name String
slug String @unique
status CompanyStatus @default(PROVISIONING)
port Int? @unique
databaseName String? @unique
databaseUrl String? // plain column for now — see plan's "later hardening" note
pm2ProcessName String? @unique
frpProxyName String? @unique
frpToken String?
publicUrl String?
provisionedAt DateTime?
memberships PersonCompanyMembership[]
auditLogs ProvisioningAuditLog[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model PersonCompanyMembership {
id Int @id @default(autoincrement())
personId Int
person Person @relation(fields: [personId], references: [id])
companyId Int
company Company @relation(fields: [companyId], references: [id])
companyUserEmail String? // override; defaults to Person.email when resolving the company-local User
isActive Boolean @default(true)
createdAt DateTime @default(now())
@@unique([personId, companyId])
}
enum ProvisioningAction {
COMPANY_CREATED
DB_CREATED
MIGRATIONS_RUN
SEED_RUN
PM2_STARTED
PM2_STOPPED
PM2_RESTARTED
FRP_PROXY_REGISTERED
IMPORT_FILE_GENERATED
HANDOFF_ISSUED
COMPANY_DECOMMISSIONED
}
model ProvisioningAuditLog {
id Int @id @default(autoincrement())
companyId Int?
company Company? @relation(fields: [companyId], references: [id])
actorId Int?
actor Person? @relation(fields: [actorId], references: [id])
action ProvisioningAction
detail Json?
createdAt DateTime @default(now())
@@index([companyId])
@@index([action])
}