feat: implement CRM and tax accounting specialized services and repositories
TaxBaik CI/CD / build-and-deploy (push) Successful in 49s
TaxBaik CI/CD / build-and-deploy (push) Successful in 49s
Phase 2: Repository Implementation (Dapper) - TaxProfileRepository: tax profile CRUD + risk level analysis + filing due dates - TaxFilingScheduleRepository: schedule tracking + upcoming due dates + completion marking - ConsultingActivityRepository: CRM activity history + pending followups + consultant tracking - ContractRepository: contract lifecycle + active contracts + expiring alerts + MRR calculation - RevenueTrackingRepository: invoice tracking + payment status + revenue analysis Phase 3: Service Layer (Business Logic) - TaxProfileService: profile creation, risk assessment, upcoming filing detection - TaxFilingScheduleService: schedule management, deadline tracking, completion workflow - ConsultingActivityService: activity logging, followup management, consultant productivity - ContractService: contract management, MRR calculation, expiring contract alerts - RevenueTrackingService: invoice creation, payment tracking, revenue analytics Phase 4: API Controller (REST Endpoints) - TaxProfileController: CRUD operations + high-risk filtering + upcoming filings query Architecture Highlights: - SOLID principles: each layer has clear responsibility - Dapper-based repositories for data access - Comprehensive service layer for business logic - RESTful API design with proper error handling - Ready for Blazor UI implementation and deployment Database Migration V015 executed: - 5 new specialized tables for CRM and tax accounting - Appropriate indexes for query performance - Foreign key constraints for data integrity
This commit is contained in:
@@ -0,0 +1,57 @@
|
||||
namespace TaxBaik.Infrastructure.Repositories;
|
||||
|
||||
using Dapper;
|
||||
using TaxBaik.Domain.Entities;
|
||||
using TaxBaik.Domain.Interfaces;
|
||||
|
||||
public class ConsultingActivityRepository(IDbConnectionFactory connectionFactory) : BaseRepository(connectionFactory), IConsultingActivityRepository
|
||||
{
|
||||
public async Task<int> CreateAsync(ConsultingActivity activity, CancellationToken cancellationToken = default)
|
||||
{
|
||||
using var conn = Conn();
|
||||
return await conn.QueryFirstAsync<int>(
|
||||
@"INSERT INTO consulting_activities (client_id, activity_type, activity_date, activity_time, assigned_consultant, description, outcome, next_followup_date, notes, created_at, updated_at)
|
||||
VALUES (@ClientId, @ActivityType, @ActivityDate, @ActivityTime, @AssignedConsultantId, @Description, @Outcome, @NextFollowupDate, @Notes, NOW(), NOW())
|
||||
RETURNING id",
|
||||
activity);
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<ConsultingActivity>> GetByClientIdAsync(int clientId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
using var conn = Conn();
|
||||
return await conn.QueryAsync<ConsultingActivity>(
|
||||
@"SELECT id, client_id, activity_type, activity_date, activity_time, assigned_consultant, description, outcome, next_followup_date, notes, created_at, updated_at
|
||||
FROM consulting_activities WHERE client_id = @ClientId ORDER BY activity_date DESC",
|
||||
new { ClientId = clientId });
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<ConsultingActivity>> GetPendingFollowupsAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
using var conn = Conn();
|
||||
return await conn.QueryAsync<ConsultingActivity>(
|
||||
@"SELECT id, client_id, activity_type, activity_date, activity_time, assigned_consultant, description, outcome, next_followup_date, notes, created_at, updated_at
|
||||
FROM consulting_activities WHERE next_followup_date IS NOT NULL AND next_followup_date <= CURRENT_DATE
|
||||
ORDER BY next_followup_date ASC");
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<ConsultingActivity>> GetByConsultantAsync(int consultantId, DateTime fromDate, CancellationToken cancellationToken = default)
|
||||
{
|
||||
using var conn = Conn();
|
||||
return await conn.QueryAsync<ConsultingActivity>(
|
||||
@"SELECT id, client_id, activity_type, activity_date, activity_time, assigned_consultant, description, outcome, next_followup_date, notes, created_at, updated_at
|
||||
FROM consulting_activities WHERE assigned_consultant = @ConsultantId AND activity_date >= @FromDate
|
||||
ORDER BY activity_date DESC",
|
||||
new { ConsultantId = consultantId, FromDate = fromDate });
|
||||
}
|
||||
|
||||
public async Task UpdateAsync(ConsultingActivity activity, CancellationToken cancellationToken = default)
|
||||
{
|
||||
using var conn = Conn();
|
||||
await conn.ExecuteAsync(
|
||||
@"UPDATE consulting_activities SET activity_type = @ActivityType, activity_date = @ActivityDate,
|
||||
activity_time = @ActivityTime, assigned_consultant = @AssignedConsultantId, description = @Description,
|
||||
outcome = @Outcome, next_followup_date = @NextFollowupDate, notes = @Notes, updated_at = NOW()
|
||||
WHERE id = @Id",
|
||||
activity);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
namespace TaxBaik.Infrastructure.Repositories;
|
||||
|
||||
using Dapper;
|
||||
using TaxBaik.Domain.Entities;
|
||||
using TaxBaik.Domain.Interfaces;
|
||||
|
||||
public class ContractRepository(IDbConnectionFactory connectionFactory) : BaseRepository(connectionFactory), IContractRepository
|
||||
{
|
||||
public async Task<int> CreateAsync(Contract contract, CancellationToken cancellationToken = default)
|
||||
{
|
||||
using var conn = Conn();
|
||||
return await conn.QueryFirstAsync<int>(
|
||||
@"INSERT INTO contracts (client_id, contract_number, service_type, contract_date, start_date, end_date, monthly_fee, total_amount, payment_status, status, notes, created_at, updated_at)
|
||||
VALUES (@ClientId, @ContractNumber, @ServiceType, @ContractDate, @StartDate, @EndDate, @MonthlyFee, @TotalAmount, @PaymentStatus, @Status, @Notes, NOW(), NOW())
|
||||
RETURNING id",
|
||||
contract);
|
||||
}
|
||||
|
||||
public async Task<Contract?> GetByIdAsync(int id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
using var conn = Conn();
|
||||
return await conn.QueryFirstOrDefaultAsync<Contract>(
|
||||
@"SELECT id, client_id, contract_number, service_type, contract_date, start_date, end_date, monthly_fee, total_amount, payment_status, status, notes, created_at, updated_at
|
||||
FROM contracts WHERE id = @Id",
|
||||
new { Id = id });
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<Contract>> GetByClientIdAsync(int clientId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
using var conn = Conn();
|
||||
return await conn.QueryAsync<Contract>(
|
||||
@"SELECT id, client_id, contract_number, service_type, contract_date, start_date, end_date, monthly_fee, total_amount, payment_status, status, notes, created_at, updated_at
|
||||
FROM contracts WHERE client_id = @ClientId ORDER BY contract_date DESC",
|
||||
new { ClientId = clientId });
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<Contract>> GetActiveContractsAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
using var conn = Conn();
|
||||
return await conn.QueryAsync<Contract>(
|
||||
@"SELECT id, client_id, contract_number, service_type, contract_date, start_date, end_date, monthly_fee, total_amount, payment_status, status, notes, created_at, updated_at
|
||||
FROM contracts WHERE status = 'active' ORDER BY client_id");
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<Contract>> GetExpiringContractsAsync(int daysAhead = 30, CancellationToken cancellationToken = default)
|
||||
{
|
||||
using var conn = Conn();
|
||||
return await conn.QueryAsync<Contract>(
|
||||
@"SELECT id, client_id, contract_number, service_type, contract_date, start_date, end_date, monthly_fee, total_amount, payment_status, status, notes, created_at, updated_at
|
||||
FROM contracts
|
||||
WHERE status = 'active' AND end_date IS NOT NULL AND end_date BETWEEN CURRENT_DATE AND CURRENT_DATE + INTERVAL '1 day' * @DaysAhead
|
||||
ORDER BY end_date ASC",
|
||||
new { DaysAhead = daysAhead });
|
||||
}
|
||||
|
||||
public async Task UpdateAsync(Contract contract, CancellationToken cancellationToken = default)
|
||||
{
|
||||
using var conn = Conn();
|
||||
await conn.ExecuteAsync(
|
||||
@"UPDATE contracts SET contract_number = @ContractNumber, service_type = @ServiceType, contract_date = @ContractDate,
|
||||
start_date = @StartDate, end_date = @EndDate, monthly_fee = @MonthlyFee, total_amount = @TotalAmount,
|
||||
payment_status = @PaymentStatus, status = @Status, notes = @Notes, updated_at = NOW()
|
||||
WHERE id = @Id",
|
||||
contract);
|
||||
}
|
||||
|
||||
public async Task<decimal> GetMonthlyRecurringRevenueAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
using var conn = Conn();
|
||||
var result = await conn.QueryFirstAsync<decimal>(
|
||||
@"SELECT COALESCE(SUM(monthly_fee), 0) FROM contracts WHERE status = 'active' AND monthly_fee IS NOT NULL");
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
namespace TaxBaik.Infrastructure.Repositories;
|
||||
|
||||
using Dapper;
|
||||
using TaxBaik.Domain.Entities;
|
||||
using TaxBaik.Domain.Interfaces;
|
||||
|
||||
public class RevenueTrackingRepository(IDbConnectionFactory connectionFactory) : BaseRepository(connectionFactory), IRevenueTrackingRepository
|
||||
{
|
||||
public async Task<int> CreateAsync(RevenueTracking revenue, CancellationToken cancellationToken = default)
|
||||
{
|
||||
using var conn = Conn();
|
||||
return await conn.QueryFirstAsync<int>(
|
||||
@"INSERT INTO revenue_tracking (client_id, invoice_number, invoice_date, service_type, amount, payment_status, payment_date, due_date, notes, created_at, updated_at)
|
||||
VALUES (@ClientId, @InvoiceNumber, @InvoiceDate, @ServiceType, @Amount, @PaymentStatus, @PaymentDate, @DueDate, @Notes, NOW(), NOW())
|
||||
RETURNING id",
|
||||
revenue);
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<RevenueTracking>> GetByClientIdAsync(int clientId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
using var conn = Conn();
|
||||
return await conn.QueryAsync<RevenueTracking>(
|
||||
@"SELECT id, client_id, invoice_number, invoice_date, service_type, amount, payment_status, payment_date, due_date, notes, created_at, updated_at
|
||||
FROM revenue_tracking WHERE client_id = @ClientId ORDER BY invoice_date DESC",
|
||||
new { ClientId = clientId });
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<RevenueTracking>> GetPendingPaymentsAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
using var conn = Conn();
|
||||
return await conn.QueryAsync<RevenueTracking>(
|
||||
@"SELECT id, client_id, invoice_number, invoice_date, service_type, amount, payment_status, payment_date, due_date, notes, created_at, updated_at
|
||||
FROM revenue_tracking WHERE payment_status = 'pending' ORDER BY due_date ASC");
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<RevenueTracking>> GetByDateRangeAsync(DateTime startDate, DateTime endDate, CancellationToken cancellationToken = default)
|
||||
{
|
||||
using var conn = Conn();
|
||||
return await conn.QueryAsync<RevenueTracking>(
|
||||
@"SELECT id, client_id, invoice_number, invoice_date, service_type, amount, payment_status, payment_date, due_date, notes, created_at, updated_at
|
||||
FROM revenue_tracking WHERE invoice_date BETWEEN @StartDate AND @EndDate ORDER BY invoice_date DESC",
|
||||
new { StartDate = startDate, EndDate = endDate });
|
||||
}
|
||||
|
||||
public async Task UpdateAsync(RevenueTracking revenue, CancellationToken cancellationToken = default)
|
||||
{
|
||||
using var conn = Conn();
|
||||
await conn.ExecuteAsync(
|
||||
@"UPDATE revenue_tracking SET invoice_number = @InvoiceNumber, invoice_date = @InvoiceDate,
|
||||
service_type = @ServiceType, amount = @Amount, payment_status = @PaymentStatus,
|
||||
payment_date = @PaymentDate, due_date = @DueDate, notes = @Notes, updated_at = NOW()
|
||||
WHERE id = @Id",
|
||||
revenue);
|
||||
}
|
||||
|
||||
public async Task MarkPaidAsync(int id, DateTime paymentDate, CancellationToken cancellationToken = default)
|
||||
{
|
||||
using var conn = Conn();
|
||||
await conn.ExecuteAsync(
|
||||
@"UPDATE revenue_tracking SET payment_status = 'paid', payment_date = @PaymentDate, updated_at = NOW() WHERE id = @Id",
|
||||
new { Id = id, PaymentDate = paymentDate });
|
||||
}
|
||||
|
||||
public async Task<decimal> GetTotalRevenueAsync(DateTime startDate, DateTime endDate, CancellationToken cancellationToken = default)
|
||||
{
|
||||
using var conn = Conn();
|
||||
var result = await conn.QueryFirstAsync<decimal>(
|
||||
@"SELECT COALESCE(SUM(amount), 0) FROM revenue_tracking WHERE invoice_date BETWEEN @StartDate AND @EndDate",
|
||||
new { StartDate = startDate, EndDate = endDate });
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
namespace TaxBaik.Infrastructure.Repositories;
|
||||
|
||||
using Dapper;
|
||||
using TaxBaik.Domain.Entities;
|
||||
using TaxBaik.Domain.Interfaces;
|
||||
|
||||
public class TaxFilingScheduleRepository(IDbConnectionFactory connectionFactory) : BaseRepository(connectionFactory), ITaxFilingScheduleRepository
|
||||
{
|
||||
public async Task<int> CreateAsync(TaxFilingSchedule schedule, CancellationToken cancellationToken = default)
|
||||
{
|
||||
using var conn = Conn();
|
||||
return await conn.QueryFirstAsync<int>(
|
||||
@"INSERT INTO tax_filing_schedules (client_id, filing_type, due_date, filing_year, status, assigned_to, notes, created_at, updated_at)
|
||||
VALUES (@ClientId, @FilingType, @DueDate, @FilingYear, @Status, @AssignedToId, @Notes, NOW(), NOW())
|
||||
RETURNING id",
|
||||
schedule);
|
||||
}
|
||||
|
||||
public async Task<TaxFilingSchedule?> GetByIdAsync(int id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
using var conn = Conn();
|
||||
return await conn.QueryFirstOrDefaultAsync<TaxFilingSchedule>(
|
||||
@"SELECT id, client_id, filing_type, due_date, filing_year, status, assigned_to, completed_date, notes, created_at, updated_at
|
||||
FROM tax_filing_schedules WHERE id = @Id",
|
||||
new { Id = id });
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<TaxFilingSchedule>> GetByClientIdAsync(int clientId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
using var conn = Conn();
|
||||
return await conn.QueryAsync<TaxFilingSchedule>(
|
||||
@"SELECT id, client_id, filing_type, due_date, filing_year, status, assigned_to, completed_date, notes, created_at, updated_at
|
||||
FROM tax_filing_schedules WHERE client_id = @ClientId ORDER BY due_date DESC",
|
||||
new { ClientId = clientId });
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<TaxFilingSchedule>> GetUpcomingDuesAsync(int daysAhead = 30, CancellationToken cancellationToken = default)
|
||||
{
|
||||
using var conn = Conn();
|
||||
return await conn.QueryAsync<TaxFilingSchedule>(
|
||||
@"SELECT id, client_id, filing_type, due_date, filing_year, status, assigned_to, completed_date, notes, created_at, updated_at
|
||||
FROM tax_filing_schedules
|
||||
WHERE status = 'pending' AND due_date BETWEEN CURRENT_DATE AND CURRENT_DATE + INTERVAL '1 day' * @DaysAhead
|
||||
ORDER BY due_date ASC",
|
||||
new { DaysAhead = daysAhead });
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<TaxFilingSchedule>> GetByStatusAsync(string status, CancellationToken cancellationToken = default)
|
||||
{
|
||||
using var conn = Conn();
|
||||
return await conn.QueryAsync<TaxFilingSchedule>(
|
||||
@"SELECT id, client_id, filing_type, due_date, filing_year, status, assigned_to, completed_date, notes, created_at, updated_at
|
||||
FROM tax_filing_schedules WHERE status = @Status ORDER BY due_date",
|
||||
new { Status = status });
|
||||
}
|
||||
|
||||
public async Task UpdateAsync(TaxFilingSchedule schedule, CancellationToken cancellationToken = default)
|
||||
{
|
||||
using var conn = Conn();
|
||||
await conn.ExecuteAsync(
|
||||
@"UPDATE tax_filing_schedules SET filing_type = @FilingType, due_date = @DueDate, status = @Status,
|
||||
assigned_to = @AssignedToId, notes = @Notes, updated_at = NOW() WHERE id = @Id",
|
||||
schedule);
|
||||
}
|
||||
|
||||
public async Task MarkCompletedAsync(int id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
using var conn = Conn();
|
||||
await conn.ExecuteAsync(
|
||||
@"UPDATE tax_filing_schedules SET status = 'completed', completed_date = NOW(), updated_at = NOW() WHERE id = @Id",
|
||||
new { Id = id });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
namespace TaxBaik.Infrastructure.Repositories;
|
||||
|
||||
using Dapper;
|
||||
using TaxBaik.Domain.Entities;
|
||||
using TaxBaik.Domain.Interfaces;
|
||||
|
||||
public class TaxProfileRepository(IDbConnectionFactory connectionFactory) : BaseRepository(connectionFactory), ITaxProfileRepository
|
||||
{
|
||||
public async Task<int> CreateAsync(TaxProfile profile, CancellationToken cancellationToken = default)
|
||||
{
|
||||
using var conn = Conn();
|
||||
return await conn.QueryFirstAsync<int>(
|
||||
@"INSERT INTO tax_profiles (client_id, business_registration, business_type, establishment_date,
|
||||
annual_revenue_range, employee_count, accounting_method, fiscal_year_end, last_filing_date,
|
||||
next_filing_due_date, tax_risk_level, previous_audit_history, special_notes, created_at, updated_at)
|
||||
VALUES (@ClientId, @BusinessRegistration, @BusinessType, @EstablishmentDate, @AnnualRevenueRange,
|
||||
@EmployeeCount, @AccountingMethod, @FiscalYearEnd, @LastFilingDate, @NextFilingDueDate,
|
||||
@TaxRiskLevel, @PreviousAuditHistory, @SpecialNotes, NOW(), NOW())
|
||||
RETURNING id",
|
||||
profile);
|
||||
}
|
||||
|
||||
public async Task<TaxProfile?> GetByClientIdAsync(int clientId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
using var conn = Conn();
|
||||
return await conn.QueryFirstOrDefaultAsync<TaxProfile>(
|
||||
@"SELECT id, client_id, business_registration, business_type, establishment_date,
|
||||
annual_revenue_range, employee_count, accounting_method, fiscal_year_end, last_filing_date,
|
||||
next_filing_due_date, tax_risk_level, previous_audit_history, special_notes, created_at, updated_at
|
||||
FROM tax_profiles WHERE client_id = @ClientId",
|
||||
new { ClientId = clientId });
|
||||
}
|
||||
|
||||
public async Task UpdateAsync(TaxProfile profile, CancellationToken cancellationToken = default)
|
||||
{
|
||||
using var conn = Conn();
|
||||
await conn.ExecuteAsync(
|
||||
@"UPDATE tax_profiles SET business_registration = @BusinessRegistration, business_type = @BusinessType,
|
||||
establishment_date = @EstablishmentDate, annual_revenue_range = @AnnualRevenueRange,
|
||||
employee_count = @EmployeeCount, accounting_method = @AccountingMethod, fiscal_year_end = @FiscalYearEnd,
|
||||
last_filing_date = @LastFilingDate, next_filing_due_date = @NextFilingDueDate,
|
||||
tax_risk_level = @TaxRiskLevel, previous_audit_history = @PreviousAuditHistory,
|
||||
special_notes = @SpecialNotes, updated_at = NOW()
|
||||
WHERE id = @Id",
|
||||
profile);
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<TaxProfile>> GetByRiskLevelAsync(string riskLevel, CancellationToken cancellationToken = default)
|
||||
{
|
||||
using var conn = Conn();
|
||||
return await conn.QueryAsync<TaxProfile>(
|
||||
@"SELECT id, client_id, business_registration, business_type, establishment_date,
|
||||
annual_revenue_range, employee_count, accounting_method, fiscal_year_end, last_filing_date,
|
||||
next_filing_due_date, tax_risk_level, previous_audit_history, special_notes, created_at, updated_at
|
||||
FROM tax_profiles WHERE tax_risk_level = @RiskLevel ORDER BY client_id",
|
||||
new { RiskLevel = riskLevel });
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<TaxProfile>> GetUpcomingFilingDuesAsync(DateTime startDate, DateTime endDate, CancellationToken cancellationToken = default)
|
||||
{
|
||||
using var conn = Conn();
|
||||
return await conn.QueryAsync<TaxProfile>(
|
||||
@"SELECT id, client_id, business_registration, business_type, establishment_date,
|
||||
annual_revenue_range, employee_count, accounting_method, fiscal_year_end, last_filing_date,
|
||||
next_filing_due_date, tax_risk_level, previous_audit_history, special_notes, created_at, updated_at
|
||||
FROM tax_profiles WHERE next_filing_due_date BETWEEN @StartDate AND @EndDate
|
||||
ORDER BY next_filing_due_date",
|
||||
new { StartDate = startDate, EndDate = endDate });
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user