Skip to content

Commit

Permalink
feat: calculate churn of license subscriptions, not the churn of cust…
Browse files Browse the repository at this point in the history
…omers.

Organizations may have more than one license subscription and would not be counted.
  • Loading branch information
jansorg committed Jan 2, 2024
1 parent 90d788c commit 55edd46
Show file tree
Hide file tree
Showing 4 changed files with 146 additions and 75 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ class MarketplaceChurnProcessor<ID, T>(
override fun getResult(period: LicensePeriod): ChurnResult<T> {
val activeAtStart = previousPeriodItems.size

// e.g. users which were licensed end of last month, but no longer are licensed end of this month.
// For example, users which were licensed end of last month, but no longer are licensed end of this month.
// We're not counting users, which switched the license type, e.g. from "monthly" to "annual"
val churned = churnedIds()
val churnRate = when (activeAtStart) {
Expand All @@ -69,10 +69,10 @@ class MarketplaceChurnProcessor<ID, T>(
}

fun churnedIds(): Set<ID> {
val churned = hashSetFactory()
val churned = hashSetFactory().toMutableSet()
churned.addAll(previousPeriodItems)
churned.removeAll(activeItems)
churned.removeAll(activeItemsUnaccepted)
return churned.toSet()
return churned
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2023 Joachim Ansorg.
* Copyright (c) 2023-2024 Joachim Ansorg.
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

Expand All @@ -17,7 +17,10 @@ class LicenseTable(
private val showDetails: Boolean = true,
private val showFooter: Boolean = false,
private val showLicenseColumn: Boolean = true,
private val showPurchaseColumn: Boolean = true,
private val nowDate: YearMonthDay = YearMonthDay.now(),
private val supportedChurnStyling: Boolean = true,
private val showOnlyLatestLicenseInfo: Boolean = false,
private val licenseFilter: (LicenseInfo) -> Boolean = { true },
) : SimpleDataTable("Licenses", "licenses", "table-column-wide"), MarketplaceDataSink {
private val columnLicenseId = DataTableColumn("license-id", "License ID", "col-right")
Expand All @@ -43,7 +46,7 @@ class LicenseTable(
}

override val columns: List<DataTableColumn> = listOfNotNull(
columnPurchaseDate,
columnPurchaseDate.takeIf { showPurchaseColumn },
columnValidityStart,
columnValidityEnd,
columnCustomerName.takeIf { showDetails },
Expand Down Expand Up @@ -79,10 +82,22 @@ class LicenseTable(

override fun createSections(): List<DataTableSection> {
var previousPurchaseDate: YearMonthDay? = null
val shownLicenseInfos = if (showOnlyLatestLicenseInfo) mutableSetOf<LicenseId>() else null
val rows = data
.sortedWith(comparator)
.takeNullable(maxTableRows)
.map { license ->
.mapNotNull { license ->
// only display the latest renewal (or the first purchase if new)
if (shownLicenseInfos != null) {
try {
if (license.id in shownLicenseInfos) {
return@mapNotNull null
}
} finally {
shownLicenseInfos += license.id
}
}

val purchaseDate = license.sale.date
val showPurchaseDate = previousPurchaseDate != purchaseDate
previousPurchaseDate = purchaseDate
Expand All @@ -104,7 +119,7 @@ class LicenseTable(
.sorted()
.map { it.asPercentageValue(false) }
),
cssClass = if (licenseMaxValidity[license.id]!! < nowDate) "disabled" else null,
cssClass = if (supportedChurnStyling && licenseMaxValidity[license.id]!! < nowDate) "disabled" else null,
tooltips = mapOf(
columnDiscount to license.saleLineItem.discountDescriptions
.sortedBy { it.percent ?: 0.0 }
Expand All @@ -125,8 +140,8 @@ class LicenseTable(
SimpleDateTableRow(
values = mapOf(
columnAmountUSD to data.sumOf(LicenseInfo::amountUSD).withCurrency(Currency.USD),
columnLicenseId to (if (licenseCount == 0) "" else listOf(
"$activeLicenseCount active",
columnLicenseId to (if (licenseCount == 0) "" else listOfNotNull(
"$activeLicenseCount active".takeIf { supportedChurnStyling },
"$licenseCount total"
))
),
Expand Down
Loading

0 comments on commit 55edd46

Please sign in to comment.