Release 0.2.22: capital chart, Investor View, GP stakes, doc folders
Snapshot commit bringing the uncommitted phase-2 work into version control
together with four new features and the 0.2.22 version bump.
New features:
- Investor capital-over-time chart (value, paid-in, distributions per
quarter), rendered from existing capital-account history.
- Admin Investor View: read-only reconstruction of an investor's portal
(GET /api/users/{id}/investor-view), reusing the investor portal UI.
- Document upload scoped to the selected fund's own investors, with an
explicit upload-target confirmation to prevent mis-attaching.
- GP/mgmt entities gain an Assets tab listing their stakes in the funds
they manage (new entity_stakes table + /api/entities/{id}/stakes).
- Edit-entity form (change type/status/etc.), so GP entities can be
categorized correctly.
Verified: 11/11 backend tests pass; alembic upgrades to head b8c9d0e1f2a3;
frontend tsc + vite build clean; s9pk packs at 0.2.22:0 (x86_64).
Also: ignore .DS_Store and *.s9pk artifacts.
This commit is contained in:
@@ -0,0 +1,101 @@
|
||||
"""investor access: username, entity access, documents, capital accounts
|
||||
|
||||
Revision ID: a1b2c3d4e5f6
|
||||
Revises: 2792c4ff4612
|
||||
Create Date: 2026-06-26 14:30:00.000000
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
import sqlmodel
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = 'a1b2c3d4e5f6'
|
||||
down_revision: Union[str, None] = '2792c4ff4612'
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
"""Upgrade schema."""
|
||||
# users: add username (login handle), make email optional.
|
||||
with op.batch_alter_table('users', schema=None) as batch_op:
|
||||
batch_op.add_column(
|
||||
sa.Column('username', sqlmodel.sql.sqltypes.AutoString(), nullable=True)
|
||||
)
|
||||
batch_op.alter_column('email', existing_type=sa.String(), nullable=True)
|
||||
|
||||
# Backfill username for any existing internal accounts so the NOT NULL holds.
|
||||
op.execute("UPDATE users SET username = email WHERE username IS NULL")
|
||||
|
||||
with op.batch_alter_table('users', schema=None) as batch_op:
|
||||
batch_op.alter_column(
|
||||
'username',
|
||||
existing_type=sqlmodel.sql.sqltypes.AutoString(),
|
||||
nullable=False,
|
||||
)
|
||||
batch_op.create_unique_constraint('uq_users_username', ['username'])
|
||||
|
||||
op.create_table(
|
||||
'entity_access',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('user_id', sa.Integer(), nullable=False),
|
||||
sa.Column('entity_id', sa.Integer(), nullable=False),
|
||||
sa.Column('created_at', sa.DateTime(), nullable=False),
|
||||
sa.ForeignKeyConstraint(['entity_id'], ['entities.id'], ),
|
||||
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
sa.UniqueConstraint('user_id', 'entity_id', name='uq_access_user_entity'),
|
||||
)
|
||||
|
||||
op.create_table(
|
||||
'documents',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('entity_id', sa.Integer(), nullable=False),
|
||||
sa.Column('investor_user_id', sa.Integer(), nullable=True),
|
||||
sa.Column('category', sa.Enum('capital_account', 'k1', 'statement', 'tax', 'other', name='documentcategory'), nullable=False),
|
||||
sa.Column('title', sqlmodel.sql.sqltypes.AutoString(), nullable=False),
|
||||
sa.Column('original_filename', sqlmodel.sql.sqltypes.AutoString(), nullable=False),
|
||||
sa.Column('content_type', sqlmodel.sql.sqltypes.AutoString(), nullable=False),
|
||||
sa.Column('size_bytes', sa.Integer(), nullable=False),
|
||||
sa.Column('storage_path', sqlmodel.sql.sqltypes.AutoString(), nullable=False),
|
||||
sa.Column('uploaded_by', sa.Integer(), nullable=True),
|
||||
sa.Column('created_at', sa.DateTime(), nullable=False),
|
||||
sa.ForeignKeyConstraint(['entity_id'], ['entities.id'], ),
|
||||
sa.ForeignKeyConstraint(['investor_user_id'], ['users.id'], ),
|
||||
sa.ForeignKeyConstraint(['uploaded_by'], ['users.id'], ),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
)
|
||||
|
||||
op.create_table(
|
||||
'capital_account_statements',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('entity_id', sa.Integer(), nullable=False),
|
||||
sa.Column('investor_user_id', sa.Integer(), nullable=False),
|
||||
sa.Column('as_of_date', sa.Date(), nullable=False),
|
||||
sa.Column('beginning_balance_cents', sa.Integer(), nullable=False),
|
||||
sa.Column('contributions_cents', sa.Integer(), nullable=False),
|
||||
sa.Column('distributions_cents', sa.Integer(), nullable=False),
|
||||
sa.Column('ending_balance_cents', sa.Integer(), nullable=False),
|
||||
sa.Column('document_id', sa.Integer(), nullable=True),
|
||||
sa.Column('created_at', sa.DateTime(), nullable=False),
|
||||
sa.ForeignKeyConstraint(['document_id'], ['documents.id'], ),
|
||||
sa.ForeignKeyConstraint(['entity_id'], ['entities.id'], ),
|
||||
sa.ForeignKeyConstraint(['investor_user_id'], ['users.id'], ),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
sa.UniqueConstraint('entity_id', 'investor_user_id', 'as_of_date', name='uq_capacct_entity_investor_date'),
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
"""Downgrade schema."""
|
||||
op.drop_table('capital_account_statements')
|
||||
op.drop_table('documents')
|
||||
op.drop_table('entity_access')
|
||||
with op.batch_alter_table('users', schema=None) as batch_op:
|
||||
batch_op.drop_constraint('uq_users_username', type_='unique')
|
||||
batch_op.alter_column('email', existing_type=sa.String(), nullable=False)
|
||||
batch_op.drop_column('username')
|
||||
Reference in New Issue
Block a user