Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
636 changes: 636 additions & 0 deletions .github/instructions/bomi_insdtr.instructions.md

Large diffs are not rendered by default.

506 changes: 506 additions & 0 deletions CONTEXT_FILES/prompt_upgrade

Large diffs are not rendered by default.

33 changes: 33 additions & 0 deletions alembic/versions/0008_add_alert_fields.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
"""add alert fields rule_code source_type acknowledged_at resolved_at occurrence_count

Revision ID: 0008
Revises: 0007_reconciliation_engine
Create Date: 2026-06-03 00:00:00.000000

"""
from alembic import op
import sqlalchemy as sa


revision = "0008"
down_revision = "0007_reconciliation_engine"
branch_labels = None
depends_on = None


def upgrade() -> None:
# Add new columns to alerts table
op.add_column('alerts', sa.Column('source_type', sa.String(64), nullable=True))
op.add_column('alerts', sa.Column('rule_code', sa.String(128), nullable=True))
op.add_column('alerts', sa.Column('occurrence_count', sa.Integer, nullable=False, server_default='1'))
op.add_column('alerts', sa.Column('acknowledged_at', sa.DateTime(timezone=True), nullable=True))
op.add_column('alerts', sa.Column('resolved_at', sa.DateTime(timezone=True), nullable=True))


def downgrade() -> None:
# Remove columns from alerts table
op.drop_column('alerts', 'resolved_at')
op.drop_column('alerts', 'acknowledged_at')
op.drop_column('alerts', 'occurrence_count')
op.drop_column('alerts', 'rule_code')
op.drop_column('alerts', 'source_type')
41 changes: 41 additions & 0 deletions alembic/versions/0009_bank_accounts.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
"""Add bank_accounts table

Revision ID: 0009_bank_accounts
Revises: 0008_add_alert_fields
Create Date: 2026-06-03 00:00:00.000000
"""

from alembic import op
import sqlalchemy as sa

revision = "0009_bank_accounts"
down_revision = "0008_add_alert_fields"
branch_labels = None
depends_on = None


def upgrade() -> None:
op.create_table(
"bank_accounts",
sa.Column("id", sa.CHAR(length=36), nullable=False),
sa.Column("merchant_id", sa.CHAR(length=36), nullable=False),
sa.Column("bank_name", sa.String(length=255), nullable=False),
sa.Column("bank_code", sa.String(length=64), nullable=True),
sa.Column("account_number_encrypted", sa.String(length=1024), nullable=False),
sa.Column("account_name", sa.String(length=255), nullable=False),
sa.Column("currency", sa.String(length=16), nullable=False, server_default="NGN"),
sa.Column("purpose", sa.String(length=32), nullable=False, server_default="settlement"),
sa.Column("verification_status", sa.String(length=32), nullable=False, server_default="unverified"),
sa.Column("status", sa.String(length=32), nullable=False, server_default="active"),
sa.Column("metadata_json", sa.JSON(), nullable=True),
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False),
sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False),
sa.ForeignKeyConstraint(["merchant_id"], ["merchants.id"]),
sa.PrimaryKeyConstraint("id"),
)
op.create_index("ix_bank_accounts_merchant_id", "bank_accounts", ["merchant_id"], unique=False)


def downgrade() -> None:
op.drop_index("ix_bank_accounts_merchant_id", table_name="bank_accounts")
op.drop_table("bank_accounts")
43 changes: 43 additions & 0 deletions alembic/versions/0010_data_sources.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
"""Add data_sources table

Revision ID: 0010_data_sources
Revises: 0009_bank_accounts
Create Date: 2026-06-03 00:00:00.000000
"""

from alembic import op
import sqlalchemy as sa

revision = "0010_data_sources"
down_revision = "0009_bank_accounts"
branch_labels = None
depends_on = None


def upgrade() -> None:
op.create_table(
"data_sources",
sa.Column("id", sa.CHAR(length=36), nullable=False),
sa.Column("merchant_id", sa.CHAR(length=36), nullable=False),
sa.Column("source_type", sa.String(length=64), nullable=False),
sa.Column("provider_name", sa.String(length=128), nullable=True),
sa.Column("display_name", sa.String(length=255), nullable=False),
sa.Column("status", sa.String(length=32), nullable=False, server_default="pending_setup"),
sa.Column("last_sync_at", sa.DateTime(timezone=True), nullable=True),
sa.Column("last_success_at", sa.DateTime(timezone=True), nullable=True),
sa.Column("last_error_at", sa.DateTime(timezone=True), nullable=True),
sa.Column("last_error_message", sa.String(length=1024), nullable=True),
sa.Column("configuration_json", sa.JSON(), nullable=True),
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False),
sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False),
sa.ForeignKeyConstraint(["merchant_id"], ["merchants.id"]),
sa.PrimaryKeyConstraint("id"),
)
op.create_index("ix_data_sources_merchant_id", "data_sources", ["merchant_id"], unique=False)
op.create_index("ix_data_sources_merchant_type", "data_sources", ["merchant_id", "source_type"], unique=False)


