feat: multi-organization support + bank accounts

- Renamed singular /api/organization → CRUD /api/organizations
- New BankAccount table with CRUD under /api/organizations/:orgId/bank-accounts
- TochkaCredential gets optional bankAccountId for future per-account bank API config
- Active organization stored in cookie dm_org, /api/active-organization GET/POST
- activeOrgPlugin resolves req._orgId from cookie (or first non-archived as fallback)
- Migration 1_multiorg: BankAccount table + data backfill from legacy Organization.bank* fields
- Web: new /companies list + /companies/:id with tabs (Реквизиты, Банки и счета, Интеграции stub)
- Web: OrgSwitcher dropdown in header (active org + management link)
- Removed nav "Реквизиты", "Банк" — replaced by "Компании"
- Per-field error highlighting wired up on new forms

Existing organization data backfills cleanly: legacy bank* fields stay readable, but new
BankAccount becomes the source of truth going forward.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
admin
2026-05-01 11:08:26 +03:00
parent b28c0463b3
commit 524789facc
15 changed files with 909 additions and 200 deletions
@@ -0,0 +1,49 @@
-- AlterTable: add shortName, archivedAt to Organization
ALTER TABLE "Organization" ADD COLUMN "shortName" TEXT;
ALTER TABLE "Organization" ADD COLUMN "archivedAt" TIMESTAMP(3);
-- CreateTable: BankAccount
CREATE TABLE "BankAccount" (
"id" UUID NOT NULL,
"organizationId" UUID NOT NULL,
"name" TEXT NOT NULL,
"bankName" TEXT,
"bankBik" TEXT,
"accountNumber" TEXT,
"corrAccount" TEXT,
"currency" TEXT NOT NULL DEFAULT 'RUB',
"isPrimary" BOOLEAN NOT NULL DEFAULT false,
"archivedAt" TIMESTAMP(3),
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "BankAccount_pkey" PRIMARY KEY ("id")
);
CREATE INDEX "BankAccount_organizationId_idx" ON "BankAccount"("organizationId");
ALTER TABLE "BankAccount" ADD CONSTRAINT "BankAccount_organizationId_fkey"
FOREIGN KEY ("organizationId") REFERENCES "Organization"("id")
ON DELETE CASCADE ON UPDATE CASCADE;
-- AlterTable: add bankAccountId on TochkaCredential
ALTER TABLE "TochkaCredential" ADD COLUMN "bankAccountId" UUID;
ALTER TABLE "TochkaCredential" ADD CONSTRAINT "TochkaCredential_bankAccountId_fkey"
FOREIGN KEY ("bankAccountId") REFERENCES "BankAccount"("id")
ON DELETE SET NULL ON UPDATE CASCADE;
-- Data migration: для каждой организации с заполненным bank* — создать запись BankAccount(isPrimary=true)
INSERT INTO "BankAccount" ("id", "organizationId", "name", "bankName", "bankBik", "accountNumber", "currency", "isPrimary", "createdAt", "updatedAt")
SELECT
gen_random_uuid(),
o."id",
'Основной счёт',
o."bankName",
o."bankBik",
o."bankAccount",
'RUB',
true,
NOW(),
NOW()
FROM "Organization" o
WHERE o."bankName" IS NOT NULL OR o."bankBik" IS NOT NULL OR o."bankAccount" IS NOT NULL;
+27
View File
@@ -58,15 +58,19 @@ enum PaymentKind {
model Organization {
id String @id @default(uuid()) @db.Uuid
name String
shortName String? // короткое имя для бейджей в UI / селектора
inn String
kpp String?
ogrn String?
legalAddress String?
// Поля bankName/bankBik/bankAccount устарели после введения BankAccount.
// Оставлены для обратной совместимости с уже сохранёнными данными.
bankName String?
bankBik String?
bankAccount String?
signatoryName String?
signatoryPosition String?
archivedAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@ -77,6 +81,27 @@ model Organization {
payments Payment[]
tochkaCredentials TochkaCredential[]
auditLog AuditLog[]
bankAccounts BankAccount[]
}
model BankAccount {
id String @id @default(uuid()) @db.Uuid
organizationId String @db.Uuid
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
name String // отображаемое имя, напр. "Точка — основной"
bankName String?
bankBik String?
accountNumber String? // р/счёт (20 цифр)
corrAccount String? // к/счёт (20 цифр)
currency String @default("RUB")
isPrimary Boolean @default(false)
archivedAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
tochkaCredentials TochkaCredential[]
@@index([organizationId])
}
model Client {
@@ -209,6 +234,8 @@ model TochkaCredential {
id String @id @default(uuid()) @db.Uuid
organizationId String @db.Uuid
organization Organization @relation(fields: [organizationId], references: [id])
bankAccountId String? @db.Uuid
bankAccount BankAccount? @relation(fields: [bankAccountId], references: [id])
environment TochkaEnv
// AES-256-GCM ciphertext (iv|tag|ct), base64
jwtEncrypted String