-
-
Notifications
You must be signed in to change notification settings - Fork 0
Database.md
This document covers Entity Framework Core configuration, migrations, seeding, and database schema.
OpenCashFlow uses PostgreSQL 16 with Entity Framework Core 9.0 as the ORM.
Database provider: Npgsql.EntityFrameworkCore.PostgreSQL
Host=localhost;Database=opencashflow_db;Username=opencashflow;Password=your_password;Port=5432
Production additions:
SSL Mode=Require;Trust Server Certificate=true;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=100
Location: src/OpenCashFlow.Shared/Data/ApplicationDbContext.cs
public class ApplicationDbContext : DbContext
{
// Identity
public DbSet<AspNetUser> AspNetUser_DS { get; set; }
public DbSet<AspNetRole> AspNetRole_DS { get; set; }
public DbSet<AspNetUserRole> AspNetUserRole_DS { get; set; }
public DbSet<AspNetUserClaim> AspNetUserClaim_DS { get; set; }
public DbSet<AspNetUserLogin> AspNetUserLogin_DS { get; set; }
public DbSet<AspNetUserToken> AspNetUserToken_DS { get; set; }
// Companies
public DbSet<Company> Company_DS { get; set; }
public DbSet<Company_Address> Company_Address_DS { get; set; }
public DbSet<Company_Invoice> Company_Invoice_DS { get; set; }
public DbSet<Company_Staff> Company_Staff_DS { get; set; }
// Payments
public DbSet<Payment> Payment_DS { get; set; }
public DbSet<Payment_Method_LookUps> Payment_Method_LookUps_DS { get; set; }
public DbSet<Payment_DocumentType_LookUp> Payment_DocumentType_LookUp_DS { get; set; }
public DbSet<Payment_DailyPayments> Payment_DailyPayments_DS { get; set; }
// Billing
public DbSet<Plan> Plan_DS { get; set; }
public DbSet<Company_Subscription> Company_Subscription_DS { get; set; }
public DbSet<Company_Renewal> Company_Renewal_DS { get; set; }
public DbSet<Stripe_Webhook_Event> Stripe_Webhook_Event_DS { get; set; }
// Cash Management
public DbSet<CashBalance> CashBalance_DS { get; set; }
public DbSet<CashLedger> CashLedger_DS { get; set; }
// Admin
public DbSet<Admin_AuditLog> Admin_AuditLog_DS { get; set; }
}public class AspNetUser
{
public Guid UserID { get; set; }
public string UserName { get; set; }
public string Email { get; set; }
public string? PhoneNumber { get; set; }
public string? PhoneNumberPrefix { get; set; }
// Personal information
public string? FirstName { get; set; }
public string? LastName { get; set; }
public string? Gender { get; set; }
public DateTime? DoB { get; set; }
public string? Nationality { get; set; }
// Authentication
public string PasswordHash { get; set; }
public string PasswordSalt { get; set; }
public string? SecurityStamp { get; set; }
// MFA
public bool TwoFactorEnabled { get; set; }
public bool LockoutEnabled { get; set; }
public DateTime? LockoutEnd { get; set; }
public int AccessFailedCount { get; set; }
// Preferences
public string? UserAvatar { get; set; }
public string? Language { get; set; }
public string? Country { get; set; }
public string? Timezone { get; set; }
// Permissions
public string? AssignedPermissions { get; set; }
public string? DeniedPermissions { get; set; }
// Audit
public DateTime DateIns { get; set; }
public DateTime? DateEdit { get; set; }
public bool IsDeleted { get; set; }
}public class Company
{
public Guid TenantID { get; set; }
public string CompanyName { get; set; }
public int MaxUsers { get; set; }
public string? Avatar { get; set; }
public string? BusinessCategory { get; set; }
// Stripe integration
public string? StripeCustomerID { get; set; }
public string? StripeDefaultPaymentMethodID { get; set; }
public string? BillingEmail { get; set; }
// Billing information
public string? VAT { get; set; }
public string? IBAN { get; set; }
public string? BIC { get; set; }
public DateTime? StartingContract { get; set; }
public DateTime? EndingContract { get; set; }
// Audit fields
public string CreatedBy { get; set; }
public DateTime DateIns { get; set; }
public string? EditedBy { get; set; }
public DateTime? DateEdit { get; set; }
public bool IsDeleted { get; set; }
public string? IsDeletedBy { get; set; }
public string? IsDeletedWhy { get; set; }
public DateTime? DateDeleted { get; set; }
}public class Payment
{
public Guid PaymentID { get; set; }
public Guid TenantID { get; set; }
public Guid? RequestId { get; set; } // Idempotency key
public decimal Amount { get; set; }
public string EntryType { get; set; } // "Income" or "Outcome"
public string? Description { get; set; }
public DateTime PaymentDate { get; set; }
// Foreign keys
public int PaymentMethodID { get; set; }
public int DocumentTypeID { get; set; }
public Guid UserID { get; set; }
// Navigation properties
public Payment_Method_LookUps PaymentMethod { get; set; }
public Payment_DocumentType_LookUp DocumentType { get; set; }
// Audit fields
public string CreatedBy { get; set; }
public DateTime DateIns { get; set; }
public string? EditedBy { get; set; }
public DateTime? DateEdit { get; set; }
public bool IsDeleted { get; set; }
}public class Payment_Method_LookUps
{
public int PaymentMethodID { get; set; }
public string PaymentMethodName { get; set; }
public string? Description { get; set; }
public bool IsActive { get; set; }
}public class Payment_DocumentType_LookUp
{
public int DocumentTypeID { get; set; }
public string DocumentTypeName { get; set; }
public string? Description { get; set; }
public bool IsActive { get; set; }
}public class CashBalance
{
public Guid CompanyId { get; set; }
public decimal Balance { get; set; }
public DateTime LastUpdated { get; set; }
// Optimistic concurrency
public uint xmin { get; set; }
}public class CashLedger
{
public Guid Id { get; set; }
public Guid CompanyId { get; set; }
public decimal Delta { get; set; }
public string RefType { get; set; }
public Guid RefId { get; set; }
public DateTime CreatedAt { get; set; }
}Migrations are stored in: src/OpenCashFlow.Shared/Data/Migrations/
Using the helper script (recommended):
./scripts/create-migration.sh AddPaymentIndexUsing dotnet CLI directly:
dotnet ef migrations add AddPaymentIndex \
--project src/OpenCashFlow.Shared/OpenCashFlow.Shared.csproj \
--startup-project src/OpenCashFlow.API/OpenCashFlow.API.csproj \
--context ApplicationDbContext \
--output-dir Data/MigrationsUsing the helper script:
./scripts/create-migration.sh AddPaymentIndex --applyUsing dotnet CLI:
dotnet ef database update \
--project src/OpenCashFlow.Shared/OpenCashFlow.Shared.csproj \
--startup-project src/OpenCashFlow.API/OpenCashFlow.API.csproj \
--context ApplicationDbContext./scripts/create-migration.sh --removeOr:
dotnet ef migrations remove \
--project src/OpenCashFlow.Shared/OpenCashFlow.Shared.csproj \
--startup-project src/OpenCashFlow.API/OpenCashFlow.API.csproj \
--context ApplicationDbContextUse descriptive names that indicate the change:
-
InitialCreate- Initial schema -
AddPaymentIndex- Adding an index -
AddCompanyAvatar- Adding a column -
RenameUserEmailToContactEmail- Renaming a column -
CreateCashLedgerTable- Creating a new table
The InitialCreate migration includes seed data for:
migrationBuilder.InsertData(
table: "AspNetRoles",
columns: new[] { "RoleID", "RoleName", "NormalizedName" },
values: new object[,]
{
{ Guid.Parse("00000000-0000-0000-0000-000000000001"), "Administrator", "ADMINISTRATOR" },
{ Guid.Parse("00000000-0000-0000-0000-000000000002"), "Employee", "EMPLOYEE" },
{ Guid.Parse("00000000-9999-9999-9999-000000000009"), "GIManagers", "GIMANAGERS" }
});migrationBuilder.InsertData(
table: "Payment_Method_LookUps",
columns: new[] { "PaymentMethodID", "PaymentMethodName", "IsActive" },
values: new object[,]
{
{ 1, "Cash", true },
{ 2, "Bank Transfer", true },
{ 3, "Credit Card", true },
{ 4, "PayPal", true }
});migrationBuilder.InsertData(
table: "Payment_DocumentType_LookUp",
columns: new[] { "DocumentTypeID", "DocumentTypeName", "IsActive" },
values: new object[,]
{
{ 1, "Invoice", true },
{ 2, "Receipt", true },
{ 3, "Credit Note", true },
{ 4, "Other", true }
});For development, a demo user and company are seeded:
// Demo Company
migrationBuilder.InsertData(
table: "Companies",
columns: new[] { "TenantID", "CompanyName", "MaxUsers", "DateIns", "CreatedBy" },
values: new object[]
{
Guid.Parse("11111111-1111-1111-1111-111111111111"),
"Demo Company",
10,
DateTime.UtcNow,
"SYSTEM"
});
// Demo User (password: DemoPassword123!)
migrationBuilder.InsertData(
table: "AspNetUsers",
columns: new[] { "UserID", "Email", "UserName", "PasswordHash", "PasswordSalt", ... },
values: new object[] { ... });// Payment queries by tenant and date
modelBuilder.Entity<Payment>()
.HasIndex(p => new { p.TenantID, p.PaymentDate })
.HasDatabaseName("IX_Payment_TenantID_PaymentDate");
// Soft delete filtering
modelBuilder.Entity<Payment>()
.HasIndex(p => p.IsDeleted)
.HasDatabaseName("IX_Payment_IsDeleted");
// User lookup by email
modelBuilder.Entity<AspNetUser>()
.HasIndex(u => u.Email)
.IsUnique()
.HasDatabaseName("IX_AspNetUser_Email");// Stripe customer ID must be unique
modelBuilder.Entity<Company>()
.HasIndex(c => c.StripeCustomerID)
.IsUnique()
.HasFilter("\"StripeCustomerID\" IS NOT NULL")
.HasDatabaseName("IX_Company_StripeCustomerID");
// Cash ledger entry uniqueness
modelBuilder.Entity<CashLedger>()
.HasIndex(l => new { l.CompanyId, l.RefType, l.RefId })
.IsUnique()
.HasDatabaseName("IX_CashLedger_Company_Ref");The CashBalance table uses PostgreSQL's xmin system column for optimistic concurrency:
modelBuilder.Entity<CashBalance>()
.Property(e => e.xmin)
.IsRowVersion();Usage:
var balance = await context.CashBalance_DS.FindAsync(companyId);
balance.Balance += amount;
try
{
await context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
// Handle concurrent update conflict
}public async Task<IEnumerable<Payment>> GetPaymentsAsync(Guid tenantId)
{
return await _context.Payment_DS
.Where(p => p.TenantID == tenantId && !p.IsDeleted)
.Include(p => p.PaymentMethod)
.OrderByDescending(p => p.PaymentDate)
.ToListAsync();
}Consider adding global query filters for soft deletes:
modelBuilder.Entity<Payment>()
.HasQueryFilter(p => !p.IsDeleted);
modelBuilder.Entity<Company>()
.HasQueryFilter(c => !c.IsDeleted);public async Task<PagedResult<Payment>> GetPaymentsPagedAsync(
Guid tenantId, int page, int pageSize)
{
var query = _context.Payment_DS
.Where(p => p.TenantID == tenantId && !p.IsDeleted);
var total = await query.CountAsync();
var items = await query
.OrderByDescending(p => p.PaymentDate)
.Skip((page - 1) * pageSize)
.Take(pageSize)
.ToListAsync();
return new PagedResult<Payment>
{
Items = items,
TotalCount = total,
Page = page,
PageSize = pageSize
};
}ββββββββββββββββββββ ββββββββββββββββββββ
β AspNetUsers β β AspNetRoles β
ββββββββββββββββββββ€ ββββββββββββββββββββ€
β UserID (PK) β β RoleID (PK) β
β Email β β RoleName β
β PasswordHash β ββββββββββ¬ββββββββββ
β ... β β
ββββββββββ¬ββββββββββ β
β β
β βββββββββββββββββββββββ
β β
βΌ βΌ
ββββββββββββββββββββ
β AspNetUserRoles β
ββββββββββββββββββββ€
β UserID (FK) β
β RoleID (FK) β
ββββββββββββββββββββ
ββββββββββββββββββββ ββββββββββββββββββββ
β Companies β β Company_Staff β
ββββββββββββββββββββ€ ββββββββββββββββββββ€
β TenantID (PK) βββββββββ TenantID (FK) β
β CompanyName β β UserID (FK) β
β StripeCustomerID β ββββββββββββββββββββ
β ... β
ββββββββββ¬ββββββββββ
β
β TenantID
βΌ
ββββββββββββββββββββ
β Payments β
ββββββββββββββββββββ€
β PaymentID (PK) β
β TenantID (FK) βββββββββ
β Amount β β
β EntryType β β
β PaymentMethodID βββββββββΌβββΊ Payment_Method_LookUps
β DocumentTypeID βββββββββΌβββΊ Payment_DocumentType_LookUp
β UserID (FK) βββββββββ
ββββββββββββββββββββ
See Docs/Backup.md and Docs/database-recovery-and-connection-guide.md for backup procedures and recovery guidance.
Project status
OpenCashFlow is under active development.
APIs, database schema, and UI may change until the first stable release.
Built with
.NET Β· ASP.NET Core Β· Entity Framework Core Β· PostgreSQL Β· Tabler
Β© 2026 OpenCashFlow
- Developer Preview
- Not production-ready
- First-run setup included