def downgrade() -> None:
op.drop_index("ix_data_sources_merchant_type", table_name="data_sources")
op.drop_index("ix_data_sources_merchant_id", table_name="data_sources")
op.drop_table("data_sources")
81 changes: 81 additions & 0 deletions alembic/versions/0011_bank_statements.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
"""Add bank statement tables

Revision ID: 0011_bank_statements
Revises: 0010_data_sources
Create Date: 2026-06-03 00:00:00.000000
"""

from alembic import op
import sqlalchemy as sa

revision = "0011_bank_statements"
down_revision = "0010_data_sources"
branch_labels = None
depends_on = None


def upgrade() -> None:
op.create_table(
"bank_statement_imports",
sa.Column("id", sa.CHAR(length=36), nullable=False),
sa.Column("merchant_id", sa.CHAR(length=36), nullable=False),
sa.Column("bank_account_id", sa.CHAR(length=36), nullable=True),
sa.Column("file_name", sa.String(length=512), nullable=False),
sa.Column("file_type", sa.String(length=16), nullable=False),
sa.Column("status", sa.String(length=32), nullable=False, server_default="uploaded"),
sa.Column("total_rows", sa.Integer(), nullable=False, server_default="0"),
sa.Column("processed_rows", sa.Integer(), nullable=False, server_default="0"),
sa.Column("failed_rows", sa.Integer(), nullable=False, server_default="0"),
sa.Column("error_summary", sa.JSON(), nullable=True),
sa.Column("completed_at", sa.DateTime(timezone=True), nullable=True),
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False),
sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False),
sa.ForeignKeyConstraint(["merchant_id"], ["merchants.id"]),
sa.ForeignKeyConstraint(["bank_account_id"], ["bank_accounts.id"]),
sa.PrimaryKeyConstraint("id"),
)
op.create_index("ix_bank_statement_imports_merchant_id", "bank_statement_imports", ["merchant_id"], unique=False)
op.create_index("ix_bank_statement_imports_bank_account", "bank_statement_imports", ["bank_account_id"], unique=False)

op.create_table(
"bank_statement_entries",
sa.Column("id", sa.CHAR(length=36), nullable=False),
sa.Column("merchant_id", sa.CHAR(length=36), nullable=False),
sa.Column("import_id", sa.CHAR(length=36), nullable=False),
sa.Column("bank_account_id", sa.CHAR(length=36), nullable=True),
sa.Column("entry_date", sa.DateTime(timezone=True), nullable=False),
sa.Column("value_date", sa.DateTime(timezone=True), nullable=True),
sa.Column("description", sa.String(length=1024), nullable=False),
sa.Column("reference", sa.String(length=255), nullable=True),
sa.Column("debit_amount_minor", sa.Integer(), nullable=False, server_default="0"),
sa.Column("credit_amount_minor", sa.Integer(), nullable=False, server_default="0"),
sa.Column("currency", sa.String(length=16), nullable=False),
sa.Column("balance_after_minor", sa.Integer(), nullable=True),
sa.Column("counterparty_name", sa.String(length=255), nullable=True),
sa.Column("raw_row_json", sa.JSON(), nullable=True),
sa.Column("normalized_hash", sa.String(length=128), nullable=False),
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False),
sa.ForeignKeyConstraint(["merchant_id"], ["merchants.id"]),
sa.ForeignKeyConstraint(["import_id"], ["bank_statement_imports.id"]),
sa.ForeignKeyConstraint(["bank_account_id"], ["bank_accounts.id"]),
sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint("normalized_hash", name="uq_bank_statement_entries_hash"),
)
op.create_index("ix_bank_statement_entries_merchant_id", "bank_statement_entries", ["merchant_id"], unique=False)
op.create_index("ix_bank_statement_entries_import_id", "bank_statement_entries", ["import_id"], unique=False)
op.create_index("ix_bank_statement_entries_entry_date", "bank_statement_entries", ["merchant_id", "entry_date"], unique=False)
op.create_index("ix_bank_statement_entries_reference", "bank_statement_entries", ["reference"], unique=False)
op.create_index("ix_bank_statement_entries_hash", "bank_statement_entries", ["normalized_hash"], unique=True)


def downgrade() -> None:
op.drop_index("ix_bank_statement_entries_hash", table_name="bank_statement_entries")
op.drop_index("ix_bank_statement_entries_reference", table_name="bank_statement_entries")
op.drop_index("ix_bank_statement_entries_entry_date", table_name="bank_statement_entries")
op.drop_index("ix_bank_statement_entries_import_id", table_name="bank_statement_entries")
op.drop_index("ix_bank_statement_entries_merchant_id", table_name="bank_statement_entries")
op.drop_table("bank_statement_entries")

