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
130 lines
3.9 KiB
Plaintext
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])
|
|
}
|