Skip to content

Commit

Permalink
refactor to use java.money
Browse files Browse the repository at this point in the history
Unfortunately, it not much better and even FastMoney is still a SlowMoney
  • Loading branch information
jansorg committed Jul 12, 2024
1 parent 6a7fafe commit 166d9f8
Show file tree
Hide file tree
Showing 55 changed files with 941 additions and 1,177 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ jte-classes/
.kotlin/sessions
.env
export-data/
.resourceCache/
2 changes: 1 addition & 1 deletion VERSION.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.20.0-display-currency1
0.20.0-display-currency3
9 changes: 8 additions & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ allprojects {
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.1")

// https://mvnrepository.com/artifact/org.jetbrains.kotlinx/kotlinx-coroutines-core
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.0")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.1")

// https://mvnrepository.com/artifact/it.unimi.dsi/fastutil-core
implementation("it.unimi.dsi:fastutil-core:8.5.13")
Expand All @@ -64,6 +64,13 @@ allprojects {
// https://github.com/ajalt/clikt
implementation("com.github.ajalt.clikt:clikt:4.4.0")

// https://mvnrepository.com/artifact/org.javamoney.moneta/moneta-core
implementation("org.javamoney.moneta:moneta-core:1.4.4")
// https://mvnrepository.com/artifact/org.javamoney.moneta/moneta-convert
implementation("org.javamoney.moneta:moneta-convert:1.4.4")
// https://mvnrepository.com/artifact/org.javamoney.moneta/moneta-convert-ecb
implementation("org.javamoney.moneta:moneta-convert-ecb:1.4.4")

// https://mvnrepository.com/artifact/org.junit.jupiter/junit-jupiter-api
testImplementation("org.junit.jupiter:junit-jupiter-api:5.10.3")
testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.10.3")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,6 @@ class CachingMarketplaceClient(
return loadHistoricPluginData(plugin, "salesInfo", cachedSalesInfo, delegate::salesInfo)
}