op.drop_index("ix_bank_statement_imports_bank_account", table_name="bank_statement_imports")
op.drop_index("ix_bank_statement_imports_merchant_id", table_name="bank_statement_imports")
op.drop_table("bank_statement_imports")
52 changes: 52 additions & 0 deletions alembic/versions/0012_provider_sync_jobs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
"""Add provider_sync_jobs table

Revision ID: 0012_provider_sync_jobs
Revises: 0011_bank_statements
Create Date: 2026-06-03 00:00:00.000000
"""

from alembic import op
import sqlalchemy as sa

revision = "0012_provider_sync_jobs"
down_revision = "0011_bank_statements"
branch_labels = None
depends_on = None


def upgrade() -> None:
op.create_table(
"provider_sync_jobs",
sa.Column("id", sa.CHAR(length=36), nullable=False),
sa.Column("merchant_id", sa.CHAR(length=36), nullable=False),
sa.Column("provider_account_id", sa.CHAR(length=36), nullable=False),
sa.Column("sync_type", sa.String(length=32), nullable=False),
sa.Column("status", sa.String(length=32), nullable=False, server_default="queued"),
sa.Column("date_from", sa.DateTime(timezone=True), nullable=True),
sa.Column("date_to", sa.DateTime(timezone=True), nullable=True),
sa.Column("started_at", sa.DateTime(timezone=True), nullable=True),
sa.Column("completed_at", sa.DateTime(timezone=True), nullable=True),
sa.Column("records_seen", sa.Integer(), nullable=False, server_default="0"),
sa.Column("records_created", sa.Integer(), nullable=False, server_default="0"),
sa.Column("records_updated", sa.Integer(), nullable=False, server_default="0"),
sa.Column("error_message", sa.String(length=1024), nullable=True),
sa.Column("correlation_id", sa.String(length=255), nullable=False),
sa.Column("raw_response_json", sa.JSON(), nullable=True),
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False),
sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False),
sa.ForeignKeyConstraint(["merchant_id"], ["merchants.id"]),
sa.ForeignKeyConstraint(["provider_account_id"], ["provider_accounts.id"]),
sa.PrimaryKeyConstraint("id"),
)
op.create_index("ix_provider_sync_jobs_merchant", "provider_sync_jobs", ["merchant_id"], unique=False)
op.create_index("ix_provider_sync_jobs_provider_account", "provider_sync_jobs", ["provider_account_id"], unique=False)
op.create_index("ix_provider_sync_jobs_correlation", "provider_sync_jobs", ["correlation_id"], unique=False)
op.create_index("ix_provider_sync_jobs_status", "provider_sync_jobs", ["merchant_id", "status"], unique=False)


def downgrade() -> None:
op.drop_index("ix_provider_sync_jobs_status", table_name="provider_sync_jobs")
op.drop_index("ix_provider_sync_jobs_correlation", table_name="provider_sync_jobs")
op.drop_index("ix_provider_sync_jobs_provider_account", table_name="provider_sync_jobs")
op.drop_index("ix_provider_sync_jobs_merchant", table_name="provider_sync_jobs")
op.drop_table("provider_sync_jobs")
65 changes: 65 additions & 0 deletions alembic/versions/0013_incidents.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
"""Add incidents and incident_events tables

Revision ID: 0013_incidents
Revises: 0012_provider_sync_jobs
Create Date: 2026-06-03 00:00:00.000000
"""

from alembic import op
import sqlalchemy as sa

revision = "0013_incidents"
down_revision = "0012_provider_sync_jobs"
branch_labels = None
depends_on = None


def upgrade() -> None:
op.create_table(
"incidents",
sa.Column("id", sa.CHAR(length=36), nullable=False),
sa.Column("merchant_id", sa.CHAR(length=36), nullable=False),
sa.Column("title", sa.String(length=512), nullable=False),
sa.Column("incident_type", sa.String(length=64), nullable=False),
sa.Column("severity", sa.String(length=32), nullable=False),
sa.Column("status", sa.String(length=32), nullable=False, server_default="open"),
sa.Column("provider_name", sa.String(length=128), nullable=True),
sa.Column("affected_amount_minor", sa.Integer(), nullable=False, server_default="0"),
sa.Column("affected_transaction_count", sa.Integer(), nullable=False, server_default="0"),
sa.Column("started_at", sa.DateTime(timezone=True), nullable=False),
sa.Column("ended_at", sa.DateTime(timezone=True), nullable=True),
sa.Column("summary", sa.String(length=2048), nullable=False),
sa.Column("ai_summary", sa.String(length=4096), nullable=True),
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False),
sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False),
sa.ForeignKeyConstraint(["merchant_id"], ["merchants.id"]),
sa.PrimaryKeyConstraint("id"),
)
op.create_index("ix_incidents_merchant_id", "incidents", ["merchant_id"], unique=False)
op.create_index("ix_incidents_merchant_status", "incidents", ["merchant_id", "status"], unique=False)
op.create_index("ix_incidents_merchant_severity", "incidents", ["merchant_id", "severity"], unique=False)

