Kotlin Coding Standards
This page documents the Kotlin coding standards for Arda backend services. Apply these rules when writing or reviewing Kotlin code in any Arda repository.
General
Section titled “General”- When a function has multiple parameters of the same type, use named arguments at the call site to prevent argument-order mistakes.
- When implementing
LogEnabled by LogProvider(...), always pass the enclosing class as the argument:LogProvider(MyUniverse::class). Never copy aLogProvider(...)delegation from a neighboring file without updating the class reference.
Formatting
Section titled “Formatting”- Do not reformat existing code unless you have been explicitly asked to do so. Leave formatting to the project’s automated tools.
- For new code, follow the style defined in the
.editorconfigfile at the repository root.
Functions and Methods
Section titled “Functions and Methods”-
Any function or method that may fail must return
Result<T>instead of throwing an exception. -
Single exit point. Functions and methods must have a single
returnstatement (or a single expression body). Do not scatter multiplereturnorreturn@labelstatements throughout a function body. Instead, usewhenexpressions,Result.flatMapchains, or localvalbindings to funnel all paths to one exit. Multiple early returns make control flow hard to follow and easy to break during refactoring.// WRONG — multiple return pointssuspend fun validate(url: String): Result<URL> {val parsed = try { URL(url) } catch (e: Exception) {return Result.failure(AppError.ArgumentValidation("url", e.message))}if (parsed.host != expectedHost) {return Result.failure(AppError.ArgumentValidation("url", "wrong host"))}return Result.success(parsed)}// CORRECT — single expression using flatMap chainsuspend fun validate(url: String): Result<URL> =runCatching { URL(url) }.mapError { AppError.ArgumentValidation("url", it.message) }.flatMap { parsed ->when {parsed.host != expectedHost -> Result.failure(AppError.ArgumentValidation("url", "wrong host"))else -> Result.success(parsed)}} -
Prefer
whenexpressions overifstatements wherever possible. -
Use
Result.map,Result.flatMap, and similar operators to chain operations that may fail. Model the logic around Success and Failure channels. -
Do not use
getOrThroworgetOrNullto extract a value from aResult. UsemaporflatMapand place the logic inside the lambda. -
Result<T>for all fallible operations. Any operation that can fail — including URL construction, parsing, or other seemingly simple operations — must returnResult<T>. Consistency matters: if there is any code path that throws, wrap it inrunCatchingand returnResult. -
Fail-fast ordering in chains. When chaining multiple operations, order them cheapest-first. Validate inputs (pure logic, no I/O) before accessing
ApplicationContext(coroutine context) or making service calls (network). -
flatInApplicationContextfor coroutine context access. UseApplicationContext.Key.flatInApplicationContext { ctx -> ... }to accessApplicationContextfrom a coroutine. Do not useApplicationContext.current().flatMap { ... }—flatInApplicationContextis the idiomatic common-module pattern. -
Data classes before their producers. Define data classes (result types, value objects) above the class that produces or consumes them. The reader encounters the type before the code that uses it.
Tail-Recursive Retry Loops
Section titled “Tail-Recursive Retry Loops”When a service must attempt an operation up to N times, express the loop as a tail-recursive private suspend function — not as a mutable variable threaded through a while loop.
Anti-pattern — mutable state in a loop:
// WRONG — mutable state, multiple exit points, hard to auditvar result: SendOutcome? = nullvar attempts = 0while (attempts < maxAttempts) { result = trySend(request) attempts++ if (result is SendOutcome.Sent || result is SendOutcome.Rejected) break}return result ?: SendOutcome.Exhausted(maxAttempts)The loop’s terminal condition requires reading the loop body, the if guard, and the post-loop fallback together. Refactoring the terminal cases is error-prone.
Canonical replacement — tail-recursive function:
// CORRECT — single exit per branch, max depth bounded by maxAttempts// (canonical shape from EmailSender.attemptSend in// cards.arda.operations.shopaccess.email.service)private suspend fun attemptSend( request: SendRequest, attempt: Int = 1,): SendOutcome = when { attempt > maxAttempts -> SendOutcome.Exhausted(maxAttempts) else -> when (val outcome = trySend(request)) { is SendOutcome.Sent, is SendOutcome.Rejected -> outcome // terminal is SendOutcome.TransientFailure -> attemptSend(request, attempt + 1) }}Each branch either returns a terminal result or recurses — the depth is bounded by maxAttempts (default 3). The JVM stack is not at risk for small bounds.
Note: The recursion cannot be annotated tailrec because Kotlin’s tailrec does not compose with suspend. This is a known language limitation; the depth bound remains the safety guarantee.
File Size and Cohesion
Section titled “File Size and Cohesion”Keep source files small and cohesive. The size limits below are a signal, not a hard gate: a file a few lines over is not a defect, but a file well past the limit almost always hides more than one responsibility.
- Production files: ≤ 300 lines.
- Test files: ≤ 500 lines.
When a file grows past its limit, split it along its internal seams — extract
collaborators, factory or extension functions, or sub-services that each own a
cohesive slice of the behavior and the state/dependencies it needs. Split by
concern, not by arbitrary line count: two halves that share the same
collaborators and reading order belong together; the goal is cohesion, not
merely a smaller number. The orders module is a worked example of decomposing
a large service into cohesive collaborators wired at the composition root.
When a single class legitimately exceeds the limit and cannot be cleanly split, record the reason in review and seek sign-off rather than silently shipping it.
Result Handling
Section titled “Result Handling”In production code, never unwrap a Result with getOrThrow or getOrNull.
Always use map or flatMap to work with the value while keeping it in the
Result context. If this cannot be done reasonably for a given case, escalate
to the team for guidance.
One normalizeFailure() per monadic chain
Section titled “One normalizeFailure() per monadic chain”A single .normalizeFailure() at the tail of a flatMap chain is
sufficient — it converts any generic exception surfacing anywhere in the chain
into an AppError. Do not sprinkle .normalizeFailure() after each step. Add a
second one only when the body genuinely branches on error type and a later
branch must normalize independently of the tail.
// CORRECT — one normalizeFailure() covers every failure in the chainbusinessAffiliateService.findByNameAndRole(name = supplierName, role = VENDOR, asOf = asOf) .flatMap { existingBa -> when (existingBa) { null -> businessAffiliateService.add(/* ... */).flatMap { /* createBusinessRole */ } else -> businessAffiliateService.businessRolesFor(existingBa.payload.eId, asOf).flatMap { /* link */ } } }.normalizeFailure()Guard, don’t checkNotNull, inside a Result chain
Section titled “Guard, don’t checkNotNull, inside a Result chain”When an invariant guarantees two nullable fields are co-present (e.g., a smart
constructor that makes a non-null eId imply a non-null affiliateEId), funnel
them to non-null locals with a single when guard that returns a
Result.failure for the impossible-but-typed case. The smart-cast keeps the
rest of the body in the Result channel. Never reach for checkNotNull, !!,
or getOrThrow to discharge such an invariant in production resolution code —
those throw, escaping the Result channel.
val roleEId = supplierRef.eIdval affiliateEId = supplierRef.affiliateEIdreturn when { roleEId == null || affiliateEId == null -> Result.failure( AppError.IncompatibleState( "resolveWithExistingRef requires a linked SupplierReference (eId and affiliateEId)" ) ) // roleEId and affiliateEId are smart-cast non-null below; the body stays in Result. else -> businessAffiliateService.detailsFor(affiliateEId, asOf).flatMap { details -> /* ... */ }}.unitify() over .map { }
Section titled “.unitify() over .map { }”To convert a Result<T> whose value you no longer need into a Result<Unit>,
use .unitify() (cards.arda.common.lib.lang) rather than .map { }. It states
the intent — discard the value, keep the success/failure channel — without an
empty lambda.
// WRONGbusinessAffiliateService.updateName(affiliateEId, supplierName, asOf.effective).map { }
// CORRECTbusinessAffiliateService.updateName(affiliateEId, supplierName, asOf.effective).unitify()Related architecture patterns
Section titled “Related architecture patterns”A backend session writing reference-resolution or cross-entity persistence code should also read these pattern pages:
- Cross-Child Queries — tenant-scoped queries that resolve child entities across many parents in a single pass (no N+1).
- Parent-Child Persistence — entities scoped to a parent (lines on a header, roles on a company).
- Data Authority Module Pattern — the four-layer module structure that owns these services and universes.
Documented exception:
fillPayloadbridging. The bitemporal framework’sfillPayload(row): EntityPayloadis declared to throw on corrupt rows (it predates theResultconvention). Smart-constructors insideComponent.build()returnResult; bridge them with.getOrThrow()only insidefillPayload. This is the only place in module code wheregetOrThrow()is acceptable, and only because the framework contract demands it. See Persistent Components § “ThefillPayloadboundary”.
ResultExt Combinators
Section titled “ResultExt Combinators”The ResultExt.kt file in common-module provides combinators that eliminate boilerplate when/if chains. Use them before reaching for manual unwrapping.
resultNotNull — collapse nullable success to typed failure
Section titled “resultNotNull — collapse nullable success to typed failure”Use when a Result<T?> null payload means “not found, and the caller requires a value.” It converts a Result.success(null) into a Result.failure(err) while leaving non-null successes and failures unchanged.
// Before — manual null check inside flatMapfun getServerToken(configEId: EntityId): Result<PostmarkAccountToken> = configRepo.findByEId(configEId) .flatMap { cfg -> if (cfg == null) Result.failure(AppError.NotFound("emailConfiguration")) else Result.success(cfg.token) }
// After — resultNotNull collapses the nullable stepfun getServerToken(configEId: EntityId): Result<PostmarkAccountToken> = configRepo.findByEId(configEId) .resultNotNull(AppError.NotFound("emailConfiguration")) .map { it.token }flatMap(r1, r2, transform) — combine two independent Results
Section titled “flatMap(r1, r2, transform) — combine two independent Results”Use when two separate Result computations are both needed before a downstream step. If either fails the combinator short-circuits; if both succeed, transform receives both values.
flatMap(resolveConfig(tenantId), resolveSender(senderId)) { cfg, sender -> EmailJob(cfg, sender, payload)}collectAll — fail-fast aggregation over a collection
Section titled “collectAll — fail-fast aggregation over a collection”Use when mapping a collection of inputs to Result<T> and needing Result<List<T>>. It stops at the first failure and returns it; all items must succeed for the list to be returned.
// Before — manual fold with early exitfun loadAll(paths: List<Path>): Result<List<Template>> { val results = mutableListOf<Template>() for (p in paths) { val r = loadTemplate(p) if (r.isFailure) return r.map { emptyList() } // awkward cast results += r.getOrThrow() } return Result.success(results)}
// After — collectAll is a single expressionfun loadAll(paths: List<Path>): Result<List<Template>> = paths.map { loadTemplate(it) }.collectAll()Choosing collectAll vs a collect-all-errors approach. collectAll is fail-fast: it stops and returns the first failure. When the goal is to surface all validation errors at once (so the user can fix everything in one pass), wrap each item’s failure in AppError.Composite instead — accumulate all AppError values, then return AppError.Composite(message, causes) when the list is non-empty. Reserve collectAll for pipeline steps where the first failure is the only actionable signal (e.g., loading required files on startup).
Value-class smart constructors
Section titled “Value-class smart constructors”Value classes that validate their input must use the private constructor +
companion operator fun invoke pattern, not init { require(...) }. The
factory returns Result<T> so callers see validation failures as values, not
exceptions.
// WRONG — throws on invalid input; callers must runCatching every construction@JvmInlinevalue class LocalPart(val value: String) { init { require(value.matches(localPartRegex)) { "Invalid local part: $value" } }}
// CORRECT — private constructor, companion operator invoke returning Result@JvmInlinevalue class LocalPart private constructor(val value: String) { companion object { operator fun invoke(value: String): Result<LocalPart> = when { value.matches(localPartRegex) -> Result.success(LocalPart(value)) else -> Result.failure( AppError.ArgumentValidation("localPart", "Invalid local part: $value") ) } }}
// Call sitesLocalPart("noreply") .flatMap { local -> EmailAddress(local, domain) } .flatMap { address -> sendTo(address) }Conventions:
-
Factory name:
operator fun invoke, nevercreate. Call sites readLocalPart("noreply"), identical to the deprecated constructor call. -
Companion-only construction. The private constructor and the companion share the value class;
kotlinx.serializationstill generates a serializer via the companion-adjacent declaration, so@Serializablevalue classes work without changes. -
Constant baselines for known-valid defaults. Expose a
companion objectconstant (RecentHealth.OK) rather than calling the factory and.getOrThrow()at every reference. The constant is built once at class load with verified inputs.@JvmInlinevalue class RecentHealth private constructor(val consecutiveFailureCounter: Int) {companion object {val OK = RecentHealth(consecutiveFailureCounter = 0)operator fun invoke(consecutiveFailureCounter: Int): Result<RecentHealth> = when {consecutiveFailureCounter < 0 -> Result.failure(AppError.ArgumentValidation("consecutiveFailureCounter", "must be >= 0"))else -> Result.success(RecentHealth(consecutiveFailureCounter))}}} -
Test fixtures. When constructing a value class with a literal valid input inside a test, the trailing
.getOrThrow()is acceptable because the input is known-valid at write time. Wrap result-returning factories with.flatMapin production paths.
Exception and Error Handling
Section titled “Exception and Error Handling”- All exceptions must be a subclass of
cards.arda.common.lib.lang.errors.AppError, using the most specific subclass available. - Choose the exception subclass by origin:
AppError.Invocation— errors attributable to inputs to the method.AppError.ArgumentValidation(argumentName, validationMessage)— invalid input parameter.AppError.ContextValidation(msg)— invalid request context (e.g., missing tenant scope).
AppError.Internal— errors arising from internal logic or implementation-resource issues.AppError.ExternalService(msg, code, description)— failure in an external service call (e.g., AWS SDK error).AppError.IncompatibleState(message)— internal state inconsistency.AppError.Infrastructure(message)— infrastructure/configuration error.AppError.NotFound(resourceName)— requested resource does not exist.
AppError.Composite(message, causes)— wraps multiple errors together, as in input validation, collection operations, or batch operations. UseThrowable.normalizeToAppError()to convert generic exceptions toAppError.
- Handle errors by returning a
kotlin.Resultwherever possible. - When integrating external libraries or calling methods that cannot return
kotlin.Result(e.g., constructors,toString,equals):- Run them inside a
runCatchingblock. - Convert the exception to an
AppErrorusingmapError, either manually or via the provided extension functionsThrowable.normalizeToAppError()orResult<T>.normalizeFailure().
- Run them inside a
- Collect all errors in validation functions. When a function validates
multiple conditions, collect all failures into a list and return
AppError.Compositewhen there are multiple errors, a singleAppErrorwhen there is one, orResult.successwhen there are none. Do not fail-fast on the first error — the caller (and ultimately the user) needs the complete picture to fix all issues at once. - Preserve the original
Throwableascause. When constructing a wrappingAppErrorfrom a caught exception, passcause = erron variants that accept it (IncompatibleState,Infrastructure,InternalService).AppError.ExternalServicedoes not acceptcausein the current common-module version (tracked in PDEV-767); omitcausefor that variant and add a// TODO(PDEV-767)comment at the construction site as audit trail so it is easy to find and update once PDEV-767 ships.
Closeable Resources
Section titled “Closeable Resources”Prefer Kotlin’s Closeable.use { ... } (and the AutoCloseable variant) over
an explicit try { ... } finally { resource.close() } for any object whose
lifetime ends at the bottom of the current scope.
use is strictly safer than the equivalent try/finally:
- If both the body and
close()throw,usepropagates the body’s exception and attaches the close-time exception asThrowable.addSuppressed(...). The naïvetry/finallyform throws whichever exception fires last, silently dropping the body’s exception — usually the one the caller actually needs. - It is shorter, removes the need for a separate variable, and makes the resource’s lifetime obvious at a glance.
// WRONG — body exception is lost if close() also throwsval migrationDs = HikariDataSource(hc)try { DbMigration(fwCfg, migrationDs).migrate().onFailure { throw it }} finally { migrationDs.close()}
// CORRECTHikariDataSource(hc).use { migrationDs -> DbMigration(fwCfg, migrationDs).migrate().onFailure { throw it }}When use does not fit
Section titled “When use does not fit”use is for resources whose lifetime ends with the current block. It does
not fit when the resource’s lifetime is conditional — for example, a pool
that must outlive the function on the happy path (because it backs a value you
return to the caller) but must be closed on the failure path. In that case
keep an explicit try { ... } catch { resource.close(); throw }:
val runtimePool = HikariDataSource(hc).also { allCreatedPools.add(it) }val db = Database.connect(runtimePool)try { HikariDataSource(hc).use { migrationDs -> DbMigration(fwCfg, migrationDs).migrate().onFailure { throw it } }} catch (t: Throwable) { // runtimePool's lifetime is conditional — close it only on the failure // path, since `db` (and therefore the pool) is returned to the caller on // success. allCreatedPools.remove(runtimePool) runtimePool.close() throw t}return dbAnti-pattern: surrogate close on the wrong type
Section titled “Anti-pattern: surrogate close on the wrong type”Calling dataSource.connection.close() on a javax.sql.DataSource returns
one connection to the pool — it does not close the pool. If the intent
is to release the pool, hold a typed handle to the pool implementation (e.g.,
HikariDataSource) and wrap it in use { ... }.
Non-Null Assertions (!!)
Section titled “Non-Null Assertions (!!)”Never use !! (the non-null assertion operator). It bypasses the compiler’s
null-safety guarantees and throws KotlinNullPointerException at runtime —
exactly the class of error that Kotlin’s type system is designed to prevent.
Instead, use one of the following patterns that give the compiler enough information to smart-cast the value:
// WRONG — runtime crash if nullval name = user.name!!
// CORRECT — when expression with smart castval name = when (val n = user.name) { null -> return Result.failure(AppError.Infrastructure("name is required")) else -> n // compiler knows n is non-null here}
// CORRECT — if/else with smart castval name = user.name ?: return Result.failure(AppError.Infrastructure("name is required"))
// CORRECT — require() for preconditionsval name = requireNotNull(user.name) { "name must not be null" }The when or if pattern is preferred because it keeps failure handling in the
Result channel. Use requireNotNull only for true programming errors where a
null value indicates a bug, not a user or configuration error.
Do not use nullable types to represent missing capabilities. If a class parameter is only needed by some callers, do not make it nullable to “disable” a feature. Instead, construct the class with full capabilities and let callers use only the methods they need. For example, a service that supports both PUT and POST should always be constructed with both capabilities — a caller that only needs PUT simply does not call the POST method.
Dependency Injection
Section titled “Dependency Injection”- Inject dependencies, do not construct them internally. Classes should receive their dependencies (services, clients, configuration) as constructor parameters. The wiring point (Module.kt) creates all dependencies and passes them in. Classes should not create their own service instances, SDK clients, or other infrastructure objects.
- When a class needs access to an internal component of a dependency (e.g., an
S3AsyncClientowned by anS3AssetService), the dependency should expose it as a public property rather than having the consumer construct a separate instance.
Null Value Semantics
Section titled “Null Value Semantics”-
Null should only be returned from methods that explicitly search or retrieve information. In these cases the method returns
Result<T?>:- A
nullpayload means the value was not found. - A
failuremeans a different condition occurred (e.g., a required parent record was itself missing).
Example: retrieving a child record from a parent-child relationship — if the parent is null, return
failure. If the parent is found but the child is not, returnsuccesswith a null payload. - A
-
In all other cases (e.g., mutating system state), if the target of the operation is null or not found, return
Result.failure(...).
Database Mappings
Section titled “Database Mappings”Arda uses Exposed for database access.
-
Column names must be specified in lowercase snake_case.
-
Use column types defined under
EntityTable. When those types are insufficient, use native Exposed column types. -
When using
Filter.Eqand similar, use<TBL>.<COLUMN>.namefor the locator parameter instead of hardcoded strings, to eliminate the risk of mismatched column names. -
JSON columns with custom serializers: The reified
json<T>(name, format)overload callsserializer<T>()at runtime, which ignores@Serializable(with=...)and@Contextualannotations on type arguments. WhenTcontains a non-@Serializableclass (e.g.,java.net.URI), use the three-argument overload with an explicitKSerializer:// WRONG — fails at runtime: serializer<Map<String, URI>>() cannot find URI serializerval sites = tbl.json<Map<String, URI>>(name, JsonConfig.standardJson)// CORRECT — explicit serializerval sites = tbl.json<Map<String, URI>>(name, JsonConfig.standardJson, MapSerializer(String.serializer(), URISerializer))EntityTablealso providesstandardJson<T>(name), which resolves contextual serializers fromJsonConfig.standardJson.serializersModule. Prefer it overjson<T>(name, JsonConfig.standardJson)when contextual serializers are sufficient. -
Unchecked casts to
ChildTable: When castingEntityTabletoChildTable(required due to invariant generics onExposedLocatorTranslator), always guard withcheck()before the cast:override val translator by lazy {check(persistence.bt is ChildTable) {"MyUniverse requires a ChildTable, got ${persistence.bt::class}"}queryConfig.bindToTable(persistence.bt as ChildTable)} -
QueryCompilerreuse: Never constructQueryCompiler(table)inline inside service methods. Define a module-levellazy valin the persistence package, or expose aninternalaccessor on the universe:// Module-level lazy val (preferred for child universes)internal val myQCompiler by lazy {QueryCompiler(MY_TABLE, myQueryConfig.bindToTable(MY_TABLE))}// Internal accessor on parent universeinternal val queryCompiler get() = qCompiler
Persistence shape: Universe vs. plain table
Section titled “Persistence shape: Universe vs. plain table”Choose how to persist an entity by whether it has a lifecycle:
- No lifecycle — standalone, immutable records that are never updated (an audit / event log). A plain Exposed table is fine; there are no “versions of one entity” to interpret, so the Universe machinery buys nothing. Deduplication, if needed, is a unique-index concern.
- Has a lifecycle — created, mutated, soft-deleted, or otherwise evolving over time
(multiple rows are versions of one logical entity). Default to a bitemporal
ScopedUniverse. Do not hand-roll soft-delete, version history, or tenant scoping on a plain table — theAbstractScopedUniverse/ScopedTable/ScopedRecord/Persistencestack already provides them, tested and composable:retired-flag soft-delete, a row per change (full audit history), scoped metadata (tenant_id, author, created / updated timestamps), structural tenant isolation viaScopedUniversalCondition, andcreate/read/findOne(Filter, asOf)/list(Query, asOf)/update/deleteoperations that honor the “latest non-retired version” semantics. - Special cases (e.g. a Draft Store, or another non-standard store) — consult the user / operator before deviating from the Universe default; do not pick a bespoke persistence shape unilaterally.
A business-key uniqueness constraint on a bitemporal table (e.g. “at most one active row per
(tenant, key)”) is enforced in the application layer — check for a live entry before
inserting — not by a DB partial-unique index: a row-version write inserts the new version
before retiring the old one and would otherwise trip a non-deferrable unique constraint.
Naming pitfall: a
ScopedTablecolumn property namedsource(or any name that collides with an ExposedColumnSetmember) fails to compile — “‘source’ hides member of supertype ‘ColumnSet’ and needs an ‘override’ modifier.” Name the Kotlin property non-collidingly and keep the DB column name via the string argument, e.g.enumerated<…>("source").
URL and URI Types
Section titled “URL and URI Types”- Prefer
java.net.URIoverjava.net.URLfor URL-typed fields.URL.equals()andURL.hashCode()trigger DNS resolution, makingURLunsafe as a map key, in collections, or inside data classes. - Use
URI(...).toURL()instead ofURL(String). TheURL(String)constructor is deprecated in modern Java. Construct viaURIfirst, then convert:URI("https://example.com/path").toURL(). - Both
URISerializerandURLSerializerare registered as contextual serializers inJsonConfig.standardJson. UseURISerializerexplicitly when needed (see Database Mappings rule 4 above). - The wire format for
URIis a plain JSON string. Changing a field fromStringtoURIrequires no database migration.
Type Migration Checklist
Section titled “Type Migration Checklist”When changing a field type in a domain value object (e.g., String to URI),
update all three layers:
- Domain: sealed interface property and
Valuedata class property, including any default values. - Persistence: component column definition,
fill(),getComponentValue(), andsetComponentValue(). - Tests: update test data construction and assertions.
Verify that the wire format (JSON serialization) is unchanged if no database migration is planned.
Composite Build Safety
Section titled “Composite Build Safety”settings.gradle.kts files containing includeBuild("../common-module") or
similar local composite build overrides must never be committed or pushed.
Always exclude or stash settings.gradle.kts before staging commits.
Unit Tests
Section titled “Unit Tests”Refer to the unit-tests skill in the
workspace for detailed unit
testing guidance.
Import Statements
Section titled “Import Statements”- Prefer explicit imports over wildcard imports.
- Remove all unused imports before finishing a task.
Copyright: (c) Arda Systems 2025-2026, All rights reserved
Copyright: © Arda Systems 2025-2026, All rights reserved