override suspend fun compatibleProducts(plugin: PluginId): List<JetBrainsProductId> {
return loadCached("compatibleProducts.$plugin") {
delegate.compatibleProducts(plugin)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,26 +6,26 @@
package dev.ja.marketplace.client

import kotlinx.datetime.TimeZone
import java.math.BigDecimal
import javax.money.MonetaryAmount

val MarketplaceTimeZone = TimeZone.of("Europe/Berlin")

object Marketplace {
private val FeeChangeTimestamp = YearMonthDay(2020, 7, 1)
private val lowFeeFactor = BigDecimal("0.05")
private val regularFeeFactor = BigDecimal("0.15")

val Birthday = YearMonthDay(2019, 6, 25)

fun feeAmount(date: YearMonthDay, amount: Amount): Amount {
fun feeAmount(date: YearMonthDay, amount: MonetaryAmount): MonetaryAmount {
return when {
date < FeeChangeTimestamp -> amount * 0.05.toBigDecimal()
else -> amount * 0.15.toBigDecimal()
date < FeeChangeTimestamp -> amount * lowFeeFactor
else -> amount * regularFeeFactor
}
}

fun paidAmount(date: YearMonthDay, amount: Amount): Amount {
fun paidAmount(date: YearMonthDay, amount: MonetaryAmount): MonetaryAmount {
return amount - feeAmount(date, amount)
}

fun paidAmount(date: YearMonthDay, amount: AmountWithCurrency): AmountWithCurrency {
return amount - feeAmount(date, amount.amount)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,24 +5,25 @@

package dev.ja.marketplace.client

import kotlinx.datetime.*
import kotlinx.datetime.TimeZone
import kotlinx.datetime.toJavaZoneId
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import java.time.LocalDate
import java.time.YearMonth
import java.time.temporal.ChronoUnit
import java.time.temporal.TemporalAdjusters
import java.time.temporal.WeekFields
import java.util.*
import java.util.concurrent.ConcurrentHashMap

@Serializable(with = YearMonthDateSerializer::class)
data class YearMonthDay(
val year: Int,
val month: Int,
val day: Int,
val timezone: TimeZone = MarketplaceTimeZone
) : Comparable<YearMonthDay> {
private val instant = LocalDate(year, month, day).atStartOfDayIn(timezone)
data class YearMonthDay private constructor(private val instant: LocalDate) : Comparable<YearMonthDay> {
constructor(year: Int, month: Int, day: Int) : this(LocalDate.of(year, month, day))

val year: Int = instant.year
val month: Int = instant.monthValue
val day: Int = instant.dayOfMonth

val asIsoString: String by lazy {
String.format("%04d-%02d-%02d", year, month, day)
Expand All @@ -32,41 +33,37 @@ data class YearMonthDay(
return YearMonthDayRange(this, end)
}

fun rangeTo(end: Instant): YearMonthDayRange {
return YearMonthDayRange(this, of(end, timezone))
}

override fun toString(): String {
return asIsoString
}

val sortValue: Long
get() {
return instant.epochSeconds
return instant.toEpochDay()
}

fun add(years: Int, months: Int, days: Int): YearMonthDay {
if (years == 0 && months == 0 && days == 0) {
return this
}

return of(
instant.plus(years, DateTimeUnit.YEAR, timezone)
.plus(months, DateTimeUnit.MONTH, timezone)
.plus(days, DateTimeUnit.DAY, timezone)
)
return of(instant.plusYears(years.toLong()).plusMonths(months.toLong()).plusDays(days.toLong()))
}

override operator fun compareTo(other: YearMonthDay): Int {
return instant.compareTo(other.instant)
}

fun daysUntil(date: YearMonthDay): Int {
return instant.daysUntil(date.instant, timezone)
fun daysUntil(date: YearMonthDay): Long {
return instant.until(date.instant, ChronoUnit.DAYS)
}

fun monthsUntil(date: YearMonthDay): Long {
return instant.until(date.instant, ChronoUnit.MONTHS)
}

fun monthsUntil(date: YearMonthDay): Int {
return instant.monthsUntil(date.instant, timezone)
fun toLocalDate(): LocalDate {
return instant
}

companion object {
Expand All @@ -79,26 +76,20 @@ data class YearMonthDay(
}

fun now(): YearMonthDay {
return of(Clock.System.now())
return of(LocalDate.now())
}

fun of(date: java.time.LocalDate, timezone: TimeZone = MarketplaceTimeZone): YearMonthDay {
val timezoneDate = date.atStartOfDay(timezone.toJavaZoneId())
return YearMonthDay(timezoneDate.year, timezoneDate.monthValue, timezoneDate.dayOfMonth)
}

fun of(date: Instant, timezone: TimeZone = MarketplaceTimeZone): YearMonthDay {
fun of(date: LocalDate): YearMonthDay {
return instantCache.computeIfAbsent(date) {
val timezoneDate = date.toLocalDateTime(timezone)
YearMonthDay(timezoneDate.year, timezoneDate.monthNumber, timezoneDate.dayOfMonth)
YearMonthDay(it)
}
}

fun lastOfMonth(year: Int, month: Int, timezone: TimeZone = MarketplaceTimeZone): YearMonthDay {
return of(YearMonth.of(year, month).atEndOfMonth(), timezone)
fun lastOfMonth(year: Int, month: Int): YearMonthDay {
return of(YearMonth.of(year, month).atEndOfMonth())
}

private val instantCache = ConcurrentHashMap<Instant, YearMonthDay>()
private val instantCache = ConcurrentHashMap<LocalDate, YearMonthDay>()
}
}

Expand Down Expand Up @@ -187,18 +178,14 @@ data class YearMonthDayRange(
return YearMonthDayRange(start, end.add(years, months, days))
}

fun countDays(): Int {
return start.daysUntil(end) + 1
}

private fun asIsoStringRange(): String {
return "${start.asIsoString} - ${end.asIsoString}"
}

companion object {
fun currentWeek(timezone: TimeZone = MarketplaceTimeZone): YearMonthDayRange {
val firstDayOfWeek = WeekFields.of(Locale.getDefault()).firstDayOfWeek
val firstOfWeek = java.time.LocalDate.now(timezone.toJavaZoneId()).with(TemporalAdjusters.previousOrSame(firstDayOfWeek))
val firstOfWeek = LocalDate.now(timezone.toJavaZoneId()).with(TemporalAdjusters.previousOrSame(firstDayOfWeek))
val lastOfWeek = firstOfWeek.plusDays(6)
return YearMonthDayRange(YearMonthDay.of(firstOfWeek), YearMonthDay.of(lastOfWeek))
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/*
* Copyright (c) 2024 Joachim Ansorg.
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

package dev.ja.marketplace.client

import org.javamoney.moneta.FastMoney
import java.math.BigDecimal
import java.math.RoundingMode
import javax.money.MonetaryAmount

operator fun MonetaryAmount.plus(other: MonetaryAmount): MonetaryAmount {
return this.add(other)
}

operator fun MonetaryAmount.minus(other: MonetaryAmount): MonetaryAmount {
return this.subtract(other)
}

operator fun MonetaryAmount.times(other: Number): MonetaryAmount {
require(this is FastMoney)
val result = (other as? BigDecimal ?: BigDecimal(other.toString()))
.multiply(this.number.numberValue(BigDecimal::class.java))
.setScale(this.scale, RoundingMode.HALF_UP)
return FastMoney.of(result, this.currency)
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,23 @@

package dev.ja.marketplace.client

import dev.ja.marketplace.services.Currency
import kotlinx.datetime.Instant
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.KSerializer
import kotlinx.serialization.builtins.IntArraySerializer
import kotlinx.serialization.builtins.ListSerializer
import kotlinx.serialization.builtins.nullable
import kotlinx.serialization.builtins.serializer
import kotlinx.serialization.descriptors.PrimitiveKind
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.descriptors.*
import kotlinx.serialization.encoding.CompositeDecoder
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import kotlinx.serialization.encoding.decodeStructure
import kotlinx.serialization.json.*
import org.javamoney.moneta.FastMoney
import org.javamoney.moneta.Money
import java.math.BigDecimal
import javax.money.MonetaryAmount

@OptIn(ExperimentalSerializationApi::class)
object YearMonthDateSerializer : KSerializer<YearMonthDay> {
Expand Down Expand Up @@ -48,28 +51,28 @@ object YearMonthDateSerializer : KSerializer<YearMonthDay> {
)
}

object AmountSerializer : KSerializer<Amount> {
override fun deserialize(decoder: Decoder): Amount {
return decoder.decodeString().toBigDecimal()
object BigDecimalSerializer : KSerializer<BigDecimal> {
override fun deserialize(decoder: Decoder): BigDecimal {
return decoder.decodeDouble().toBigDecimal()
}

override fun serialize(encoder: Encoder, value: Amount) {
encoder.encodeString(value.toPlainString())
override fun serialize(encoder: Encoder, value: BigDecimal) {
encoder.encodeDouble(value.toDouble())
}

override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("Amount", PrimitiveKind.STRING)
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("amount", PrimitiveKind.DOUBLE)
}

object CurrencySerializer : KSerializer<Currency> {
override fun deserialize(decoder: Decoder): Currency {
return MarketplaceCurrencies.of(decoder.decodeString())
object MonetaryAmountUsdSerializer : KSerializer<MonetaryAmount> {
override fun deserialize(decoder: Decoder): MonetaryAmount {
return FastMoney.of(decoder.decodeDouble(), "USD")
}

override fun serialize(encoder: Encoder, value: Currency) {
encoder.encodeString(value.isoCode)
override fun serialize(encoder: Encoder, value: MonetaryAmount) {
encoder.encodeDouble(value.number.doubleValueExact())
}

override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("Currency", PrimitiveKind.STRING)
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("amount", PrimitiveKind.DOUBLE)
}

object CDateSerializer : KSerializer<Instant> {
Expand All @@ -96,3 +99,75 @@ class NullableStringSerializer : KSerializer<String?> {
return String.serializer().nullable.serialize(encoder, value)
}
}


object PluginSaleSerializer : KSerializer<PluginSale> {
override val descriptor: SerialDescriptor = buildClassSerialDescriptor("pluginSale") {
element<String>("ref")
element<YearMonthDay>("date")
element<Double>("amount")
element<String>("currency")
element<Double>("amountUSD")
element<LicensePeriod>("period")
element<CustomerInfo>("customer")
element<ResellerInfo?>("reseller")
element<List<JsonPluginSaleItem>>("lineItems")
}

@OptIn(ExperimentalSerializationApi::class)
override fun deserialize(decoder: Decoder): PluginSale {
return decoder.decodeStructure(descriptor) {
var ref: String? = null
var date: YearMonthDay? = null
var amountValue: Double? = null
var currency: String? = null
var amountValueUSD: Double? = null
var period: LicensePeriod? = null
var customer: CustomerInfo? = null
var reseller: ResellerInfo? = null
var lineItems: List<JsonPluginSaleItem>? = null

while (true) {
when (val index = decodeElementIndex(descriptor)) {
0 -> ref = decodeStringElement(descriptor, index)
1 -> date = decodeSerializableElement(descriptor, index, YearMonthDay.serializer())
2 -> amountValue = decodeDoubleElement(descriptor, index)
3 -> currency = decodeStringElement(descriptor, index)
4 -> amountValueUSD = decodeDoubleElement(descriptor, index)
5 -> period = decodeSerializableElement(descriptor, index, LicensePeriod.serializer())
6 -> customer = decodeNullableSerializableElement(descriptor, index, CustomerInfo.serializer())
7 -> reseller = decodeNullableSerializableElement(descriptor, index, ResellerInfo.serializer())
8 -> lineItems = decodeNullableSerializableElement(descriptor, index, ListSerializer(JsonPluginSaleItem.serializer()))
CompositeDecoder.DECODE_DONE -> break
else -> error("Unexpected index: $index")
}
}
require(ref != null && date != null && amountValue != null && amountValueUSD != null && period != null && customer != null)
require(lineItems != null)

PluginSale(
ref,
date,
FastMoney.of(amountValue, currency),
FastMoney.of(amountValueUSD, "USD"),
period,
customer,
reseller,
lineItems.map {
PluginSaleItem(
it.type,
it.licenseIds,
it.subscriptionDates,
FastMoney.of(it.amount, currency),
FastMoney.of(it.amountUSD, "USD"),
it.discountDescriptions
)
}
)
}
}

override fun serialize(encoder: Encoder, value: PluginSale) {
TODO("not yet implemented")
}
}
Loading

0 comments on commit 166d9f8

Please sign in to comment.