op.create_table(
"incident_events",
sa.Column("id", sa.CHAR(length=36), nullable=False),
sa.Column("incident_id", sa.CHAR(length=36), nullable=False),
sa.Column("event_type", sa.String(length=128), nullable=False),
sa.Column("actor_user_id", sa.CHAR(length=36), nullable=True),
sa.Column("message", sa.String(length=2048), nullable=False),
sa.Column("metadata_json", sa.JSON(), nullable=True),
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False),
sa.ForeignKeyConstraint(["incident_id"], ["incidents.id"]),
sa.ForeignKeyConstraint(["actor_user_id"], ["users.id"]),
sa.PrimaryKeyConstraint("id"),
)
op.create_index("ix_incident_events_incident_id", "incident_events", ["incident_id"], unique=False)


def downgrade() -> None:
op.drop_index("ix_incident_events_incident_id", table_name="incident_events")
op.drop_table("incident_events")

op.drop_index("ix_incidents_merchant_severity", table_name="incidents")
op.drop_index("ix_incidents_merchant_status", table_name="incidents")
op.drop_index("ix_incidents_merchant_id", table_name="incidents")
op.drop_table("incidents")
Empty file added bomipay-website/Prompt.md
Empty file.
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ name = "bomipay"
version = "0.1.0"
description = "Bomi Pay backend service for financial transaction intelligence and reconciliation."
readme = "README.md"
requires-python = ">=3.12"
requires-python = ">=3.11"
dependencies = [
"fastapi>=0.111.0",
"uvicorn[standard]>=0.23.0",
Expand All @@ -12,7 +12,7 @@ dependencies = [
"python-dotenv>=1.0.0",
"pydantic>=2.0.0",
"pydantic-settings>=2.0.0",
"passlib[bcrypt]>=1.8.0",
"passlib[bcrypt]>=1.7.0",
"python-jose[cryptography]>=3.4.0",
"python-json-logger>=2.0.7",
"cryptography>=41.0.0",
Expand Down
30 changes: 29 additions & 1 deletion src/bomipay/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,26 @@
from .routes.merchant import router as merchant_router
from .routes.transactions import router as transactions_router
from .routes.webhooks import router as webhooks_router
from .routes.providers import router as providers_router
from .routes.reconciliation import router as reconciliation_router
from .routes.notifications import router as notifications_router
from .routes.bank_accounts import router as bank_accounts_router
from .routes.data_sources import router as data_sources_router
from .routes.bank_statements import router as bank_statements_router
from .routes.provider_sync import router as provider_sync_router
from .routes.incidents import router as incidents_router
from .routes.analytics import router as analytics_router
from .routes.dashboard import router as dashboard_router
from .routes.timeline import router as timeline_router
from .routes.action_center import router as action_center_router
from .routes.payment_graph import router as payment_graph_router
from .routes.ai_assistant import router as ai_assistant_router

configure_logging()
logger = logging.getLogger("bomipay")

@asynccontextmanager
def lifespan(app: FastAPI):
async def lifespan(app: FastAPI):
logger.info("service.startup", extra={"environment": settings.environment})
yield
logger.info("service.shutdown")
Expand All @@ -45,6 +59,20 @@ def lifespan(app: FastAPI):
app.include_router(merchant_router, prefix="/api/v1")
app.include_router(transactions_router, prefix="/api/v1")
app.include_router(alerts_router, prefix="/api/v1")
app.include_router(providers_router, prefix="/api/v1")
app.include_router(reconciliation_router, prefix="/api/v1")
app.include_router(notifications_router, prefix="/api/v1")
app.include_router(bank_accounts_router, prefix="/api/v1")
app.include_router(data_sources_router, prefix="/api/v1")
app.include_router(bank_statements_router, prefix="/api/v1")
app.include_router(provider_sync_router, prefix="/api/v1")
app.include_router(incidents_router, prefix="/api/v1")
app.include_router(analytics_router, prefix="/api/v1")
app.include_router(dashboard_router, prefix="/api/v1")
app.include_router(timeline_router, prefix="/api/v1")
app.include_router(action_center_router, prefix="/api/v1")
app.include_router(payment_graph_router, prefix="/api/v1")
app.include_router(ai_assistant_router, prefix="/api/v1")
app.include_router(webhooks_router)


Expand Down
Loading