Auto-populate loan details from Plaid liabilities and pre-fill setup form
Fetch loan data (mortgage/student) from Plaid's Liabilities API during sync and auto-create LoanDetail records. For accounts without liabilities data, pre-populate the setup form with balance, interest rate, and smart term defaults. Also fixes maturity date computation in the frontend form. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,10 +1,13 @@
|
|||||||
import api from './api'
|
import api from './api'
|
||||||
import type { LoanDetail, PayoffScenario } from '@/types'
|
import type { LoanDefaults, LoanDetail, PayoffScenario } from '@/types'
|
||||||
|
|
||||||
export const loansApi = {
|
export const loansApi = {
|
||||||
getLoanDetail: (accountId: string) =>
|
getLoanDetail: (accountId: string) =>
|
||||||
api.get<LoanDetail>(`/loans/${accountId}/amortization`),
|
api.get<LoanDetail>(`/loans/${accountId}/amortization`),
|
||||||
|
|
||||||
|
getLoanDefaults: (accountId: string) =>
|
||||||
|
api.get<LoanDefaults>(`/loans/${accountId}/defaults`),
|
||||||
|
|
||||||
createLoanDetail: (accountId: string, data: any) =>
|
createLoanDetail: (accountId: string, data: any) =>
|
||||||
api.post<LoanDetail>(`/loans/${accountId}`, data),
|
api.post<LoanDetail>(`/loans/${accountId}`, data),
|
||||||
|
|
||||||
|
|||||||
@@ -236,6 +236,13 @@ export interface PayoffScenario {
|
|||||||
newSchedule: AmortizationEntry[]
|
newSchedule: AmortizationEntry[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface LoanDefaults {
|
||||||
|
originalBalance: number
|
||||||
|
interestRate: number
|
||||||
|
termMonths: number
|
||||||
|
hasSyncData: boolean
|
||||||
|
}
|
||||||
|
|
||||||
export interface ImportUploadResponse {
|
export interface ImportUploadResponse {
|
||||||
batchId: string
|
batchId: string
|
||||||
totalTransactions: number
|
totalTransactions: number
|
||||||
|
|||||||
@@ -215,6 +215,9 @@
|
|||||||
<v-card>
|
<v-card>
|
||||||
<v-card-title>{{ editingLoan ? 'Edit' : 'Set Up' }} Loan Details</v-card-title>
|
<v-card-title>{{ editingLoan ? 'Edit' : 'Set Up' }} Loan Details</v-card-title>
|
||||||
<v-card-text>
|
<v-card-text>
|
||||||
|
<v-alert v-if="prePopulated && !editingLoan" type="info" variant="tonal" density="compact" class="mb-4">
|
||||||
|
Some fields have been pre-populated from your bank sync data. Please review and adjust as needed.
|
||||||
|
</v-alert>
|
||||||
<v-text-field v-model.number="loanForm.originalBalance" label="Original Balance" type="number" prefix="$" />
|
<v-text-field v-model.number="loanForm.originalBalance" label="Original Balance" type="number" prefix="$" />
|
||||||
<v-text-field v-model.number="loanForm.interestRate" label="Interest Rate (%)" type="number" suffix="%" step="0.125" />
|
<v-text-field v-model.number="loanForm.interestRate" label="Interest Rate (%)" type="number" suffix="%" step="0.125" />
|
||||||
<v-text-field v-model.number="loanForm.termMonths" label="Term (months)" type="number" />
|
<v-text-field v-model.number="loanForm.termMonths" label="Term (months)" type="number" />
|
||||||
@@ -242,7 +245,7 @@ import { useRouter } from 'vue-router'
|
|||||||
import { loansApi } from '@/services/loans'
|
import { loansApi } from '@/services/loans'
|
||||||
import { useAccountsStore } from '@/stores/accounts'
|
import { useAccountsStore } from '@/stores/accounts'
|
||||||
import { formatCurrency, formatDate, accountTypeIcon } from '@/utils/formatters'
|
import { formatCurrency, formatDate, accountTypeIcon } from '@/utils/formatters'
|
||||||
import type { LoanDetail, PayoffScenario } from '@/types'
|
import type { LoanDefaults, LoanDetail, PayoffScenario } from '@/types'
|
||||||
|
|
||||||
const props = defineProps<{ id?: string }>()
|
const props = defineProps<{ id?: string }>()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
@@ -256,6 +259,7 @@ const calculatingPayoff = ref(false)
|
|||||||
const showLoanDialog = ref(false)
|
const showLoanDialog = ref(false)
|
||||||
const showSetup = ref(false)
|
const showSetup = ref(false)
|
||||||
const editingLoan = ref(false)
|
const editingLoan = ref(false)
|
||||||
|
const prePopulated = ref(false)
|
||||||
const snackbar = ref(false)
|
const snackbar = ref(false)
|
||||||
const snackbarText = ref('')
|
const snackbarText = ref('')
|
||||||
const snackbarColor = ref('success')
|
const snackbarColor = ref('success')
|
||||||
@@ -316,8 +320,9 @@ async function loadLoanDetail() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function openSetupDialog() {
|
async function openSetupDialog() {
|
||||||
editingLoan.value = false
|
editingLoan.value = false
|
||||||
|
prePopulated.value = false
|
||||||
loanForm.value = {
|
loanForm.value = {
|
||||||
originalBalance: 0,
|
originalBalance: 0,
|
||||||
interestRate: 0,
|
interestRate: 0,
|
||||||
@@ -327,6 +332,19 @@ function openSetupDialog() {
|
|||||||
escrowAmount: null,
|
escrowAmount: null,
|
||||||
extraPayment: null,
|
extraPayment: null,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (props.id) {
|
||||||
|
try {
|
||||||
|
const { data } = await loansApi.getLoanDefaults(props.id)
|
||||||
|
if (data.originalBalance > 0) loanForm.value.originalBalance = data.originalBalance
|
||||||
|
if (data.interestRate > 0) loanForm.value.interestRate = data.interestRate
|
||||||
|
if (data.termMonths > 0) loanForm.value.termMonths = data.termMonths
|
||||||
|
prePopulated.value = data.hasSyncData
|
||||||
|
} catch {
|
||||||
|
// Fall back to zero defaults
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
showLoanDialog.value = true
|
showLoanDialog.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -349,8 +367,13 @@ async function saveLoanDetail() {
|
|||||||
if (!props.id) return
|
if (!props.id) return
|
||||||
saving.value = true
|
saving.value = true
|
||||||
try {
|
try {
|
||||||
|
const originDate = new Date(loanForm.value.originationDate)
|
||||||
|
const maturityDate = new Date(originDate)
|
||||||
|
maturityDate.setMonth(maturityDate.getMonth() + loanForm.value.termMonths)
|
||||||
|
|
||||||
const payload = {
|
const payload = {
|
||||||
...loanForm.value,
|
...loanForm.value,
|
||||||
|
maturityDate: maturityDate.toISOString(),
|
||||||
escrowAmount: loanForm.value.escrowAmount || undefined,
|
escrowAmount: loanForm.value.escrowAmount || undefined,
|
||||||
extraPayment: loanForm.value.extraPayment || undefined,
|
extraPayment: loanForm.value.extraPayment || undefined,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,10 @@ public class LoansController : ControllerBase
|
|||||||
return detail == null ? NotFound() : Ok(detail);
|
return detail == null ? NotFound() : Ok(detail);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpGet("{accountId}/defaults")]
|
||||||
|
public async Task<ActionResult<LoanDefaultsResponse>> GetLoanDefaults(Guid accountId)
|
||||||
|
=> Ok(await _loanService.GetLoanDefaultsAsync(UserId, accountId));
|
||||||
|
|
||||||
[HttpPost("{accountId}")]
|
[HttpPost("{accountId}")]
|
||||||
public async Task<ActionResult<LoanDetailResponse>> CreateLoanDetail(Guid accountId, CreateLoanDetailRequest request)
|
public async Task<ActionResult<LoanDetailResponse>> CreateLoanDetail(Guid accountId, CreateLoanDetailRequest request)
|
||||||
=> Ok(await _loanService.CreateLoanDetailAsync(UserId, accountId, request));
|
=> Ok(await _loanService.CreateLoanDetailAsync(UserId, accountId, request));
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ public class BankSyncService : IBankSyncService
|
|||||||
private readonly IDuplicateDetectionService _duplicateService;
|
private readonly IDuplicateDetectionService _duplicateService;
|
||||||
private readonly IPayeeService _payeeService;
|
private readonly IPayeeService _payeeService;
|
||||||
private readonly IAiCategorizationService _aiCategorization;
|
private readonly IAiCategorizationService _aiCategorization;
|
||||||
|
private readonly ILoanService _loanService;
|
||||||
private readonly IHubContext<NotificationHub> _hub;
|
private readonly IHubContext<NotificationHub> _hub;
|
||||||
private readonly ILogger<BankSyncService> _logger;
|
private readonly ILogger<BankSyncService> _logger;
|
||||||
|
|
||||||
@@ -32,6 +33,7 @@ public class BankSyncService : IBankSyncService
|
|||||||
IDuplicateDetectionService duplicateService,
|
IDuplicateDetectionService duplicateService,
|
||||||
IPayeeService payeeService,
|
IPayeeService payeeService,
|
||||||
IAiCategorizationService aiCategorization,
|
IAiCategorizationService aiCategorization,
|
||||||
|
ILoanService loanService,
|
||||||
IHubContext<NotificationHub> hub,
|
IHubContext<NotificationHub> hub,
|
||||||
ILogger<BankSyncService> logger)
|
ILogger<BankSyncService> logger)
|
||||||
{
|
{
|
||||||
@@ -42,6 +44,7 @@ public class BankSyncService : IBankSyncService
|
|||||||
_duplicateService = duplicateService;
|
_duplicateService = duplicateService;
|
||||||
_payeeService = payeeService;
|
_payeeService = payeeService;
|
||||||
_aiCategorization = aiCategorization;
|
_aiCategorization = aiCategorization;
|
||||||
|
_loanService = loanService;
|
||||||
_hub = hub;
|
_hub = hub;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
}
|
}
|
||||||
@@ -413,6 +416,12 @@ public class BankSyncService : IBankSyncService
|
|||||||
var accountResults = new List<SyncAccountResult>();
|
var accountResults = new List<SyncAccountResult>();
|
||||||
int totalFetched = 0, totalImported = 0, totalSkipped = 0;
|
int totalFetched = 0, totalImported = 0, totalSkipped = 0;
|
||||||
|
|
||||||
|
// Try to auto-create loan details from Plaid liabilities data
|
||||||
|
if (connection.Provider == SyncProvider.Plaid)
|
||||||
|
{
|
||||||
|
await TryCreateLoanDetailsFromPlaidAsync(userId, connection, config);
|
||||||
|
}
|
||||||
|
|
||||||
// For SimpleFIN, use the earliest start date across all accounts so the
|
// For SimpleFIN, use the earliest start date across all accounts so the
|
||||||
// single API call (cached) returns transactions covering every account.
|
// single API call (cached) returns transactions covering every account.
|
||||||
var endDate = DateTime.UtcNow;
|
var endDate = DateTime.UtcNow;
|
||||||
@@ -625,6 +634,77 @@ public class BankSyncService : IBankSyncService
|
|||||||
accountResults);
|
accountResults);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task TryCreateLoanDetailsFromPlaidAsync(Guid userId, SyncConnection connection, Dictionary<string, string> config)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var liabilities = await _plaid.GetLiabilitiesAsync(config);
|
||||||
|
if (liabilities.Count == 0) return;
|
||||||
|
|
||||||
|
var linkedAccounts = connection.LinkedAccounts
|
||||||
|
.Where(la => la.IsEnabled && la.AccountId != null)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
foreach (var liability in liabilities)
|
||||||
|
{
|
||||||
|
var linked = linkedAccounts.FirstOrDefault(la => la.ExternalAccountId == liability.ExternalAccountId);
|
||||||
|
if (linked?.AccountId == null) continue;
|
||||||
|
|
||||||
|
var accountId = linked.AccountId.Value;
|
||||||
|
|
||||||
|
// Skip if loan detail already exists
|
||||||
|
var existing = await _loanService.GetLoanDetailAsync(userId, accountId);
|
||||||
|
if (existing != null) continue;
|
||||||
|
|
||||||
|
// Update account interest rate from liabilities data
|
||||||
|
if (liability.InterestRate.HasValue && linked.Account != null)
|
||||||
|
{
|
||||||
|
linked.Account.InterestRate = liability.InterestRate.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
var originalBalance = liability.OriginalBalance ?? Math.Abs(linked.LastKnownBalance ?? 0);
|
||||||
|
var interestRate = liability.InterestRate ?? 0;
|
||||||
|
var termMonths = liability.TermMonths ?? (liability.LoanType == "Mortgage" ? 360 : 60);
|
||||||
|
var originationDate = liability.OriginationDate ?? DateTime.UtcNow;
|
||||||
|
var escrowBalance = liability.EscrowBalance;
|
||||||
|
|
||||||
|
// Compute monthly payment via amortization formula if not provided
|
||||||
|
var monthlyPayment = liability.MonthlyPayment ?? ComputeMonthlyPayment(originalBalance, interestRate, termMonths);
|
||||||
|
|
||||||
|
var maturityDate = liability.MaturityDate ?? originationDate.AddMonths(termMonths);
|
||||||
|
|
||||||
|
var request = new CreateLoanDetailRequest(
|
||||||
|
originalBalance,
|
||||||
|
interestRate,
|
||||||
|
termMonths,
|
||||||
|
monthlyPayment,
|
||||||
|
DateTime.SpecifyKind(originationDate, DateTimeKind.Utc),
|
||||||
|
DateTime.SpecifyKind(maturityDate, DateTimeKind.Utc),
|
||||||
|
escrowBalance,
|
||||||
|
null
|
||||||
|
);
|
||||||
|
|
||||||
|
await _loanService.CreateLoanDetailAsync(userId, accountId, request);
|
||||||
|
_logger.LogInformation("Auto-created loan details for account {AccountId} from Plaid liabilities", accountId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "Failed to fetch Plaid liabilities for connection {ConnectionId} - sync will continue without loan details", connection.Id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static decimal ComputeMonthlyPayment(decimal principal, decimal annualRatePercent, int termMonths)
|
||||||
|
{
|
||||||
|
if (principal <= 0 || termMonths <= 0) return 0;
|
||||||
|
if (annualRatePercent <= 0) return principal / termMonths;
|
||||||
|
|
||||||
|
var monthlyRate = annualRatePercent / 100m / 12m;
|
||||||
|
var factor = (double)monthlyRate;
|
||||||
|
var pow = Math.Pow(1 + factor, termMonths);
|
||||||
|
return principal * (decimal)(factor * pow / (pow - 1));
|
||||||
|
}
|
||||||
|
|
||||||
private static Dictionary<string, string> BuildProviderConfig(SyncConnection connection, string accessToken, PlaidConfig? plaidCredentials)
|
private static Dictionary<string, string> BuildProviderConfig(SyncConnection connection, string accessToken, PlaidConfig? plaidCredentials)
|
||||||
{
|
{
|
||||||
if (connection.Provider == SyncProvider.Plaid && plaidCredentials != null)
|
if (connection.Provider == SyncProvider.Plaid && plaidCredentials != null)
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Purrse.Core.DTOs;
|
using Purrse.Core.DTOs;
|
||||||
|
using Purrse.Core.Enums;
|
||||||
using Purrse.Core.Interfaces.Services;
|
using Purrse.Core.Interfaces.Services;
|
||||||
using Purrse.Core.Models;
|
using Purrse.Core.Models;
|
||||||
using Purrse.Data;
|
using Purrse.Data;
|
||||||
@@ -154,6 +155,28 @@ public class LoanService : ILoanService
|
|||||||
totalInterestOriginal - totalInterestNew, monthsSaved, newEntries);
|
totalInterestOriginal - totalInterestNew, monthsSaved, newEntries);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<LoanDefaultsResponse> GetLoanDefaultsAsync(Guid userId, Guid accountId)
|
||||||
|
{
|
||||||
|
var account = await _db.Accounts
|
||||||
|
.Include(a => a.LinkedAccounts)
|
||||||
|
.FirstOrDefaultAsync(a => a.Id == accountId && a.UserId == userId)
|
||||||
|
?? throw new KeyNotFoundException("Account not found");
|
||||||
|
|
||||||
|
var originalBalance = Math.Abs(account.Balance);
|
||||||
|
var interestRate = account.InterestRate ?? 0;
|
||||||
|
var hasSyncData = account.LinkedAccounts.Any() && (account.InterestRate.HasValue || account.Balance != 0);
|
||||||
|
|
||||||
|
var termMonths = account.Type switch
|
||||||
|
{
|
||||||
|
AccountType.Mortgage => 360,
|
||||||
|
AccountType.AutoLoan => 60,
|
||||||
|
AccountType.PersonalLoan => 60,
|
||||||
|
_ => 360
|
||||||
|
};
|
||||||
|
|
||||||
|
return new LoanDefaultsResponse(originalBalance, interestRate, termMonths, hasSyncData);
|
||||||
|
}
|
||||||
|
|
||||||
private static List<AmortizationEntry> GenerateAmortizationSchedule(LoanDetail loan)
|
private static List<AmortizationEntry> GenerateAmortizationSchedule(LoanDetail loan)
|
||||||
{
|
{
|
||||||
var entries = new List<AmortizationEntry>();
|
var entries = new List<AmortizationEntry>();
|
||||||
|
|||||||
@@ -37,6 +37,13 @@ public record AmortizationEntryResponse(
|
|||||||
decimal RemainingBalance
|
decimal RemainingBalance
|
||||||
);
|
);
|
||||||
|
|
||||||
|
public record LoanDefaultsResponse(
|
||||||
|
decimal OriginalBalance,
|
||||||
|
decimal InterestRate,
|
||||||
|
int TermMonths,
|
||||||
|
bool HasSyncData
|
||||||
|
);
|
||||||
|
|
||||||
public record PayoffScenarioRequest(decimal? ExtraMonthlyPayment, decimal? LumpSumPayment, DateTime? LumpSumDate);
|
public record PayoffScenarioRequest(decimal? ExtraMonthlyPayment, decimal? LumpSumPayment, DateTime? LumpSumDate);
|
||||||
|
|
||||||
public record PayoffScenarioResponse(
|
public record PayoffScenarioResponse(
|
||||||
|
|||||||
@@ -9,4 +9,5 @@ public interface ILoanService
|
|||||||
Task<LoanDetailResponse> UpdateLoanDetailAsync(Guid userId, Guid accountId, CreateLoanDetailRequest request);
|
Task<LoanDetailResponse> UpdateLoanDetailAsync(Guid userId, Guid accountId, CreateLoanDetailRequest request);
|
||||||
Task<List<AmortizationEntryResponse>> GetAmortizationScheduleAsync(Guid userId, Guid accountId);
|
Task<List<AmortizationEntryResponse>> GetAmortizationScheduleAsync(Guid userId, Guid accountId);
|
||||||
Task<PayoffScenarioResponse> CalculatePayoffScenarioAsync(Guid userId, Guid accountId, PayoffScenarioRequest request);
|
Task<PayoffScenarioResponse> CalculatePayoffScenarioAsync(Guid userId, Guid accountId, PayoffScenarioRequest request);
|
||||||
|
Task<LoanDefaultsResponse> GetLoanDefaultsAsync(Guid userId, Guid accountId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
namespace Purrse.Plugins.Abstractions.Models;
|
||||||
|
|
||||||
|
public class SyncLoanData
|
||||||
|
{
|
||||||
|
public string ExternalAccountId { get; set; } = string.Empty;
|
||||||
|
public decimal? InterestRate { get; set; }
|
||||||
|
public decimal? OriginalBalance { get; set; }
|
||||||
|
public DateTime? OriginationDate { get; set; }
|
||||||
|
public DateTime? MaturityDate { get; set; }
|
||||||
|
public int? TermMonths { get; set; }
|
||||||
|
public decimal? MonthlyPayment { get; set; }
|
||||||
|
public decimal? EscrowBalance { get; set; }
|
||||||
|
public string? LoanType { get; set; }
|
||||||
|
}
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
|
using System.Text.RegularExpressions;
|
||||||
using Going.Plaid;
|
using Going.Plaid;
|
||||||
using Going.Plaid.Entity;
|
using Going.Plaid.Entity;
|
||||||
using Going.Plaid.Item;
|
using Going.Plaid.Item;
|
||||||
|
using Going.Plaid.Liabilities;
|
||||||
using Going.Plaid.Link;
|
using Going.Plaid.Link;
|
||||||
using Purrse.Plugins.Abstractions;
|
using Purrse.Plugins.Abstractions;
|
||||||
using Purrse.Plugins.Abstractions.Models;
|
using Purrse.Plugins.Abstractions.Models;
|
||||||
@@ -43,7 +45,7 @@ public class PlaidSyncProvider : IAccountSyncProvider
|
|||||||
Secret = credentials.Secret,
|
Secret = credentials.Secret,
|
||||||
User = new LinkTokenCreateRequestUser { ClientUserId = userId },
|
User = new LinkTokenCreateRequestUser { ClientUserId = userId },
|
||||||
ClientName = "Purrse",
|
ClientName = "Purrse",
|
||||||
Products = new[] { Products.Transactions },
|
Products = new[] { Products.Transactions, Products.Liabilities },
|
||||||
CountryCodes = new[] { CountryCode.Us },
|
CountryCodes = new[] { CountryCode.Us },
|
||||||
Language = Language.English
|
Language = Language.English
|
||||||
});
|
});
|
||||||
@@ -190,6 +192,75 @@ public class PlaidSyncProvider : IAccountSyncProvider
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<List<SyncLoanData>> GetLiabilitiesAsync(Dictionary<string, string> configuration)
|
||||||
|
{
|
||||||
|
var credentials = ExtractCredentials(configuration);
|
||||||
|
var client = CreateClient(credentials.Environment);
|
||||||
|
var accessToken = configuration.GetValueOrDefault("AccessToken") ?? string.Empty;
|
||||||
|
|
||||||
|
var response = await client.LiabilitiesGetAsync(new LiabilitiesGetRequest
|
||||||
|
{
|
||||||
|
ClientId = credentials.ClientId,
|
||||||
|
Secret = credentials.Secret,
|
||||||
|
AccessToken = accessToken
|
||||||
|
});
|
||||||
|
|
||||||
|
var results = new List<SyncLoanData>();
|
||||||
|
|
||||||
|
if (response.Liabilities?.Mortgage != null)
|
||||||
|
{
|
||||||
|
foreach (var m in response.Liabilities.Mortgage)
|
||||||
|
{
|
||||||
|
results.Add(new SyncLoanData
|
||||||
|
{
|
||||||
|
ExternalAccountId = m.AccountId,
|
||||||
|
InterestRate = (decimal?)(m.InterestRate?.Percentage),
|
||||||
|
OriginalBalance = (decimal?)(m.OriginationPrincipalAmount),
|
||||||
|
OriginationDate = m.OriginationDate?.ToDateTime(TimeOnly.MinValue),
|
||||||
|
MaturityDate = m.MaturityDate?.ToDateTime(TimeOnly.MinValue),
|
||||||
|
TermMonths = ParseLoanTermToMonths(m.LoanTerm),
|
||||||
|
MonthlyPayment = (decimal?)(m.NextMonthlyPayment),
|
||||||
|
EscrowBalance = (decimal?)(m.EscrowBalance),
|
||||||
|
LoanType = "Mortgage"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.Liabilities?.Student != null)
|
||||||
|
{
|
||||||
|
foreach (var s in response.Liabilities.Student)
|
||||||
|
{
|
||||||
|
results.Add(new SyncLoanData
|
||||||
|
{
|
||||||
|
ExternalAccountId = s.AccountId,
|
||||||
|
InterestRate = (decimal?)(s.InterestRatePercentage),
|
||||||
|
OriginalBalance = (decimal?)(s.OriginationPrincipalAmount),
|
||||||
|
OriginationDate = s.OriginationDate?.ToDateTime(TimeOnly.MinValue),
|
||||||
|
MaturityDate = s.ExpectedPayoffDate?.ToDateTime(TimeOnly.MinValue),
|
||||||
|
MonthlyPayment = (decimal?)(s.MinimumPaymentAmount),
|
||||||
|
LoanType = "StudentLoan"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static int? ParseLoanTermToMonths(string? loanTerm)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(loanTerm)) return null;
|
||||||
|
|
||||||
|
var match = Regex.Match(loanTerm, @"(\d+)\s*year", RegexOptions.IgnoreCase);
|
||||||
|
if (match.Success && int.TryParse(match.Groups[1].Value, out var years))
|
||||||
|
return years * 12;
|
||||||
|
|
||||||
|
match = Regex.Match(loanTerm, @"(\d+)\s*month", RegexOptions.IgnoreCase);
|
||||||
|
if (match.Success && int.TryParse(match.Groups[1].Value, out var months))
|
||||||
|
return months;
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
private static string MapAccountType(AccountType? type, AccountSubtype? subtype)
|
private static string MapAccountType(AccountType? type, AccountSubtype? subtype)
|
||||||
{
|
{
|
||||||
return type switch
|
return type switch
|
||||||
|
|||||||
Reference in New Issue
Block a user