Security (Phase 1): - Add authentication middleware on /graphql endpoint - Filter company queries by user access (prevent IDOR) - Add role-based authorization on mutations (owner/accountant) - Reduce API key cache TTL from 24h to 5 minutes - Hide exception details in production GraphQL errors - Fix RBAC in frontend companyStore (was hardcoded) Wiring broken features (Phase 2): - Wire Kassekladde submit/void/copy to GraphQL mutations - Wire Kontooversigt account creation to createAccount mutation - Wire Settings save to updateCompany mutation - Wire CreateFiscalYearModal and CloseFiscalYearWizard to mutations - Replace Momsindberetning mock data with real useVatReport query - Remove Dashboard hardcoded percentages and fake VAT deadline - Fix Kreditnotaer invoice selector to use real data - Fix mutation retry from 1 to 0 (prevent duplicate operations) Accounting compliance (Phase 3): - Add balanced entry validation (debit==credit) in JournalEntryDraftAggregate - Add fiscal year boundary enforcement (status, date range checks) - Add PostedAt timestamp to posted events (Bogføringsloven §7) - Add account number uniqueness check within company - Add fiscal year overlap and gap checks - Add sequential invoice auto-numbering - Fix InvoiceLine VAT rate to use canonical VatCodes - Fix SAF-T account type mapping (financial → Expense) - Add DraftLine validation (cannot have both debit and credit > 0) UX improvements (Phase 4): - Fix Danish character encoding across 15+ files (ø, æ, å) - Deploy DemoDataDisclaimer on pages with mock/incomplete data - Adopt PageHeader component universally across all pages - Standardize active/inactive filtering to Switch pattern - Fix dead buttons in Header (Help, Notifications) - Remove hardcoded mock data from Settings - Fix Sidebar controlled state and Kontooversigt navigation Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
84 lines
2.7 KiB
C#
84 lines
2.7 KiB
C#
using Books.Api.EventFlow.ReadModels;
|
|
using Dapper;
|
|
using Npgsql;
|
|
|
|
namespace Books.Api.EventFlow.Repositories;
|
|
|
|
public class JournalEntryDraftRepository(NpgsqlDataSource dataSource) : IJournalEntryDraftRepository
|
|
{
|
|
private const string SelectColumns = """
|
|
aggregate_id AS Id,
|
|
company_id AS CompanyId,
|
|
name AS Name,
|
|
voucher_number AS VoucherNumber,
|
|
document_date AS DocumentDate,
|
|
description AS Description,
|
|
fiscal_year_id AS FiscalYearId,
|
|
lines AS Lines,
|
|
attachment_ids AS AttachmentIds,
|
|
status AS Status,
|
|
transaction_id AS TransactionId,
|
|
posted_at AS PostedAt,
|
|
created_by AS CreatedBy,
|
|
create_time AS CreatedAt,
|
|
updated_time AS UpdatedAt,
|
|
extraction_data AS ExtractionData
|
|
""";
|
|
|
|
public async Task<JournalEntryDraftReadModelDto?> GetByIdAsync(
|
|
string id,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
await using var connection = await dataSource.OpenConnectionAsync(cancellationToken);
|
|
|
|
var sql = $"""
|
|
SELECT {SelectColumns}
|
|
FROM journal_entry_draft_read_models
|
|
WHERE aggregate_id = @Id
|
|
""";
|
|
|
|
return await connection.QuerySingleOrDefaultAsync<JournalEntryDraftReadModelDto>(
|
|
sql,
|
|
new { Id = id });
|
|
}
|
|
|
|
public async Task<IReadOnlyList<JournalEntryDraftReadModelDto>> GetActiveByCompanyIdAsync(
|
|
string companyId,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
await using var connection = await dataSource.OpenConnectionAsync(cancellationToken);
|
|
|
|
var sql = $"""
|
|
SELECT {SelectColumns}
|
|
FROM journal_entry_draft_read_models
|
|
WHERE company_id = @CompanyId AND status = 'active'
|
|
ORDER BY updated_time DESC
|
|
""";
|
|
|
|
var result = await connection.QueryAsync<JournalEntryDraftReadModelDto>(
|
|
sql,
|
|
new { CompanyId = companyId });
|
|
|
|
return result.ToList();
|
|
}
|
|
|
|
public async Task<IReadOnlyList<JournalEntryDraftReadModelDto>> GetByCompanyIdAsync(
|
|
string companyId,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
await using var connection = await dataSource.OpenConnectionAsync(cancellationToken);
|
|
|
|
var sql = $"""
|
|
SELECT {SelectColumns}
|
|
FROM journal_entry_draft_read_models
|
|
WHERE company_id = @CompanyId
|
|
ORDER BY updated_time DESC
|
|
""";
|
|
|
|
var result = await connection.QueryAsync<JournalEntryDraftReadModelDto>(
|
|
sql,
|
|
new { CompanyId = companyId });
|
|
|
|
return result.ToList();
|
|
}
|
|
}
|