Skip to content

Commit

Permalink
refactor: change 'expire' to 'mature' in facility (#1494)
Browse files Browse the repository at this point in the history
In the context of a facility, 'expired' usually refers to the date that
no more drawdowns are allowed from, whereas 'matured' referes to the
date where outstanding principal payments are due.

In our domain, these are the same thing, but the more relevant concept
for our context is the payment due date, so 'mature' makes more sense as
the word to use.

This refactor would also have the benefit or re-introducing the concept
of 'expired' if we decided to stop drawdowns at some point before the
facility matures.
  • Loading branch information
vindard authored Feb 25, 2025
1 parent a33edf7 commit 70a7a0f
Show file tree
Hide file tree
Showing 26 changed files with 134 additions and 138 deletions.
2 changes: 1 addition & 1 deletion apps/admin-panel/app/command-menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,7 @@ const CommandMenu = () => {
condition: () =>
facility?.subjectCanUpdateCollateral &&
facility?.status !== CreditFacilityStatus.Closed &&
facility?.status !== CreditFacilityStatus.Expired,
facility?.status !== CreditFacilityStatus.Matured,
},
{
label: "Create Disbursal",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,8 @@ const CreditFacilityDetailsCard: React.FC<CreditFacilityDetailsProps> = ({
),
},
{
label: "Expires At",
value: formatDate(creditFacilityDetails.expiresAt),
label: "Matures At",
value: formatDate(creditFacilityDetails.maturesAt),
},
]

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ gql`
creditFacilityId
status
facilityAmount
expiresAt
maturesAt
collateral
collateralizationState
createdAt
Expand Down
2 changes: 1 addition & 1 deletion apps/admin-panel/codegen.yml
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ generates:
createdAt: "generateMockValue.timestamp()"
updatedAt: "generateMockValue.timestamp()"
activatedAt: "generateMockValue.timestamp()"
expiresAt: "generateMockValue.timestamp()"
maturesAt: "generateMockValue.timestamp()"
recordedAt: "generateMockValue.timestamp()"
votedAt: "generateMockValue.timestamp()"
telegramId: "generateMockValue.telegramId()"
Expand Down
16 changes: 8 additions & 8 deletions apps/admin-panel/lib/graphql/generated/index.ts

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion apps/admin-panel/lib/graphql/generated/mocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -503,9 +503,9 @@ export const mockCreditFacility = (overrides?: Partial<CreditFacility>, _relatio
currentCvl: overrides && overrides.hasOwnProperty('currentCvl') ? overrides.currentCvl! : relationshipsToOmit.has('FacilityCvl') ? {} as FacilityCvl : mockFacilityCvl({}, relationshipsToOmit),
customer: overrides && overrides.hasOwnProperty('customer') ? overrides.customer! : relationshipsToOmit.has('Customer') ? {} as Customer : mockCustomer({}, relationshipsToOmit),
disbursals: overrides && overrides.hasOwnProperty('disbursals') ? overrides.disbursals! : [relationshipsToOmit.has('CreditFacilityDisbursal') ? {} as CreditFacilityDisbursal : mockCreditFacilityDisbursal({}, relationshipsToOmit)],
expiresAt: overrides && overrides.hasOwnProperty('expiresAt') ? overrides.expiresAt! : generateMockValue.timestamp(),
facilityAmount: overrides && overrides.hasOwnProperty('facilityAmount') ? overrides.facilityAmount! : generateMockValue.usdCents(),
id: overrides && overrides.hasOwnProperty('id') ? overrides.id! : faker.string.uuid(),
maturesAt: overrides && overrides.hasOwnProperty('maturesAt') ? overrides.maturesAt! : generateMockValue.timestamp(),
repaymentPlan: overrides && overrides.hasOwnProperty('repaymentPlan') ? overrides.repaymentPlan! : [relationshipsToOmit.has('CreditFacilityRepaymentInPlan') ? {} as CreditFacilityRepaymentInPlan : mockCreditFacilityRepaymentInPlan({}, relationshipsToOmit)],
status: overrides && overrides.hasOwnProperty('status') ? overrides.status! : mockEnums.creditFacilityStatus(),
subjectCanComplete: overrides && overrides.hasOwnProperty('subjectCanComplete') ? overrides.subjectCanComplete! : faker.datatype.boolean(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ gql`
status
createdAt
activatedAt
expiresAt
maturesAt
disbursals {
id
disbursalId
Expand Down Expand Up @@ -168,8 +168,8 @@ async function page({ params }: { params: { "credit-facility-id": string } }) {
value: removeUnderscore(data.creditFacility.collateralizationState),
},
{
label: "Expires At",
value: formatDate(data.creditFacility.expiresAt) || "N/A",
label: "Matures At",
value: formatDate(data.creditFacility.maturesAt) || "N/A",
},
{
label: "Status",
Expand Down
8 changes: 4 additions & 4 deletions apps/customer-portal/lib/graphql/generated/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ export type CreditFacility = {
creditFacilityTerms: TermValues;
currentCvl: FacilityCvl;
disbursals: Array<CreditFacilityDisbursal>;
expiresAt?: Maybe<Scalars['Timestamp']['output']>;
maturesAt?: Maybe<Scalars['Timestamp']['output']>;
facilityAmount: Scalars['UsdCents']['output'];
id: Scalars['ID']['output'];
repaymentPlan: Array<CreditFacilityRepaymentInPlan>;
Expand Down Expand Up @@ -178,7 +178,7 @@ export enum CreditFacilityRepaymentType {
export enum CreditFacilityStatus {
Active = 'ACTIVE',
Closed = 'CLOSED',
Expired = 'EXPIRED',
Matured = 'MATURED',
PendingApproval = 'PENDING_APPROVAL',
PendingCollateralization = 'PENDING_COLLATERALIZATION'
}
Expand Down Expand Up @@ -415,7 +415,7 @@ export type GetCreditFacilityQueryVariables = Exact<{
}>;


export type GetCreditFacilityQuery = { __typename?: 'Query', creditFacility?: { __typename?: 'CreditFacility', id: string, creditFacilityId: any, facilityAmount: any, collateral: any, collateralizationState: CollateralizationState, status: CreditFacilityStatus, createdAt: any, activatedAt?: any | null, expiresAt?: any | null, disbursals: Array<{ __typename?: 'CreditFacilityDisbursal', id: string, disbursalId: any, index: any, amount: any, status: DisbursalStatus, createdAt: any }>, creditFacilityTerms: { __typename?: 'TermValues', annualRate: any, accrualInterval: InterestInterval, incurrenceInterval: InterestInterval, oneTimeFeeRate: any, liquidationCvl: any, marginCallCvl: any, initialCvl: any, duration: { __typename?: 'Duration', period: Period, units: number } }, balance: { __typename?: 'CreditFacilityBalance', facilityRemaining: { __typename?: 'FacilityRemaining', usdBalance: any }, disbursed: { __typename?: 'Disbursed', total: { __typename?: 'Total', usdBalance: any }, outstanding: { __typename?: 'Outstanding', usdBalance: any }, dueOutstanding: { __typename?: 'Outstanding', usdBalance: any } }, interest: { __typename?: 'Interest', total: { __typename?: 'Total', usdBalance: any }, outstanding: { __typename?: 'Outstanding', usdBalance: any }, dueOutstanding: { __typename?: 'Outstanding', usdBalance: any } }, collateral: { __typename?: 'Collateral', btcBalance: any }, dueOutstanding: { __typename?: 'Outstanding', usdBalance: any }, outstanding: { __typename?: 'Outstanding', usdBalance: any } }, currentCvl: { __typename?: 'FacilityCVL', total: any, disbursed: any }, repaymentPlan: Array<{ __typename?: 'CreditFacilityRepaymentInPlan', repaymentType: CreditFacilityRepaymentType, status: CreditFacilityRepaymentStatus, initial: any, outstanding: any, accrualAt: any, dueAt: any }>, transactions: Array<{ __typename?: 'CreditFacilityCollateralUpdated', satoshis: any, recordedAt: any, action: CollateralAction, txId: any } | { __typename?: 'CreditFacilityCollateralizationUpdated', state: CollateralizationState, collateral: any, outstandingInterest: any, outstandingDisbursal: any, recordedAt: any, price: any } | { __typename?: 'CreditFacilityDisbursalExecuted', cents: any, recordedAt: any, txId: any } | { __typename?: 'CreditFacilityIncrementalPayment', cents: any, recordedAt: any, txId: any } | { __typename?: 'CreditFacilityInterestAccrued', cents: any, recordedAt: any, txId: any, days: number } | { __typename?: 'CreditFacilityOrigination', cents: any, recordedAt: any, txId: any }> } | null };
export type GetCreditFacilityQuery = { __typename?: 'Query', creditFacility?: { __typename?: 'CreditFacility', id: string, creditFacilityId: any, facilityAmount: any, collateral: any, collateralizationState: CollateralizationState, status: CreditFacilityStatus, createdAt: any, activatedAt?: any | null, maturesAt?: any | null, disbursals: Array<{ __typename?: 'CreditFacilityDisbursal', id: string, disbursalId: any, index: any, amount: any, status: DisbursalStatus, createdAt: any }>, creditFacilityTerms: { __typename?: 'TermValues', annualRate: any, accrualInterval: InterestInterval, incurrenceInterval: InterestInterval, oneTimeFeeRate: any, liquidationCvl: any, marginCallCvl: any, initialCvl: any, duration: { __typename?: 'Duration', period: Period, units: number } }, balance: { __typename?: 'CreditFacilityBalance', facilityRemaining: { __typename?: 'FacilityRemaining', usdBalance: any }, disbursed: { __typename?: 'Disbursed', total: { __typename?: 'Total', usdBalance: any }, outstanding: { __typename?: 'Outstanding', usdBalance: any }, dueOutstanding: { __typename?: 'Outstanding', usdBalance: any } }, interest: { __typename?: 'Interest', total: { __typename?: 'Total', usdBalance: any }, outstanding: { __typename?: 'Outstanding', usdBalance: any }, dueOutstanding: { __typename?: 'Outstanding', usdBalance: any } }, collateral: { __typename?: 'Collateral', btcBalance: any }, dueOutstanding: { __typename?: 'Outstanding', usdBalance: any }, outstanding: { __typename?: 'Outstanding', usdBalance: any } }, currentCvl: { __typename?: 'FacilityCVL', total: any, disbursed: any }, repaymentPlan: Array<{ __typename?: 'CreditFacilityRepaymentInPlan', repaymentType: CreditFacilityRepaymentType, status: CreditFacilityRepaymentStatus, initial: any, outstanding: any, accrualAt: any, dueAt: any }>, transactions: Array<{ __typename?: 'CreditFacilityCollateralUpdated', satoshis: any, recordedAt: any, action: CollateralAction, txId: any } | { __typename?: 'CreditFacilityCollateralizationUpdated', state: CollateralizationState, collateral: any, outstandingInterest: any, outstandingDisbursal: any, recordedAt: any, price: any } | { __typename?: 'CreditFacilityDisbursalExecuted', cents: any, recordedAt: any, txId: any } | { __typename?: 'CreditFacilityIncrementalPayment', cents: any, recordedAt: any, txId: any } | { __typename?: 'CreditFacilityInterestAccrued', cents: any, recordedAt: any, txId: any, days: number } | { __typename?: 'CreditFacilityOrigination', cents: any, recordedAt: any, txId: any }> } | null };

export type MeQueryVariables = Exact<{ [key: string]: never; }>;

Expand Down Expand Up @@ -447,7 +447,7 @@ export const GetCreditFacilityDocument = gql`
status
createdAt
activatedAt
expiresAt
maturesAt
disbursals {
id
disbursalId
Expand Down
2 changes: 1 addition & 1 deletion bats/admin-gql/find-credit-facility.gql
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ query creditFacility($id: UUID!) {
creditFacility(id: $id) {
creditFacilityId
status
expiresAt
maturesAt
collateralizationState
facilityAmount
disbursals {
Expand Down
4 changes: 2 additions & 2 deletions bats/credit-facility.bats
Original file line number Diff line number Diff line change
Expand Up @@ -178,8 +178,8 @@ ymd() {
[[ "$amount" -gt "0" ]] || exit 1

last_accrual_at=$(echo $last_accrual | jq -r '.recordedAt' | ymd)
expires_at=$(graphql_output '.data.creditFacility.expiresAt' | ymd)
[[ "$last_accrual_at" == "$expires_at" ]] || exit 1
matures_at=$(graphql_output '.data.creditFacility.maturesAt' | ymd)
[[ "$last_accrual_at" == "$matures_at" ]] || exit 1

assert_accounts_balanced
}
52 changes: 26 additions & 26 deletions core/credit/src/credit_facility/entity.rs
Original file line number Diff line number Diff line change
Expand Up @@ -322,7 +322,7 @@ pub struct CreditFacility {
#[builder(setter(strip_option), default)]
pub activated_at: Option<DateTime<Utc>>,
#[builder(setter(strip_option), default)]
pub expires_at: Option<DateTime<Utc>>,
pub matures_at: Option<DateTime<Utc>>,

#[es_entity(nested)]
#[builder(default)]
Expand Down Expand Up @@ -383,7 +383,7 @@ impl CreditFacility {
}

fn disbursed_due(&self) -> UsdCents {
if self.is_expired() {
if self.is_matured() {
self.total_disbursed()
} else {
UsdCents::ZERO
Expand Down Expand Up @@ -468,16 +468,16 @@ impl CreditFacility {
false
}

pub fn is_expired(&self) -> bool {
pub fn is_matured(&self) -> bool {
let now = crate::time::now();
self.expires_at.is_some_and(|expires_at| now > expires_at)
self.matures_at.is_some_and(|matures_at| now > matures_at)
}

pub fn status(&self) -> CreditFacilityStatus {
if self.is_completed() {
CreditFacilityStatus::Closed
} else if self.is_expired() {
CreditFacilityStatus::Expired
} else if self.is_matured() {
CreditFacilityStatus::Matured
} else if self.is_activated() {
CreditFacilityStatus::Active
} else if self.is_fully_collateralized() {
Expand Down Expand Up @@ -532,7 +532,7 @@ impl CreditFacility {
.check_approval_allowed(self.terms)?;

self.activated_at = Some(activated_at);
self.expires_at = Some(self.terms.duration.expiration_date(activated_at));
self.matures_at = Some(self.terms.duration.maturity_date(activated_at));
let tx_id = LedgerTxId::new();
self.events.push(CreditFacilityEvent::Activated {
ledger_tx_id: tx_id,
Expand Down Expand Up @@ -564,9 +564,9 @@ impl CreditFacility {
approval_process_id: Option<ApprovalProcessId>,
audit_info: AuditInfo,
) -> Result<NewDisbursal, CreditFacilityError> {
if let Some(expires_at) = self.expires_at {
if initiated_at > expires_at {
return Err(CreditFacilityError::DisbursalPastExpiryDate);
if let Some(matures_at) = self.matures_at {
if initiated_at > matures_at {
return Err(CreditFacilityError::DisbursalPastMaturityDate);
}
}

Expand Down Expand Up @@ -647,7 +647,7 @@ impl CreditFacility {
),
};

Ok(full_period.truncate(self.expires_at.expect("Facility is already active")))
Ok(full_period.truncate(self.matures_at.expect("Facility is already active")))
}

pub(crate) fn start_interest_accrual(
Expand Down Expand Up @@ -686,7 +686,7 @@ impl CreditFacility {
.credit_facility_id(self.id)
.idx(idx)
.started_at(accrual_period.start)
.facility_expires_at(self.expires_at.expect("Facility is already approved"))
.facility_matures_at(self.matures_at.expect("Facility is already approved"))
.terms(self.terms)
.audit_info(audit_info)
.build()
Expand Down Expand Up @@ -898,7 +898,7 @@ impl CreditFacility {
| CreditFacilityStatus::PendingApproval => facility_cvl
.total
.collateralization_update(self.terms, last_collateralization_state, None, true),
CreditFacilityStatus::Active | CreditFacilityStatus::Expired => {
CreditFacilityStatus::Active | CreditFacilityStatus::Matured => {
let cvl = if self.total_disbursed() == UsdCents::ZERO {
facility_cvl.total
} else {
Expand Down Expand Up @@ -1126,11 +1126,11 @@ impl TryFromEvents<CreditFacilityEvent> for CreditFacility {
.approval_process_id(*approval_process_id)
}
CreditFacilityEvent::Activated { activated_at, .. } => {
builder = builder.activated_at(*activated_at).expires_at(
builder = builder.activated_at(*activated_at).matures_at(
terms
.expect("terms should be set")
.duration
.expiration_date(*activated_at),
.maturity_date(*activated_at),
)
}
CreditFacilityEvent::ApprovalProcessConcluded { .. } => (),
Expand Down Expand Up @@ -1381,7 +1381,7 @@ mod test {
}

#[test]
fn outstanding_from_due_before_expiry() {
fn outstanding_from_due_before_maturity() {
let mut events = initial_events();
let activated_at = Utc::now();
let disbursal_id = DisbursalId::new();
Expand Down Expand Up @@ -1415,7 +1415,7 @@ mod test {
}

#[test]
fn outstanding_from_due_after_expiry() {
fn outstanding_from_due_after_maturity() {
let mut events = initial_events();
let activated_at = "2023-01-01T00:00:00Z".parse::<DateTime<Utc>>().unwrap();
let disbursal_id = DisbursalId::new();
Expand Down Expand Up @@ -1613,9 +1613,9 @@ mod test {
.credit_facility_id(credit_facility.id)
.idx(new_idx)
.started_at(accrual_starts_at)
.facility_expires_at(
.facility_matures_at(
credit_facility
.expires_at
.matures_at
.expect("Facility is already approved"),
)
.terms(credit_facility.terms)
Expand All @@ -1631,7 +1631,7 @@ mod test {
assert_eq!(
accrual_period.start.format("%Y-%m").to_string(),
credit_facility
.expires_at
.matures_at
.unwrap()
.format("%Y-%m")
.to_string()
Expand Down Expand Up @@ -1699,7 +1699,7 @@ mod test {
fn check_activated_at() {
let mut credit_facility = facility_from(initial_events());
assert_eq!(credit_facility.activated_at, None);
assert_eq!(credit_facility.expires_at, None);
assert_eq!(credit_facility.matures_at, None);

credit_facility
.record_collateral_update(
Expand All @@ -1720,7 +1720,7 @@ mod test {
.unwrap()
.did_execute());
assert_eq!(credit_facility.activated_at, Some(approval_time));
assert!(credit_facility.expires_at.is_some())
assert!(credit_facility.matures_at.is_some())
}

#[test]
Expand Down Expand Up @@ -2017,7 +2017,7 @@ mod test {
}

#[test]
fn initiate_repayment_before_expiry_errors_for_amount_above_interest() {
fn initiate_repayment_before_maturity_errors_for_amount_above_interest() {
let activated_at = Utc::now();
let mut credit_facility = credit_facility_with_interest_accrual(activated_at);
let interest = credit_facility.outstanding().interest;
Expand All @@ -2043,7 +2043,7 @@ mod test {
}

#[test]
fn initiate_repayment_after_expiry_errors_for_amount_above_total() {
fn initiate_repayment_after_maturity_errors_for_amount_above_total() {
let activated_at = "2023-01-01T00:00:00Z".parse::<DateTime<Utc>>().unwrap();
let mut credit_facility = credit_facility_with_interest_accrual(activated_at);
let outstanding = credit_facility.outstanding().total();
Expand All @@ -2069,7 +2069,7 @@ mod test {
}

#[test]
fn confirm_repayment_before_expiry() {
fn confirm_repayment_before_maturity() {
let activated_at = Utc::now();
let mut credit_facility = credit_facility_with_interest_accrual(activated_at);

Expand All @@ -2094,7 +2094,7 @@ mod test {
}

#[test]
fn confirm_partial_repayment_after_expiry() {
fn confirm_partial_repayment_after_maturity() {
let activated_at = "2023-01-01T00:00:00Z".parse::<DateTime<Utc>>().unwrap();
let mut credit_facility = credit_facility_with_interest_accrual(activated_at);

Expand Down
4 changes: 2 additions & 2 deletions core/credit/src/credit_facility/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ pub enum CreditFacilityError {
ApprovalInProgress,
#[error("CreditFacilityError - Denied")]
Denied,
#[error("CreditFacilityError - DisbursalExpiryDate")]
DisbursalPastExpiryDate,
#[error("CreditFacilityError - DisbursalMaturityDate")]
DisbursalPastMaturityDate,
#[error("CreditFacilityError - NotActivatedYet")]
NotActivatedYet,
#[error("CreditFacilityError - InterestAccrualNotCompletedYet")]
Expand Down
10 changes: 5 additions & 5 deletions core/credit/src/credit_facility/repayment_plan.rs
Original file line number Diff line number Diff line change
Expand Up @@ -120,13 +120,13 @@ pub(super) fn project<'a>(
.map(CreditFacilityRepaymentInPlan::Interest)
.collect();

let expiry_date = terms.duration.expiration_date(activated_at);
let maturity_date = terms.duration.maturity_date(activated_at);
let last_interest_payment = last_interest_accrual_at.unwrap_or(activated_at);
let mut next_interest_period = terms
.accrual_interval
.period_from(last_interest_payment)
.next()
.truncate(expiry_date);
.truncate(maturity_date);

if !due_and_outstanding.is_zero() {
while let Some(period) = next_interest_period {
Expand All @@ -142,7 +142,7 @@ pub(super) fn project<'a>(
due_at: period.end,
}));

next_interest_period = period.next().truncate(expiry_date);
next_interest_period = period.next().truncate(maturity_date);
}
}

Expand All @@ -154,8 +154,8 @@ pub(super) fn project<'a>(
},
initial: total_disbursed,
outstanding: due_and_outstanding_disbursed,
accrual_at: expiry_date,
due_at: expiry_date,
accrual_at: maturity_date,
due_at: maturity_date,
}));

res
Expand Down
Loading

0 comments on commit 70a7a0f

Please sign in to comment.