All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.fincast.household.impl.ChPillarOne.kt Maven / Gradle / Ivy

There is a newer version: 0.0.45
Show newest version
package io.fincast.household.impl

import io.fincast.enums.FundsAllocation
import io.fincast.enums.Periodicity
import io.fincast.enums.ProductType
import io.fincast.household.Contract
import io.fincast.household.Person
import io.fincast.portfolio.Portfolio
import io.fincast.portfolio.Position
import io.fincast.portfolio.PositionCompo
import io.fincast.portfolio.ValueProviders.constValue
import io.fincast.portfolio.impl.CashflowCompo
import io.fincast.util.SimDate

data class ChPillarOne(
	override val tag: String,
	override val owner: Person,
	val annualPension44: Double? = 0.0,
	val missingContributionYears: Int? = 0,
) : Contract, HoldingBase() {

	override val productType = ProductType.CONTRACT

	override val startDate get() = owner.getActualRetirementDate() + 1

	override val endDate get() = getLastDeathDate()

	class Builder {
		private var tag: String? = null
		private var owner: Person? = null
		private var annualPension44: Double? = null
		private var missingContributionYears: Int? = null
		fun tag(tag: String) = apply { this.tag = tag }
		fun owner(owner: Person) = apply { this.owner = owner }
		fun annualPension44(annualPension44: Double?) = apply { this.annualPension44 = annualPension44 }
		fun missingContributionYears(missingContributionYears: Int?) = apply { this.missingContributionYears = missingContributionYears }
		fun build(): ChPillarOne {
			return ChPillarOne(
				tag = tag ?: throw IllegalArgumentException("tag is required"),
				owner = owner ?: throw IllegalArgumentException("owner is required"),
				annualPension44 = annualPension44 ?: 0.0,
				missingContributionYears = missingContributionYears ?: 0,
			)
		}
	}

	override fun createCompos(portfolio: Portfolio, pos: Position): List {
		return if (isCouple()) {
			createCoupleCompos(pos)
		} else {
			createSingleCompos(pos)
		}
	}

	/**
	 * There are 3 phases for a swiss first pillar pension of a single person:
	 * 1. person is working and pays contributions (this is not modeled, since it is accounted for in the net salary)
	 * 2. person is retired and receives a pension
	 * 3. person is dead and the pension stops
	 */
	private fun createSingleCompos(pos: Position): List {
		val ownerRetirementDate = owner.getActualRetirementDate()
		val ownerDeathDate = owner.deathDate
		val ownerHasRetirementPhase = ownerDeathDate == null || ownerDeathDate > ownerRetirementDate
		if (ownerHasRetirementPhase) {
			val ownerPension = getSinglePension(owner)
			return listOf(getPensionCompo(pos, ownerPension, ownerRetirementDate + 1, ownerDeathDate))
		}
		return emptyList()
	}

	/**
	 * There are 9 different combinations of phases for a swiss first pillar pension of a couple:
	 *
	 * | owner | partner | pension cashflow from owners pillar one |
	 * |-------|---------|-------|
	 * | working | working | n/a (no cashflows) |
	 * | working | retired | n/a (no cashflows) |
	 * | working | dead | n/a (but might receive widow pension from partner pillar, if owner is eligible) |
	 * | retired | working | single pension |
	 * | retired | retired | couple pension (with pro rata couple cap) |
	 * | retired | dead | owner single pension, only if higher than partner widow pension |
	 * | dead | working | widow pension, if partner is eligible |
	 * | dead | retired | widow pension, only if higher than partners pension |
	 * | dead | dead | n/a |
	 */
	private fun createCoupleCompos(pos: Position): List {

		val compos: MutableList = mutableListOf()
		val ownerRetirementDate = owner.getActualRetirementDate()
		val ownerDeathDate = owner.deathDate
		val ownerHasRetirementPhase = ownerDeathDate == null || ownerDeathDate > ownerRetirementDate
		val ownerBasePension = getBasePension(owner)
		val ownerSinglePension = getSinglePension(owner)
		//println("\n${pos.holding.tag}.createCoupleCompos.owner($ownerRetirementDate, $ownerDeathDate, $ownerHasRetirementPhase, $ownerBasePension, $ownerSinglePension))")

		val partner = getPartner()
		val partnerRetirementDate = partner.getActualRetirementDate()
		val partnerDeathDate = partner.deathDate
		val partnerEndOfWorkDate = SimDate.min(partnerRetirementDate, partnerDeathDate ?: partnerRetirementDate)
		val partnerHasRetirementPhase = partnerDeathDate == null || partnerDeathDate > partnerRetirementDate
		val partnerBasePension = getBasePension(partner)
		val partnerSinglePension = getSinglePension(partner)
		//println("${pos.holding.tag}.createCoupleCompos.partner($partnerRetirementDate, $partnerDeathDate, $partnerEndOfWorkDate, $partnerHasRetirementPhase, $partnerBasePension, $partnerSinglePension))\n")

		val firstDeathDate = if (ownerDeathDate == null) partnerDeathDate else if (partnerDeathDate == null) null else SimDate.min(ownerDeathDate, partnerDeathDate)

		// owner retirement phase (ownerRetirementDate+1 .. ownerDeathDate)
		if (ownerHasRetirementPhase) {
			// partner working [ownerRetirementDate+1 .. min(ownerDeathDate, min(partnerRetirementDate, partnerDeathDate)]
			if (partnerEndOfWorkDate > ownerRetirementDate) {
				val workEndDate = if (ownerDeathDate == null) partnerEndOfWorkDate else SimDate.min(ownerDeathDate, partnerEndOfWorkDate)
				//println("${pos.holding.tag}.createCoupleCompos.retired/working (${ownerRetirementDate + 1} .. ${workEndDate})")
				compos.add(getPensionCompo(pos, ownerSinglePension, ownerRetirementDate + 1, workEndDate))
			}
			// partner retired [partnerRetirementDate+1 .. min(ownerDeathDate, partnerDeathDate)]
			if (firstDeathDate == null || (firstDeathDate > partnerRetirementDate && firstDeathDate > ownerRetirementDate)) {
				val retirementStartDate = SimDate.max(partnerRetirementDate, ownerRetirementDate) + 1
				var pension = ownerBasePension
				val couplePension = pension + partnerBasePension
				if (couplePension > MaxCouplePension) {
					pension *= MaxCouplePension / couplePension
				}
				//println("${pos.holding.tag}.createCoupleCompos.retired/retired (${retirementStartDate} .. ${firstDeathDate})")
				compos.add(getPensionCompo(pos, pension, retirementStartDate, firstDeathDate))
			}
			// partner dead (partnerDeathDate+1 .. ownerDeathDate)
			if (partnerDeathDate != null && (ownerDeathDate == null || partnerDeathDate + 1 < ownerDeathDate)) {
				if (ownerSinglePension >= getWidowPension(partner)) {
					val deathStartDate = SimDate.max(partnerDeathDate, ownerRetirementDate) + 1
					//println("${pos.holding.tag}.createCoupleCompos.retired/dead (${deathStartDate} .. ${ownerDeathDate})")
					compos.add(getPensionCompo(pos, ownerSinglePension, deathStartDate, ownerDeathDate))
				}
			}
		}

		// owner death phase
		if (ownerDeathDate != null && (partnerDeathDate == null || ownerDeathDate + 1 < partnerDeathDate)) {
			val widowPension = getWidowPension()
			// partner working (ownerDeathDate+1 .. partnerEndOfWorkDate)
			if (ownerDeathDate + 1 < partnerEndOfWorkDate) { // TODO: check eligibility
				//println("${pos.holding.tag}.createCoupleCompos.dead/working (${ownerDeathDate + 1} .. ${partnerEndOfWorkDate})")
				compos.add(getPensionCompo(pos, widowPension, ownerDeathDate + 1, partnerEndOfWorkDate))
			}
			// partner retired (partnerEndOfWorkDate+1 .. partnerDeathDate)
			if (partnerHasRetirementPhase) {
				if (widowPension > partnerSinglePension) {
					//println("${pos.holding.tag}.createCoupleCompos.dead/retired (${partnerEndOfWorkDate + 1} .. ${partnerDeathDate})")
					compos.add(getPensionCompo(pos, widowPension, partnerEndOfWorkDate + 1, partnerDeathDate))
				}
			}
		}

		return compos
	}

	private fun getPensionCompo(pos: Position, pension: Double, startDate: SimDate?, endDate: SimDate?): PositionCompo {
		val monthlyPension = pension / SimDate.MONTHS_PER_YEAR
		return CashflowCompo(
			position = pos,
			tag = "pension",
			fundsAllocation = FundsAllocation.DISBURSE,
			amount = constValue(monthlyPension),
			sign = 1,
			startDate = startDate,
			endDate = endDate,
			periodicity = Periodicity.MONTHLY,
		)
	}

	private fun getBasePension(person: Person): Double {
		val pillarOne = getPillarOne(person) ?: return 0.0
		return pillarOne.getBasePension()
	}

	private fun getSinglePension(person: Person): Double {
		val pillarOne = getPillarOne(person) ?: return 0.0
		return pillarOne.getSinglePension()
	}

	private fun getWidowPension(person: Person): Double {
		val pillarOne = getPillarOne(person) ?: return 0.0
		return pillarOne.getWidowPension()
	}

	private fun getPillarOne(person: Person): ChPillarOne? {
		return household.holdings.filterIsInstance().firstOrNull { it.owner == person }
	}

	private fun getSinglePension(): Double {
		var pension = getBasePension()
		if (pension > MaxSinglePension) {
			pension = MaxSinglePension
		}
		return pension
	}

	private fun getWidowPension(): Double {
		var pension = 0.8 * getBasePension()
		if (pension > MaxSinglePension) {
			pension = MaxSinglePension
		}
		return pension
	}

	private fun getBasePension(): Double {
		val annualPension = this.annualPension44 ?: return 0.0
		var pension = annualPension
		val retirementAge = owner.getActualRetirementAge()
		val stdRetirementAge = owner.getStandardRetirementAge()
		if (retirementAge < stdRetirementAge) {
			val ageDiff = stdRetirementAge - retirementAge
			pension *= (1 - ageDiff * ReductionPerAdvanceYear / 100)
		} else if (retirementAge > stdRetirementAge) {
			val ageDiff = retirementAge - stdRetirementAge
			pension *= (1 + ageDiff * IncreasePerDelayedYear / 100)
		}
		if (null != missingContributionYears) {
			pension *= (1 - missingContributionYears * ReductionPerMissingYear / 100)
		}
		return pension
	}

	private fun getPartner(): Person {
		require(isCouple()) { "only valid for couples" }
		return if (owner == household.partner1) household.partner2!! else household.partner1
	}

	private fun getLastDeathDate(): SimDate? {
		val deathDate = owner.deathDate ?: return null
		val partnerDeathDate = household.partner2?.deathDate ?: return deathDate
		return SimDate.max(deathDate, partnerDeathDate)
	}

	private fun isCouple(): Boolean {
		return household.partner2 != null
	}

	companion object {
		const val MaxSinglePension = 12.0 * 2450
		const val MaxCouplePension = 1.5 * MaxSinglePension
		const val ReductionPerMissingYear = 2.27
		const val ReductionPerAdvanceYear = 6.8
		const val IncreasePerDelayedYear = 5.2
	}

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy