From 62b2458afe8d3b302b1819f7bc8e302cfb0b784f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 22 Feb 2026 13:02:09 +0000 Subject: [PATCH 1/2] Initial plan From 99b2deda6c925827915bde60caf5e321acb1819e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 22 Feb 2026 13:15:31 +0000 Subject: [PATCH 2/2] feat: overhaul kite repository as pure Kotlin Multiplatform library wrapping Mite REST API Co-authored-by: Bombo <224092+Bombo@users.noreply.github.com> --- README.md | 96 ++++++++++ build.gradle.kts | 3 +- gradle.properties | 6 +- gradle/libs.versions.toml | 22 ++- gradle/wrapper/gradle-wrapper.properties | 2 +- kite/build.gradle.kts | 40 ++++ .../commonMain/kotlin/dev/kite/MiteClient.kt | 43 +++++ .../commonMain/kotlin/dev/kite/MiteConfig.kt | 6 + .../kotlin/dev/kite/api/CustomersApi.kt | 55 ++++++ .../kotlin/dev/kite/api/ProjectsApi.kt | 56 ++++++ .../kotlin/dev/kite/api/ServicesApi.kt | 56 ++++++ .../kotlin/dev/kite/api/TimeEntriesApi.kt | 84 +++++++++ .../kotlin/dev/kite/api/UsersApi.kt | 19 ++ .../kotlin/dev/kite/dsl/TimeEntryBody.kt | 13 ++ .../kotlin/dev/kite/dsl/TimeEntryFilter.kt | 19 ++ .../kotlin/dev/kite/model/Customer.kt | 19 ++ .../kotlin/dev/kite/model/Project.kt | 21 +++ .../kotlin/dev/kite/model/Service.kt | 20 ++ .../kotlin/dev/kite/model/TimeEntry.kt | 30 +++ .../commonMain/kotlin/dev/kite/model/User.kt | 19 ++ .../kotlin/dev/kite/MiteClientTest.kt | 176 ++++++++++++++++++ .../kotlin/dev/kite/TimeEntryFilterTest.kt | 70 +++++++ settings.gradle.kts | 4 +- shared/build.gradle.kts | 44 ----- .../kotlin/dev/boris/kite/models/Account.kt | 20 -- .../kotlin/dev/boris/kite/models/TimeEntry.kt | 45 ----- .../kotlin/dev/boris/kite/models/User.kt | 23 --- .../dev/boris/kite/models/enums/GroupBy.kt | 12 -- .../dev/boris/kite/models/enums/OrderBy.kt | 6 - .../dev/boris/kite/models/enums/SortBy.kt | 12 -- .../models/params/GetTimeEntriesParams.kt | 25 --- .../kotlin/dev/boris/kite/models/Account.kt | 46 ----- 32 files changed, 858 insertions(+), 254 deletions(-) create mode 100644 README.md create mode 100644 kite/build.gradle.kts create mode 100644 kite/src/commonMain/kotlin/dev/kite/MiteClient.kt create mode 100644 kite/src/commonMain/kotlin/dev/kite/MiteConfig.kt create mode 100644 kite/src/commonMain/kotlin/dev/kite/api/CustomersApi.kt create mode 100644 kite/src/commonMain/kotlin/dev/kite/api/ProjectsApi.kt create mode 100644 kite/src/commonMain/kotlin/dev/kite/api/ServicesApi.kt create mode 100644 kite/src/commonMain/kotlin/dev/kite/api/TimeEntriesApi.kt create mode 100644 kite/src/commonMain/kotlin/dev/kite/api/UsersApi.kt create mode 100644 kite/src/commonMain/kotlin/dev/kite/dsl/TimeEntryBody.kt create mode 100644 kite/src/commonMain/kotlin/dev/kite/dsl/TimeEntryFilter.kt create mode 100644 kite/src/commonMain/kotlin/dev/kite/model/Customer.kt create mode 100644 kite/src/commonMain/kotlin/dev/kite/model/Project.kt create mode 100644 kite/src/commonMain/kotlin/dev/kite/model/Service.kt create mode 100644 kite/src/commonMain/kotlin/dev/kite/model/TimeEntry.kt create mode 100644 kite/src/commonMain/kotlin/dev/kite/model/User.kt create mode 100644 kite/src/commonTest/kotlin/dev/kite/MiteClientTest.kt create mode 100644 kite/src/commonTest/kotlin/dev/kite/TimeEntryFilterTest.kt delete mode 100644 shared/build.gradle.kts delete mode 100644 shared/src/commonMain/kotlin/dev/boris/kite/models/Account.kt delete mode 100644 shared/src/commonMain/kotlin/dev/boris/kite/models/TimeEntry.kt delete mode 100644 shared/src/commonMain/kotlin/dev/boris/kite/models/User.kt delete mode 100644 shared/src/commonMain/kotlin/dev/boris/kite/models/enums/GroupBy.kt delete mode 100644 shared/src/commonMain/kotlin/dev/boris/kite/models/enums/OrderBy.kt delete mode 100644 shared/src/commonMain/kotlin/dev/boris/kite/models/enums/SortBy.kt delete mode 100644 shared/src/commonMain/kotlin/dev/boris/kite/models/params/GetTimeEntriesParams.kt delete mode 100644 shared/src/commonTest/kotlin/dev/boris/kite/models/Account.kt diff --git a/README.md b/README.md new file mode 100644 index 0000000..d3a5532 --- /dev/null +++ b/README.md @@ -0,0 +1,96 @@ +# kite + +A pure **Kotlin Multiplatform** library that wraps the [Mite REST API](https://mite.de/en/api/), giving you idiomatic Kotlin access to time entries, projects, customers, services, and users. + +## Platforms + +`jvm` · `iosX64` · `iosArm64` · `iosSimulatorArm64` · `macosX64` · `macosArm64` · `js (IR)` · `wasmJs` + +## Installation + +```kotlin +// build.gradle.kts +dependencies { + implementation("dev.kite:kite:0.1.0") +} +``` + +## Quick Start + +Use the convenience factory function — it creates a sensible default `HttpClient` for you: + +```kotlin +import dev.kite.MiteClient +import dev.kite.MiteConfig +import kotlinx.datetime.LocalDate + +val mite = MiteClient( + MiteConfig( + accountName = "yourcompany", + apiKey = "your-api-key", + ) +) + +// List time entries with a DSL filter +val entries = mite.timeEntries.list { + from = LocalDate(2026, 1, 1) + to = LocalDate(2026, 1, 31) + projectId = 42 +} + +// Create a time entry +val newEntry = mite.timeEntries.create { + date = LocalDate(2026, 1, 15) + minutes = 90 + note = "Implementing new feature" + projectId = 42 + serviceId = 3 +} + +// Fetch a single entry +val entry = mite.timeEntries.get(id = 12345L) + +// List projects / customers / services / users +val projects = mite.projects.list() +val customers = mite.customers.list() +val services = mite.services.list() +val users = mite.users.list() +val me = mite.users.myself() +``` + +## Advanced: Bring Your Own HttpClient + +Inject a custom Ktor `HttpClient` directly for full control over timeouts, logging, engines, etc.: + +```kotlin +import io.ktor.client.HttpClient +import io.ktor.client.engine.cio.CIO +import io.ktor.client.plugins.contentnegotiation.ContentNegotiation +import io.ktor.client.plugins.defaultrequest.defaultRequest +import io.ktor.client.plugins.logging.LogLevel +import io.ktor.client.plugins.logging.Logging +import io.ktor.http.HttpHeaders +import io.ktor.serialization.kotlinx.json.json +import kotlinx.serialization.json.Json + +val customClient = HttpClient(CIO) { + install(ContentNegotiation) { + json(Json { ignoreUnknownKeys = true }) + } + install(Logging) { + level = LogLevel.HEADERS + } + defaultRequest { + url("https://yourcompany.mite.de/") + headers.append("X-MiteApiKey", "your-api-key") + headers.append(HttpHeaders.Accept, "application/json") + headers.append(HttpHeaders.UserAgent, "my-app/1.0") + } +} + +val mite = MiteClient(customClient) +``` + +## License + +[MIT](LICENSE) diff --git a/build.gradle.kts b/build.gradle.kts index 12d80f7..58b1d22 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,5 +1,4 @@ plugins { - //trick: for the same plugin versions in all sub-modules - alias(libs.plugins.androidLibrary).apply(false) alias(libs.plugins.kotlinMultiplatform).apply(false) + alias(libs.plugins.kotlinSerialization).apply(false) } diff --git a/gradle.properties b/gradle.properties index 7f53ad4..e2482ca 100644 --- a/gradle.properties +++ b/gradle.properties @@ -4,8 +4,4 @@ org.gradle.caching=true org.gradle.configuration-cache=true #Kotlin -kotlin.code.style=official - -#Android -android.useAndroidX=true -android.nonTransitiveRClass=true \ No newline at end of file +kotlin.code.style=official \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 62423f7..b9b6b3f 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,19 +1,21 @@ [versions] -agp = "8.2.0" -kotlin = "1.9.21" -kotlinx-datetime = "0.5.0" -kotlinx-serialization-json = "1.6.2" +kotlin = "2.3.10" +ktor = "3.4.0" +kotlinx-serialization = "1.7.0" +kotlinx-datetime = "0.6.0" +kotlinx-coroutines = "1.9.0" [libraries] kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" } kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "kotlinx-datetime" } -kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization-json" } - +kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization" } +kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinx-coroutines" } +kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "kotlinx-coroutines" } +ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" } +ktor-client-content-negotiation = { module = "io.ktor:ktor-client-content-negotiation", version.ref = "ktor" } +ktor-serialization-kotlinx-json = { module = "io.ktor:ktor-serialization-kotlinx-json", version.ref = "ktor" } +ktor-client-mock = { module = "io.ktor:ktor-client-mock", version.ref = "ktor" } [plugins] -androidApplication = { id = "com.android.application", version.ref = "agp" } -androidLibrary = { id = "com.android.library", version.ref = "agp" } -kotlinAndroid = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } -kotlinCocoapods = { id = "org.jetbrains.kotlin.native.cocoapods", version.ref = "kotlin" } kotlinSerialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 3cb7a67..8712872 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Tue Dec 19 16:22:54 CET 2023 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.3-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/kite/build.gradle.kts b/kite/build.gradle.kts new file mode 100644 index 0000000..2076c82 --- /dev/null +++ b/kite/build.gradle.kts @@ -0,0 +1,40 @@ +plugins { + alias(libs.plugins.kotlinMultiplatform) + alias(libs.plugins.kotlinSerialization) +} + +group = "dev.kite" +version = "0.1.0" + +kotlin { + jvm() + iosX64() + iosArm64() + iosSimulatorArm64() + macosX64() + macosArm64() + js(IR) { + browser() + nodejs() + } + wasmJs { + browser() + nodejs() + } + + sourceSets { + commonMain.dependencies { + implementation(libs.kotlinx.datetime) + implementation(libs.kotlinx.serialization.json) + implementation(libs.kotlinx.coroutines.core) + implementation(libs.ktor.client.core) + implementation(libs.ktor.client.content.negotiation) + implementation(libs.ktor.serialization.kotlinx.json) + } + commonTest.dependencies { + implementation(libs.kotlin.test) + implementation(libs.ktor.client.mock) + implementation(libs.kotlinx.coroutines.test) + } + } +} diff --git a/kite/src/commonMain/kotlin/dev/kite/MiteClient.kt b/kite/src/commonMain/kotlin/dev/kite/MiteClient.kt new file mode 100644 index 0000000..fa972b3 --- /dev/null +++ b/kite/src/commonMain/kotlin/dev/kite/MiteClient.kt @@ -0,0 +1,43 @@ +package dev.kite + +import dev.kite.api.CustomersApi +import dev.kite.api.ProjectsApi +import dev.kite.api.ServicesApi +import dev.kite.api.TimeEntriesApi +import dev.kite.api.UsersApi +import io.ktor.client.HttpClient +import io.ktor.client.plugins.contentnegotiation.ContentNegotiation +import io.ktor.client.plugins.defaultrequest.defaultRequest +import io.ktor.http.HttpHeaders +import io.ktor.http.URLProtocol +import io.ktor.serialization.kotlinx.json.json +import kotlinx.serialization.json.Json + +class MiteClient(private val httpClient: HttpClient) { + val timeEntries: TimeEntriesApi = TimeEntriesApi(httpClient) + val projects: ProjectsApi = ProjectsApi(httpClient) + val customers: CustomersApi = CustomersApi(httpClient) + val services: ServicesApi = ServicesApi(httpClient) + val users: UsersApi = UsersApi(httpClient) +} + +fun MiteClient(config: MiteConfig): MiteClient { + val client = HttpClient { + install(ContentNegotiation) { + json(Json { + ignoreUnknownKeys = true + explicitNulls = false + }) + } + defaultRequest { + url { + protocol = URLProtocol.HTTPS + host = "${config.accountName}.mite.de" + } + headers.append("X-MiteApiKey", config.apiKey) + headers.append(HttpHeaders.Accept, "application/json") + headers.append(HttpHeaders.UserAgent, "kite/1.0") + } + } + return MiteClient(client) +} diff --git a/kite/src/commonMain/kotlin/dev/kite/MiteConfig.kt b/kite/src/commonMain/kotlin/dev/kite/MiteConfig.kt new file mode 100644 index 0000000..25302d3 --- /dev/null +++ b/kite/src/commonMain/kotlin/dev/kite/MiteConfig.kt @@ -0,0 +1,6 @@ +package dev.kite + +data class MiteConfig( + val accountName: String, + val apiKey: String, +) diff --git a/kite/src/commonMain/kotlin/dev/kite/api/CustomersApi.kt b/kite/src/commonMain/kotlin/dev/kite/api/CustomersApi.kt new file mode 100644 index 0000000..d74db27 --- /dev/null +++ b/kite/src/commonMain/kotlin/dev/kite/api/CustomersApi.kt @@ -0,0 +1,55 @@ +package dev.kite.api + +import dev.kite.model.Customer +import dev.kite.model.CustomerWrapper +import io.ktor.client.HttpClient +import io.ktor.client.call.body +import io.ktor.client.request.delete +import io.ktor.client.request.get +import io.ktor.client.request.parameter +import io.ktor.client.request.patch +import io.ktor.client.request.post +import io.ktor.client.request.setBody +import io.ktor.http.ContentType +import io.ktor.http.contentType +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +class CustomersApi internal constructor(private val httpClient: HttpClient) { + + suspend fun list(): List = + httpClient.get("customers.json").body>().map { it.customer } + + suspend fun listArchived(): List = + httpClient.get("customers.json") { + parameter("archived", "true") + }.body>().map { it.customer } + + suspend fun get(id: Long): Customer = + httpClient.get("customers/$id.json").body().customer + + suspend fun create(name: String, note: String? = null): Customer = + httpClient.post("customers.json") { + contentType(ContentType.Application.Json) + setBody(CustomerRequest(CustomerBodyPayload(name = name, note = note))) + }.body().customer + + suspend fun update(id: Long, name: String? = null, note: String? = null): Customer = + httpClient.patch("customers/$id.json") { + contentType(ContentType.Application.Json) + setBody(CustomerRequest(CustomerBodyPayload(name = name, note = note))) + }.body().customer + + suspend fun delete(id: Long) { + httpClient.delete("customers/$id.json") + } +} + +@Serializable +internal data class CustomerBodyPayload( + val name: String? = null, + val note: String? = null, +) + +@Serializable +internal data class CustomerRequest(@SerialName("customer") val customer: CustomerBodyPayload) diff --git a/kite/src/commonMain/kotlin/dev/kite/api/ProjectsApi.kt b/kite/src/commonMain/kotlin/dev/kite/api/ProjectsApi.kt new file mode 100644 index 0000000..0ca9fed --- /dev/null +++ b/kite/src/commonMain/kotlin/dev/kite/api/ProjectsApi.kt @@ -0,0 +1,56 @@ +package dev.kite.api + +import dev.kite.model.Project +import dev.kite.model.ProjectWrapper +import io.ktor.client.HttpClient +import io.ktor.client.call.body +import io.ktor.client.request.delete +import io.ktor.client.request.get +import io.ktor.client.request.parameter +import io.ktor.client.request.patch +import io.ktor.client.request.post +import io.ktor.client.request.setBody +import io.ktor.http.ContentType +import io.ktor.http.contentType +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +class ProjectsApi internal constructor(private val httpClient: HttpClient) { + + suspend fun list(): List = + httpClient.get("projects.json").body>().map { it.project } + + suspend fun listArchived(): List = + httpClient.get("projects.json") { + parameter("archived", "true") + }.body>().map { it.project } + + suspend fun get(id: Long): Project = + httpClient.get("projects/$id.json").body().project + + suspend fun create(name: String, customerId: Long? = null, note: String? = null): Project = + httpClient.post("projects.json") { + contentType(ContentType.Application.Json) + setBody(ProjectRequest(ProjectBodyPayload(name = name, customerId = customerId, note = note))) + }.body().project + + suspend fun update(id: Long, name: String? = null, customerId: Long? = null, note: String? = null): Project = + httpClient.patch("projects/$id.json") { + contentType(ContentType.Application.Json) + setBody(ProjectRequest(ProjectBodyPayload(name = name, customerId = customerId, note = note))) + }.body().project + + suspend fun delete(id: Long) { + httpClient.delete("projects/$id.json") + } +} + +@Serializable +internal data class ProjectBodyPayload( + val name: String? = null, + @SerialName("customer_id") val customerId: Long? = null, + val note: String? = null, +) + +@Serializable +internal data class ProjectRequest(@SerialName("project") val project: ProjectBodyPayload) diff --git a/kite/src/commonMain/kotlin/dev/kite/api/ServicesApi.kt b/kite/src/commonMain/kotlin/dev/kite/api/ServicesApi.kt new file mode 100644 index 0000000..f7336f8 --- /dev/null +++ b/kite/src/commonMain/kotlin/dev/kite/api/ServicesApi.kt @@ -0,0 +1,56 @@ +package dev.kite.api + +import dev.kite.model.Service +import dev.kite.model.ServiceWrapper +import io.ktor.client.HttpClient +import io.ktor.client.call.body +import io.ktor.client.request.delete +import io.ktor.client.request.get +import io.ktor.client.request.parameter +import io.ktor.client.request.patch +import io.ktor.client.request.post +import io.ktor.client.request.setBody +import io.ktor.http.ContentType +import io.ktor.http.contentType +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +class ServicesApi internal constructor(private val httpClient: HttpClient) { + + suspend fun list(): List = + httpClient.get("services.json").body>().map { it.service } + + suspend fun listArchived(): List = + httpClient.get("services.json") { + parameter("archived", "true") + }.body>().map { it.service } + + suspend fun get(id: Long): Service = + httpClient.get("services/$id.json").body().service + + suspend fun create(name: String, note: String? = null, hourlyRate: Int? = null): Service = + httpClient.post("services.json") { + contentType(ContentType.Application.Json) + setBody(ServiceRequest(ServiceBodyPayload(name = name, note = note, hourlyRate = hourlyRate))) + }.body().service + + suspend fun update(id: Long, name: String? = null, note: String? = null, hourlyRate: Int? = null): Service = + httpClient.patch("services/$id.json") { + contentType(ContentType.Application.Json) + setBody(ServiceRequest(ServiceBodyPayload(name = name, note = note, hourlyRate = hourlyRate))) + }.body().service + + suspend fun delete(id: Long) { + httpClient.delete("services/$id.json") + } +} + +@Serializable +internal data class ServiceBodyPayload( + val name: String? = null, + val note: String? = null, + @SerialName("hourly_rate") val hourlyRate: Int? = null, +) + +@Serializable +internal data class ServiceRequest(@SerialName("service") val service: ServiceBodyPayload) diff --git a/kite/src/commonMain/kotlin/dev/kite/api/TimeEntriesApi.kt b/kite/src/commonMain/kotlin/dev/kite/api/TimeEntriesApi.kt new file mode 100644 index 0000000..aeb1bd7 --- /dev/null +++ b/kite/src/commonMain/kotlin/dev/kite/api/TimeEntriesApi.kt @@ -0,0 +1,84 @@ +package dev.kite.api + +import dev.kite.dsl.TimeEntryBody +import dev.kite.dsl.TimeEntryFilter +import dev.kite.model.TimeEntry +import dev.kite.model.TimeEntryWrapper +import io.ktor.client.HttpClient +import io.ktor.client.call.body +import io.ktor.client.request.delete +import io.ktor.client.request.get +import io.ktor.client.request.parameter +import io.ktor.client.request.patch +import io.ktor.client.request.post +import io.ktor.client.request.setBody +import io.ktor.http.ContentType +import io.ktor.http.contentType +import kotlinx.datetime.LocalDate +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +class TimeEntriesApi internal constructor(private val httpClient: HttpClient) { + + suspend fun list(filter: TimeEntryFilter.() -> Unit = {}): List { + val f = TimeEntryFilter().apply(filter) + return httpClient.get("time_entries.json") { + f.userId?.let { parameter("user_id", it) } + f.customerId?.let { parameter("customer_id", it) } + f.projectId?.let { parameter("project_id", it) } + f.serviceId?.let { parameter("service_id", it) } + f.from?.let { parameter("from", it.toString()) } + f.to?.let { parameter("to", it.toString()) } + f.billable?.let { parameter("billable", it) } + f.locked?.let { parameter("locked", it) } + f.groupBy?.let { parameter("group_by", it.name.lowercase()) } + }.body>().map { it.timeEntry } + } + + suspend fun get(id: Long): TimeEntry = + httpClient.get("time_entries/$id.json").body().timeEntry + + suspend fun create(body: TimeEntryBody.() -> Unit): TimeEntry { + val b = TimeEntryBody().apply(body) + return httpClient.post("time_entries.json") { + contentType(ContentType.Application.Json) + setBody(TimeEntryRequest(b.toPayload())) + }.body().timeEntry + } + + suspend fun update(id: Long, body: TimeEntryBody.() -> Unit): TimeEntry { + val b = TimeEntryBody().apply(body) + return httpClient.patch("time_entries/$id.json") { + contentType(ContentType.Application.Json) + setBody(TimeEntryRequest(b.toPayload())) + }.body().timeEntry + } + + suspend fun delete(id: Long) { + httpClient.delete("time_entries/$id.json") + } +} + +private fun TimeEntryBody.toPayload() = TimeEntryBodyPayload( + date = date, + minutes = minutes, + note = note, + userId = userId, + projectId = projectId, + serviceId = serviceId, + locked = locked, +) + +@Serializable +internal data class TimeEntryBodyPayload( + @SerialName("date_at") val date: LocalDate? = null, + val minutes: Int? = null, + val note: String? = null, + @SerialName("user_id") val userId: Long? = null, + @SerialName("project_id") val projectId: Long? = null, + @SerialName("service_id") val serviceId: Long? = null, + val locked: Boolean? = null, +) + +@Serializable +internal data class TimeEntryRequest(@SerialName("time_entry") val timeEntry: TimeEntryBodyPayload) diff --git a/kite/src/commonMain/kotlin/dev/kite/api/UsersApi.kt b/kite/src/commonMain/kotlin/dev/kite/api/UsersApi.kt new file mode 100644 index 0000000..f009b3a --- /dev/null +++ b/kite/src/commonMain/kotlin/dev/kite/api/UsersApi.kt @@ -0,0 +1,19 @@ +package dev.kite.api + +import dev.kite.model.User +import dev.kite.model.UserWrapper +import io.ktor.client.HttpClient +import io.ktor.client.call.body +import io.ktor.client.request.get + +class UsersApi internal constructor(private val httpClient: HttpClient) { + + suspend fun list(): List = + httpClient.get("users.json").body>().map { it.user } + + suspend fun get(id: Long): User = + httpClient.get("users/$id.json").body().user + + suspend fun myself(): User = + httpClient.get("myself.json").body().user +} diff --git a/kite/src/commonMain/kotlin/dev/kite/dsl/TimeEntryBody.kt b/kite/src/commonMain/kotlin/dev/kite/dsl/TimeEntryBody.kt new file mode 100644 index 0000000..c14a7c9 --- /dev/null +++ b/kite/src/commonMain/kotlin/dev/kite/dsl/TimeEntryBody.kt @@ -0,0 +1,13 @@ +package dev.kite.dsl + +import kotlinx.datetime.LocalDate + +class TimeEntryBody { + var date: LocalDate? = null + var minutes: Int? = null + var note: String? = null + var userId: Long? = null + var projectId: Long? = null + var serviceId: Long? = null + var locked: Boolean? = null +} diff --git a/kite/src/commonMain/kotlin/dev/kite/dsl/TimeEntryFilter.kt b/kite/src/commonMain/kotlin/dev/kite/dsl/TimeEntryFilter.kt new file mode 100644 index 0000000..d74d78c --- /dev/null +++ b/kite/src/commonMain/kotlin/dev/kite/dsl/TimeEntryFilter.kt @@ -0,0 +1,19 @@ +package dev.kite.dsl + +import kotlinx.datetime.LocalDate + +class TimeEntryFilter { + var userId: Long? = null + var customerId: Long? = null + var projectId: Long? = null + var serviceId: Long? = null + var from: LocalDate? = null + var to: LocalDate? = null + var billable: Boolean? = null + var locked: Boolean? = null + var groupBy: GroupBy? = null + + enum class GroupBy { + USER, CUSTOMER, PROJECT, SERVICE, DAY, WEEK, MONTH, YEAR + } +} diff --git a/kite/src/commonMain/kotlin/dev/kite/model/Customer.kt b/kite/src/commonMain/kotlin/dev/kite/model/Customer.kt new file mode 100644 index 0000000..1d3d495 --- /dev/null +++ b/kite/src/commonMain/kotlin/dev/kite/model/Customer.kt @@ -0,0 +1,19 @@ +package dev.kite.model + +import kotlinx.datetime.Instant +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class Customer( + val id: Long, + val name: String, + val note: String?, + val archived: Boolean, + @SerialName("hourly_rate") val hourlyRate: Int?, + @SerialName("created_at") val createdAt: Instant, + @SerialName("updated_at") val updatedAt: Instant, +) + +@Serializable +internal data class CustomerWrapper(@SerialName("customer") val customer: Customer) diff --git a/kite/src/commonMain/kotlin/dev/kite/model/Project.kt b/kite/src/commonMain/kotlin/dev/kite/model/Project.kt new file mode 100644 index 0000000..9697877 --- /dev/null +++ b/kite/src/commonMain/kotlin/dev/kite/model/Project.kt @@ -0,0 +1,21 @@ +package dev.kite.model + +import kotlinx.datetime.Instant +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class Project( + val id: Long, + val name: String, + val note: String?, + val archived: Boolean, + @SerialName("customer_id") val customerId: Long?, + @SerialName("customer_name") val customerName: String?, + @SerialName("hourly_rate") val hourlyRate: Int?, + @SerialName("created_at") val createdAt: Instant, + @SerialName("updated_at") val updatedAt: Instant, +) + +@Serializable +internal data class ProjectWrapper(@SerialName("project") val project: Project) diff --git a/kite/src/commonMain/kotlin/dev/kite/model/Service.kt b/kite/src/commonMain/kotlin/dev/kite/model/Service.kt new file mode 100644 index 0000000..77329bb --- /dev/null +++ b/kite/src/commonMain/kotlin/dev/kite/model/Service.kt @@ -0,0 +1,20 @@ +package dev.kite.model + +import kotlinx.datetime.Instant +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class Service( + val id: Long, + val name: String, + val note: String?, + val archived: Boolean, + val billable: Boolean, + @SerialName("hourly_rate") val hourlyRate: Int?, + @SerialName("created_at") val createdAt: Instant, + @SerialName("updated_at") val updatedAt: Instant, +) + +@Serializable +internal data class ServiceWrapper(@SerialName("service") val service: Service) diff --git a/kite/src/commonMain/kotlin/dev/kite/model/TimeEntry.kt b/kite/src/commonMain/kotlin/dev/kite/model/TimeEntry.kt new file mode 100644 index 0000000..f6df38a --- /dev/null +++ b/kite/src/commonMain/kotlin/dev/kite/model/TimeEntry.kt @@ -0,0 +1,30 @@ +package dev.kite.model + +import kotlinx.datetime.Instant +import kotlinx.datetime.LocalDate +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class TimeEntry( + val id: Long, + @SerialName("date_at") val date: LocalDate, + val minutes: Int, + @SerialName("revenue") val revenue: Double?, + val locked: Boolean, + val billable: Boolean, + val note: String, + @SerialName("user_id") val userId: Long, + @SerialName("user_name") val userName: String, + @SerialName("project_id") val projectId: Long?, + @SerialName("project_name") val projectName: String?, + @SerialName("customer_id") val customerId: Long?, + @SerialName("customer_name") val customerName: String?, + @SerialName("service_id") val serviceId: Long?, + @SerialName("service_name") val serviceName: String?, + @SerialName("created_at") val createdAt: Instant, + @SerialName("updated_at") val updatedAt: Instant, +) + +@Serializable +internal data class TimeEntryWrapper(@SerialName("time_entry") val timeEntry: TimeEntry) diff --git a/kite/src/commonMain/kotlin/dev/kite/model/User.kt b/kite/src/commonMain/kotlin/dev/kite/model/User.kt new file mode 100644 index 0000000..04ee51b --- /dev/null +++ b/kite/src/commonMain/kotlin/dev/kite/model/User.kt @@ -0,0 +1,19 @@ +package dev.kite.model + +import kotlinx.datetime.Instant +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class User( + val id: Long, + val name: String, + val email: String, + val role: String, + @SerialName("archived_at") val archivedAt: Instant?, + @SerialName("created_at") val createdAt: Instant, + @SerialName("updated_at") val updatedAt: Instant, +) + +@Serializable +internal data class UserWrapper(@SerialName("user") val user: User) diff --git a/kite/src/commonTest/kotlin/dev/kite/MiteClientTest.kt b/kite/src/commonTest/kotlin/dev/kite/MiteClientTest.kt new file mode 100644 index 0000000..1bf9cbd --- /dev/null +++ b/kite/src/commonTest/kotlin/dev/kite/MiteClientTest.kt @@ -0,0 +1,176 @@ +package dev.kite + +import io.ktor.client.HttpClient +import io.ktor.client.engine.mock.MockEngine +import io.ktor.client.engine.mock.MockRequestHandleScope +import io.ktor.client.engine.mock.HttpResponseData +import io.ktor.client.engine.mock.respond +import io.ktor.client.plugins.contentnegotiation.ContentNegotiation +import io.ktor.client.request.HttpRequestData +import io.ktor.http.ContentType +import io.ktor.http.HttpHeaders +import io.ktor.http.HttpMethod +import io.ktor.http.content.OutgoingContent +import io.ktor.http.headersOf +import io.ktor.serialization.kotlinx.json.json +import kotlinx.coroutines.test.runTest +import kotlinx.serialization.json.Json +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +class MiteClientTest { + + private val sampleTimeEntryJson = """ + { + "time_entry": { + "id": 1, + "date_at": "2026-01-15", + "minutes": 90, + "revenue": 135.0, + "locked": false, + "billable": true, + "note": "Development work", + "user_id": 10, + "user_name": "Alice", + "project_id": 42, + "project_name": "My Project", + "customer_id": 5, + "customer_name": "Acme Corp", + "service_id": 3, + "service_name": "Development", + "created_at": "2026-01-15T09:00:00Z", + "updated_at": "2026-01-15T10:30:00Z" + } + } + """.trimIndent() + + private val sampleListJson = """ + [ + { + "time_entry": { + "id": 1, + "date_at": "2026-01-15", + "minutes": 90, + "revenue": 135.0, + "locked": false, + "billable": true, + "note": "Development work", + "user_id": 10, + "user_name": "Alice", + "project_id": 42, + "project_name": "My Project", + "customer_id": 5, + "customer_name": "Acme Corp", + "service_id": 3, + "service_name": "Development", + "created_at": "2026-01-15T09:00:00Z", + "updated_at": "2026-01-15T10:30:00Z" + } + } + ] + """.trimIndent() + + private fun buildClient( + handler: suspend MockRequestHandleScope.(HttpRequestData) -> HttpResponseData, + ): MiteClient { + val engine = MockEngine(handler) + val httpClient = HttpClient(engine) { + install(ContentNegotiation) { + json(Json { ignoreUnknownKeys = true; explicitNulls = false }) + } + } + return MiteClient(httpClient) + } + + @Test + fun `list deserializes response correctly`() = runTest { + val client = buildClient { _ -> + respond( + content = sampleListJson, + headers = headersOf(HttpHeaders.ContentType, ContentType.Application.Json.toString()), + ) + } + + val entries = client.timeEntries.list() + + assertEquals(1, entries.size) + val entry = entries.first() + assertEquals(1L, entry.id) + assertEquals(90, entry.minutes) + assertEquals("Development work", entry.note) + assertEquals(10L, entry.userId) + assertEquals("Alice", entry.userName) + assertEquals(42L, entry.projectId) + assertTrue(entry.billable) + } + + @Test + fun `list with projectId filter sends correct query parameter`() = runTest { + var capturedUrl: String? = null + + val client = buildClient { request -> + capturedUrl = request.url.toString() + respond( + content = "[]", + headers = headersOf(HttpHeaders.ContentType, ContentType.Application.Json.toString()), + ) + } + + client.timeEntries.list { projectId = 42 } + + assertTrue( + capturedUrl?.contains("project_id=42") == true, + "URL should contain project_id=42, was: $capturedUrl", + ) + } + + @Test + fun `create sends correct request body`() = runTest { + var capturedBody: String? = null + + val client = buildClient { request -> + capturedBody = (request.body as? OutgoingContent.ByteArrayContent) + ?.bytes() + ?.decodeToString() + respond( + content = sampleTimeEntryJson, + headers = headersOf(HttpHeaders.ContentType, ContentType.Application.Json.toString()), + ) + } + + client.timeEntries.create { + minutes = 60 + note = "Test entry" + projectId = 42 + } + + val body = capturedBody ?: "" + assertTrue(body.contains("\"minutes\":60"), "Body should contain minutes, was: $body") + assertTrue(body.contains("\"note\":\"Test entry\""), "Body should contain note, was: $body") + assertTrue(body.contains("\"project_id\":42"), "Body should contain project_id, was: $body") + } + + @Test + fun `create sends request to correct endpoint`() = runTest { + var capturedMethod: HttpMethod? = null + var capturedPath: String? = null + + val client = buildClient { request -> + capturedMethod = request.method + capturedPath = request.url.encodedPath + respond( + content = sampleTimeEntryJson, + headers = headersOf(HttpHeaders.ContentType, ContentType.Application.Json.toString()), + ) + } + + client.timeEntries.create { minutes = 30 } + + assertEquals(HttpMethod.Post, capturedMethod) + assertTrue( + capturedPath?.contains("time_entries") == true, + "Path should contain time_entries, was: $capturedPath", + ) + } +} diff --git a/kite/src/commonTest/kotlin/dev/kite/TimeEntryFilterTest.kt b/kite/src/commonTest/kotlin/dev/kite/TimeEntryFilterTest.kt new file mode 100644 index 0000000..c4a91fb --- /dev/null +++ b/kite/src/commonTest/kotlin/dev/kite/TimeEntryFilterTest.kt @@ -0,0 +1,70 @@ +package dev.kite + +import dev.kite.dsl.TimeEntryFilter +import kotlinx.datetime.LocalDate +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNull + +class TimeEntryFilterTest { + + @Test + fun `default filter has all null fields`() { + val filter = TimeEntryFilter() + assertNull(filter.userId) + assertNull(filter.customerId) + assertNull(filter.projectId) + assertNull(filter.serviceId) + assertNull(filter.from) + assertNull(filter.to) + assertNull(filter.billable) + assertNull(filter.locked) + assertNull(filter.groupBy) + } + + @Test + fun `filter builder sets fields correctly`() { + val filter = TimeEntryFilter().apply { + userId = 1L + customerId = 2L + projectId = 42L + serviceId = 7L + from = LocalDate(2026, 1, 1) + to = LocalDate(2026, 1, 31) + billable = true + locked = false + groupBy = TimeEntryFilter.GroupBy.PROJECT + } + + assertEquals(1L, filter.userId) + assertEquals(2L, filter.customerId) + assertEquals(42L, filter.projectId) + assertEquals(7L, filter.serviceId) + assertEquals(LocalDate(2026, 1, 1), filter.from) + assertEquals(LocalDate(2026, 1, 31), filter.to) + assertEquals(true, filter.billable) + assertEquals(false, filter.locked) + assertEquals(TimeEntryFilter.GroupBy.PROJECT, filter.groupBy) + } + + @Test + fun `GroupBy enum values are correct`() { + val values = TimeEntryFilter.GroupBy.entries + assertEquals(8, values.size) + assertEquals("USER", TimeEntryFilter.GroupBy.USER.name) + assertEquals("CUSTOMER", TimeEntryFilter.GroupBy.CUSTOMER.name) + assertEquals("PROJECT", TimeEntryFilter.GroupBy.PROJECT.name) + assertEquals("SERVICE", TimeEntryFilter.GroupBy.SERVICE.name) + assertEquals("DAY", TimeEntryFilter.GroupBy.DAY.name) + assertEquals("WEEK", TimeEntryFilter.GroupBy.WEEK.name) + assertEquals("MONTH", TimeEntryFilter.GroupBy.MONTH.name) + assertEquals("YEAR", TimeEntryFilter.GroupBy.YEAR.name) + } + + @Test + fun `GroupBy name lowercased matches Mite API values`() { + assertEquals("user", TimeEntryFilter.GroupBy.USER.name.lowercase()) + assertEquals("project", TimeEntryFilter.GroupBy.PROJECT.name.lowercase()) + assertEquals("day", TimeEntryFilter.GroupBy.DAY.name.lowercase()) + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index 69aeec3..c1a8fc6 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,7 +1,6 @@ enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") pluginManagement { repositories { - google() gradlePluginPortal() mavenCentral() } @@ -9,10 +8,9 @@ pluginManagement { dependencyResolutionManagement { repositories { - google() mavenCentral() } } rootProject.name = "kite" -include(":shared") \ No newline at end of file +include(":kite") \ No newline at end of file diff --git a/shared/build.gradle.kts b/shared/build.gradle.kts deleted file mode 100644 index 0800391..0000000 --- a/shared/build.gradle.kts +++ /dev/null @@ -1,44 +0,0 @@ -plugins { - alias(libs.plugins.kotlinMultiplatform) - alias(libs.plugins.androidLibrary) - alias(libs.plugins.kotlinSerialization) -} - -kotlin { - androidTarget { - compilations.all { - kotlinOptions { - jvmTarget = "1.8" - } - } - } - - listOf( - iosX64(), - iosArm64(), - iosSimulatorArm64() - ).forEach { - it.binaries.framework { - baseName = "shared" - isStatic = true - } - } - - sourceSets { - commonMain.dependencies { - implementation(libs.kotlinx.datetime) - implementation(libs.kotlinx.serialization.json) - } - commonTest.dependencies { - implementation(libs.kotlin.test) - } - } -} - -android { - namespace = "dev.boris.kite" - compileSdk = 34 - defaultConfig { - minSdk = 24 - } -} diff --git a/shared/src/commonMain/kotlin/dev/boris/kite/models/Account.kt b/shared/src/commonMain/kotlin/dev/boris/kite/models/Account.kt deleted file mode 100644 index 60965f8..0000000 --- a/shared/src/commonMain/kotlin/dev/boris/kite/models/Account.kt +++ /dev/null @@ -1,20 +0,0 @@ -package dev.boris.kite.models - -import kotlinx.datetime.Instant -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable - -@Serializable -data class Account( - val id: Int, - val name: String, - val title: String, - val currency: String, - @SerialName("created_at") - val createdAt: Instant, - @SerialName("updated_at") - val updatedAt: Instant, -) - -@Serializable -internal data class AccountWrapper(val account: Account) \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/dev/boris/kite/models/TimeEntry.kt b/shared/src/commonMain/kotlin/dev/boris/kite/models/TimeEntry.kt deleted file mode 100644 index 4991936..0000000 --- a/shared/src/commonMain/kotlin/dev/boris/kite/models/TimeEntry.kt +++ /dev/null @@ -1,45 +0,0 @@ -package dev.boris.kite.models - -import kotlinx.datetime.Instant -import kotlinx.datetime.LocalDate -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable - -@Serializable -data class TimeEntry( - val id: Int, - @SerialName("user_id") - val userId: Int, - @SerialName("user_name") - val userName: String, - @SerialName("date_at") - val dateAt: LocalDate, - @SerialName("started_time") - val startedTime: Int?, // in minutes, e.g. 540 = 9am - val minutes: Int, - val note: String, - @SerialName("customer_id") - val customerId: Int?, - @SerialName("customer_name") - val customerName: String?, - @SerialName("project_id") - val projectId: Int?, - @SerialName("project_name") - val projectName: String?, - @SerialName("service_id") - val serviceId: Int?, - @SerialName("service_name") - val serviceName: String?, - val billable: Boolean, - val revenue: Double?, - @SerialName("hourly_rate") - val hourlyRate: Int, - @SerialName("created_at") - val createdAt: Instant, - @SerialName("updated_at") - val updatedAt: Instant, - val locked: Boolean, -) - -@Serializable -internal data class TimeEntryWrapper(@SerialName("time_entry") val timeEntry: TimeEntry) \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/dev/boris/kite/models/User.kt b/shared/src/commonMain/kotlin/dev/boris/kite/models/User.kt deleted file mode 100644 index ba7e7a4..0000000 --- a/shared/src/commonMain/kotlin/dev/boris/kite/models/User.kt +++ /dev/null @@ -1,23 +0,0 @@ -package dev.boris.kite.models - -import kotlinx.datetime.Instant -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable - -@Serializable -data class User( - val id: Int, - val name: String, - val email: String, - val role: String, - val note: String, - val language: String, - @SerialName("created_at") - val createdAt: Instant, - @SerialName("updated_at") - val updatedAt: Instant, - val archived: Boolean, -) - -@Serializable -internal data class UserWrapper(val user: User) \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/dev/boris/kite/models/enums/GroupBy.kt b/shared/src/commonMain/kotlin/dev/boris/kite/models/enums/GroupBy.kt deleted file mode 100644 index a04d739..0000000 --- a/shared/src/commonMain/kotlin/dev/boris/kite/models/enums/GroupBy.kt +++ /dev/null @@ -1,12 +0,0 @@ -package dev.boris.kite.models.enums - -enum class GroupBy { - user, - customer, - project, - service, - day, - week, - month, - year, -} \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/dev/boris/kite/models/enums/OrderBy.kt b/shared/src/commonMain/kotlin/dev/boris/kite/models/enums/OrderBy.kt deleted file mode 100644 index 39f505d..0000000 --- a/shared/src/commonMain/kotlin/dev/boris/kite/models/enums/OrderBy.kt +++ /dev/null @@ -1,6 +0,0 @@ -package dev.boris.kite.models.enums - -enum class OrderBy { - asc, - desc, -} \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/dev/boris/kite/models/enums/SortBy.kt b/shared/src/commonMain/kotlin/dev/boris/kite/models/enums/SortBy.kt deleted file mode 100644 index 86cdf61..0000000 --- a/shared/src/commonMain/kotlin/dev/boris/kite/models/enums/SortBy.kt +++ /dev/null @@ -1,12 +0,0 @@ -package dev.boris.kite.models.enums - -enum class SortBy { - date, - user, - customer, - project, - service, - note, - minutes, - revenue, -} \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/dev/boris/kite/models/params/GetTimeEntriesParams.kt b/shared/src/commonMain/kotlin/dev/boris/kite/models/params/GetTimeEntriesParams.kt deleted file mode 100644 index 16d3baf..0000000 --- a/shared/src/commonMain/kotlin/dev/boris/kite/models/params/GetTimeEntriesParams.kt +++ /dev/null @@ -1,25 +0,0 @@ -package dev.boris.kite.models.params - -import dev.boris.kite.models.enums.GroupBy -import dev.boris.kite.models.enums.OrderBy -import dev.boris.kite.models.enums.SortBy -import kotlinx.datetime.LocalDate - -data class GetTimeEntriesParams( - val userIds: Array, - val customerId: Int?, - val projectId: Int?, - val serviceId: Int?, - val notes: Array?, - val at: LocalDate?, - val from: LocalDate?, - val to: LocalDate?, - val billable: Boolean?, - val locked: Boolean?, - val tracking: Boolean?, - val sort: SortBy?, - val direction: OrderBy?, - val groupBy: GroupBy?, - val limit: Int?, - val page: Int?, -) \ No newline at end of file diff --git a/shared/src/commonTest/kotlin/dev/boris/kite/models/Account.kt b/shared/src/commonTest/kotlin/dev/boris/kite/models/Account.kt deleted file mode 100644 index 76ac8d5..0000000 --- a/shared/src/commonTest/kotlin/dev/boris/kite/models/Account.kt +++ /dev/null @@ -1,46 +0,0 @@ -package dev.boris.kite.models - -import kotlinx.datetime.LocalDateTime -import kotlinx.datetime.TimeZone -import kotlinx.datetime.toInstant -import kotlinx.serialization.encodeToString -import kotlinx.serialization.json.Json -import kotlin.test.Test -import kotlin.test.assertEquals - -class AccountTest { - - companion object { - const val INPUT = """{ - "account": { - "id": 117571, - "name": "2orgu", - "title": "2OrgU", - "currency": "EUR", - "created_at": "2023-01-02T12:19:38+01:00", - "updated_at": "2023-12-21T01:00:07+01:00" - } -}""" - } - - @Test - fun shouldParseContent() { - println(AccountWrapper.serializer().descriptor) - println(Account.serializer().descriptor) - val account = Json.decodeFromString(INPUT).account - assertEquals(account.id, 117571) - } - - @Test - fun shouldSerializeContent() { - val account = Account( - 117571, - "2orgu", - "2OrgU", - "EUR", - LocalDateTime(2023, 1, 2, 12, 19, 38).toInstant(TimeZone.of("UTC+1")), - LocalDateTime(2023, 12, 21, 1, 0, 7).toInstant(TimeZone.of("UTC+1")) - ) - println(Json { prettyPrint = true }.encodeToString(AccountWrapper(account))) - } -} \ No newline at end of file