@@ -310,6 +310,104 @@ mod propchain_lending {
310310 pub start_block : Option < u64 > ,
311311 }
312312
313+ // ── #829: Variable amortization schedules ────────────────────────────────
314+
315+ /// The repayment schedule type for a loan.
316+ #[ derive(
317+ Debug ,
318+ Clone ,
319+ Copy ,
320+ PartialEq ,
321+ Eq ,
322+ scale:: Encode ,
323+ scale:: Decode ,
324+ ink:: storage:: traits:: StorageLayout ,
325+ ) ]
326+ #[ cfg_attr( feature = "std" , derive( scale_info:: TypeInfo ) ) ]
327+ pub enum Schedule {
328+ /// Single lump-sum repayment at maturity (principal + all interest).
329+ Bullet ,
330+ /// Equal total payments each period (principal portion grows over time).
331+ Annuity ,
332+ /// Equal principal payments each period (descending total outlay).
333+ Linear ,
334+ /// User-defined custom schedule parameters.
335+ Custom {
336+ /// Custom number of installments.
337+ num_installments : u32 ,
338+ /// Custom interval between payments (blocks).
339+ interval_blocks : u64 ,
340+ /// Custom principal per installment (0 = computed from total).
341+ principal_per_payment : u128 ,
342+ } ,
343+ }
344+
345+ impl Schedule {
346+ /// Compute the per-period installment amount given total principal,
347+ /// interest rate (bps), term months, and blocks per period.
348+ pub fn installment (
349+ & self ,
350+ principal : u128 ,
351+ rate_bps : u32 ,
352+ term_months : u32 ,
353+ interval_blocks : u64 ,
354+ ) -> u128 {
355+ match self {
356+ Schedule :: Bullet => principal, // full principal at end
357+ Schedule :: Annuity => {
358+ // Simplified annuity: equal total payments each period.
359+ // per_period_rate = rate_bps * interval_blocks / (5_256_000 * 10_000)
360+ let n = ( term_months as u64 * 432_000u64 )
361+ . checked_div ( interval_blocks. max ( 1 ) )
362+ . unwrap_or ( 1 ) as u128 ;
363+ if n == 0 {
364+ return principal;
365+ }
366+ let per_period_rate_numer =
367+ ( rate_bps as u128 ) . saturating_mul ( interval_blocks as u128 ) ;
368+ let per_period_rate_denom = 52_560_000_000u128 ; // 5_256_000 * 10_000
369+ let interest = principal
370+ . saturating_mul ( per_period_rate_numer)
371+ . checked_div ( per_period_rate_denom)
372+ . unwrap_or ( 0 ) ;
373+ let base = principal / n;
374+ base. saturating_add ( interest)
375+ }
376+ Schedule :: Linear => {
377+ let n = ( term_months as u64 * 432_000u64 )
378+ . checked_div ( interval_blocks. max ( 1 ) )
379+ . unwrap_or ( 1 ) as u128 ;
380+ if n == 0 {
381+ return principal;
382+ }
383+ // Equal principal + declining interest
384+ let per_period_principal = principal / n;
385+ let per_period_rate_numer =
386+ ( rate_bps as u128 ) . saturating_mul ( interval_blocks as u128 ) ;
387+ let per_period_rate_denom = 52_560_000_000u128 ; // 5_256_000 * 10_000
388+ let interest_first = principal
389+ . saturating_mul ( per_period_rate_numer)
390+ . checked_div ( per_period_rate_denom)
391+ . unwrap_or ( 0 ) ;
392+ per_period_principal. saturating_add ( interest_first)
393+ }
394+ Schedule :: Custom {
395+ principal_per_payment,
396+ ..
397+ } => {
398+ if * principal_per_payment > 0 {
399+ * principal_per_payment
400+ } else {
401+ let n = ( term_months as u64 * 432_000u64 )
402+ . checked_div ( interval_blocks. max ( 1 ) )
403+ . unwrap_or ( 1 ) as u128 ;
404+ principal. checked_div ( n) . unwrap_or ( principal)
405+ }
406+ }
407+ }
408+ }
409+ }
410+
313411 #[ derive(
314412 Debug ,
315413 Clone ,
@@ -334,6 +432,7 @@ mod propchain_lending {
334432 pub schedule_id : u64 ,
335433 pub loan_id : u64 ,
336434 pub borrower : AccountId ,
435+ pub schedule_type : Schedule ,
337436 pub principal_due : u128 ,
338437 pub interest_due : u128 ,
339438 pub installment_amount : u128 ,
@@ -424,6 +523,8 @@ mod propchain_lending {
424523 Cancelled ,
425524 }
426525
526+ pub type TokenId = u64 ;
527+
427528 /// A borrower's public loan request listed on the marketplace (#304).
428529 #[ derive(
429530 Debug , Clone , PartialEq , scale:: Encode , scale:: Decode , ink:: storage:: traits:: StorageLayout ,
@@ -438,6 +539,8 @@ mod propchain_lending {
438539 pub max_rate_bps : u32 ,
439540 pub term_months : u32 ,
440541 pub collateral_kind : CollateralKind ,
542+ /// Multi-token collateral basket: (token_id, amount) pairs (#827).
543+ pub collateral_basket : Vec < ( TokenId , u128 ) > ,
441544 pub status : ListingStatus ,
442545 pub created_at : u64 ,
443546 /// ID of the accepted offer, if any.
@@ -1168,6 +1271,70 @@ mod propchain_lending {
11681271 Ok ( ( ) )
11691272 }
11701273
1274+ /// Create a payment schedule for a loan with a specified schedule type (#829).
1275+ #[ ink( message) ]
1276+ pub fn create_payment_schedule (
1277+ & mut self ,
1278+ loan_id : u64 ,
1279+ schedule_type : Schedule ,
1280+ interval_blocks : u64 ,
1281+ ) -> Result < u64 , LendingError > {
1282+ let caller = self . env ( ) . caller ( ) ;
1283+ let loan = self
1284+ . loan_applications
1285+ . get ( loan_id)
1286+ . ok_or ( LendingError :: LoanNotFound ) ?;
1287+
1288+ if caller != self . admin && caller != loan. applicant {
1289+ return Err ( LendingError :: Unauthorized ) ;
1290+ }
1291+ if interval_blocks == 0 {
1292+ return Err ( LendingError :: InvalidParameters ) ;
1293+ }
1294+
1295+ // Calculate installment amount based on schedule type
1296+ let installment = schedule_type. installment (
1297+ loan. requested_amount ,
1298+ loan. interest_rate_bps ,
1299+ loan. term_months ,
1300+ interval_blocks,
1301+ ) ;
1302+
1303+ let total_blocks = loan. term_months as u64 * 432_000 ;
1304+ let total_installments = ( total_blocks / interval_blocks) as u32 ;
1305+ let now = self . env ( ) . block_number ( ) as u64 ;
1306+
1307+ self . schedule_count += 1 ;
1308+ let schedule = PaymentSchedule {
1309+ schedule_id : self . schedule_count ,
1310+ loan_id,
1311+ borrower : loan. applicant ,
1312+ schedule_type,
1313+ principal_due : loan. requested_amount ,
1314+ interest_due : 0 ,
1315+ installment_amount : installment,
1316+ total_installments : total_installments. max ( 1 ) ,
1317+ installments_paid : 0 ,
1318+ first_due_block : now. saturating_add ( interval_blocks) ,
1319+ interval_blocks,
1320+ next_due_block : now. saturating_add ( interval_blocks) ,
1321+ total_paid : 0 ,
1322+ status : PaymentScheduleStatus :: Active ,
1323+ } ;
1324+ self . payment_schedules
1325+ . insert ( self . schedule_count , & schedule) ;
1326+ self . loan_payment_schedule
1327+ . insert ( loan_id, & self . schedule_count ) ;
1328+ Ok ( self . schedule_count )
1329+ }
1330+
1331+ /// Get the payment schedule for a loan.
1332+ #[ ink( message) ]
1333+ pub fn get_payment_schedule_by_loan ( & self , loan_id : u64 ) -> Option < PaymentSchedule > {
1334+ let schedule_id = self . loan_payment_schedule . get ( loan_id) ?;
1335+ self . payment_schedules . get ( schedule_id)
1336+ }
1337+
11711338 #[ ink( message) ]
11721339 pub fn propose_loan_restructuring (
11731340 & mut self ,
@@ -1561,8 +1728,9 @@ mod propchain_lending {
15611728
15621729 /// Create a new loan listing on the marketplace (#304).
15631730 ///
1564- /// Any borrower can list their loan request. Lenders can then submit
1565- /// competing offers via `submit_loan_offer`.
1731+ /// Any borrower can list their loan request with an optional multi-token
1732+ /// collateral basket (#827). Lenders can then submit competing offers
1733+ /// via `submit_loan_offer`.
15661734 #[ ink( message) ]
15671735 pub fn create_loan_listing (
15681736 & mut self ,
@@ -1571,6 +1739,8 @@ mod propchain_lending {
15711739 max_rate_bps : u32 ,
15721740 term_months : u32 ,
15731741 collateral_kind : CollateralKind ,
1742+ // Multi-token collateral basket: (token_id, amount) pairs (#827).
1743+ collateral_basket : Vec < ( TokenId , u128 ) > ,
15741744 ) -> Result < u64 , LendingError > {
15751745 if requested_amount == 0 || max_rate_bps == 0 || term_months == 0 {
15761746 return Err ( LendingError :: InvalidParameters ) ;
@@ -1587,6 +1757,7 @@ mod propchain_lending {
15871757 max_rate_bps,
15881758 term_months,
15891759 collateral_kind,
1760+ collateral_basket,
15901761 status : ListingStatus :: Open ,
15911762 created_at : self . env ( ) . block_number ( ) as u64 ,
15921763 accepted_offer_id : None ,
0 commit comments