From 236bbcb9dd52666e33c5b0bdbc14cf75a5994c2a Mon Sep 17 00:00:00 2001 From: dadachi Date: Thu, 2 Apr 2026 18:01:11 +0900 Subject: [PATCH 1/3] Implement CodedError system with NATA-XXXX error codes Add platform-specific error codes (NATA prefix) for user-facing error messages. Error types in common/errors/ package. - Add CodedError interface, AppError, NfcError, SubscriptionError - Make ApiException implement CodedError (NATA-2001, NATA-2002) - Move error types to common/errors/ package - Add Throwable.codedDescription extension - Update all ViewModels to use codedDescription - Add unit tests for all error types Co-Authored-By: Claude Opus 4.6 (1M context) --- .../MainActivityViewModel.kt | 5 +- .../common/errors/ApiException.kt | 21 +++++ .../common/errors/AppError.kt | 12 +++ .../common/errors/CodedError.kt | 11 +++ .../common/errors/NfcError.kt | 12 +++ .../common/errors/SubscriptionError.kt | 17 ++++ .../data/login/LoginRepositoryImpl.kt | 2 +- .../network/ApiException.kt | 23 ----- .../network/ApiResponseExtensions.kt | 1 + .../ui/app_root/AcceptPrivacyViewModel.kt | 5 +- .../ui/app_root/AcceptTermsViewModel.kt | 5 +- .../ui/app_root/ForgotPasswordViewModel.kt | 5 +- ...ResendConfirmationInstructionsViewModel.kt | 5 +- .../SignInEmailAndPasswordViewModel.kt | 5 +- .../ui/app_root/SignUpViewModel.kt | 5 +- .../ui/scan/DoScanViewModel.kt | 9 +- .../ui/scan/ScanViewModel.kt | 17 ++-- .../ui/settings/PasswordEditViewModel.kt | 5 +- .../ui/settings/SettingsViewModel.kt | 9 +- .../ui/settings/ShopkeeperEditViewModel.kt | 13 +-- .../ui/shop_detail/ShopDetailViewModel.kt | 13 +-- .../NumberTagsWebpageListViewModel.kt | 5 +- .../ShopBasicSettingsViewModel.kt | 9 +- .../ui/shop_settings/ShopSettingsViewModel.kt | 13 +-- .../item_tag_detail/ItemTagDetailViewModel.kt | 9 +- .../item_tag_detail/ItemTagEditViewModel.kt | 9 +- .../item_tag_list/ItemTagCreateViewModel.kt | 9 +- .../item_tag_list/ItemTagListViewModel.kt | 9 +- .../ui/shops/ShopCreateViewModel.kt | 5 +- .../ui/shops/ShopListViewModel.kt | 5 +- .../common/errors/CodedErrorTest.kt | 89 +++++++++++++++++++ .../network/ApiResponseExtensionsTest.kt | 1 + 32 files changed, 263 insertions(+), 100 deletions(-) create mode 100644 app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/common/errors/ApiException.kt create mode 100644 app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/common/errors/AppError.kt create mode 100644 app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/common/errors/CodedError.kt create mode 100644 app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/common/errors/NfcError.kt create mode 100644 app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/common/errors/SubscriptionError.kt delete mode 100644 app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/network/ApiException.kt create mode 100644 app/src/test/kotlin/com/nativeapptemplate/nativeapptemplatefree/common/errors/CodedErrorTest.kt diff --git a/app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/MainActivityViewModel.kt b/app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/MainActivityViewModel.kt index c3e569a..7c33d7a 100644 --- a/app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/MainActivityViewModel.kt +++ b/app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/MainActivityViewModel.kt @@ -5,6 +5,7 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.nativeapptemplate.nativeapptemplatefree.MainActivityUiState.Loading import com.nativeapptemplate.nativeapptemplatefree.MainActivityUiState.Success +import com.nativeapptemplate.nativeapptemplatefree.common.errors.codedDescription import com.nativeapptemplate.nativeapptemplatefree.data.login.LoginRepository import com.nativeapptemplate.nativeapptemplatefree.model.CompleteScanResult import com.nativeapptemplate.nativeapptemplatefree.model.CompleteScanResultType @@ -125,8 +126,8 @@ class MainActivityViewModel @Inject constructor( try { loginRepository.setCompleteScanResult(completeScanResult) } catch (exception: Exception) { - val message = exception.message - completeScanResult.message = message ?: "Unknown Error" + val message = exception.codedDescription + completeScanResult.message = message completeScanResult.completeScanResultType = CompleteScanResultType.Failed loginRepository.setCompleteScanResult(completeScanResult) diff --git a/app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/common/errors/ApiException.kt b/app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/common/errors/ApiException.kt new file mode 100644 index 0000000..42fa85c --- /dev/null +++ b/app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/common/errors/ApiException.kt @@ -0,0 +1,21 @@ +package com.nativeapptemplate.nativeapptemplatefree.common.errors + +sealed class ApiException(message: String, cause: Throwable? = null) : + Exception(message, cause), CodedError { + + class ApiError( + val code: Int, + val apiMessage: String, + ) : ApiException("$apiMessage [Status: $code]") { + override val errorCode: String = "NATA-2001" + override val errorDescription: String = "$apiMessage [Status: $code]" + } + + class UnprocessableError( + val rawMessage: String, + cause: Throwable? = null, + ) : ApiException("Not processable error($rawMessage).", cause) { + override val errorCode: String = "NATA-2002" + override val errorDescription: String = "Processing error: $rawMessage" + } +} diff --git a/app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/common/errors/AppError.kt b/app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/common/errors/AppError.kt new file mode 100644 index 0000000..75b13eb --- /dev/null +++ b/app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/common/errors/AppError.kt @@ -0,0 +1,12 @@ +package com.nativeapptemplate.nativeapptemplatefree.common.errors + +sealed class AppError( + override val errorCode: String, + override val errorDescription: String, +) : Exception(errorDescription), CodedError { + + class Unexpected(detail: String? = null) : AppError( + errorCode = "NATA-1001", + errorDescription = "Unexpected error" + if (detail != null) ": $detail" else "", + ) +} diff --git a/app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/common/errors/CodedError.kt b/app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/common/errors/CodedError.kt new file mode 100644 index 0000000..7f99101 --- /dev/null +++ b/app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/common/errors/CodedError.kt @@ -0,0 +1,11 @@ +package com.nativeapptemplate.nativeapptemplatefree.common.errors + +interface CodedError { + val errorCode: String + val errorDescription: String + val formattedDescription: String + get() = "[$errorCode] $errorDescription" +} + +val Throwable.codedDescription: String + get() = (this as? CodedError)?.formattedDescription ?: message ?: "Unknown Error" diff --git a/app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/common/errors/NfcError.kt b/app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/common/errors/NfcError.kt new file mode 100644 index 0000000..d650aaa --- /dev/null +++ b/app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/common/errors/NfcError.kt @@ -0,0 +1,12 @@ +package com.nativeapptemplate.nativeapptemplatefree.common.errors + +sealed class NfcError( + override val errorCode: String, + override val errorDescription: String, +) : Exception(errorDescription), CodedError { + + class ScanFailed(detail: String? = null) : NfcError( + errorCode = "NATA-3001", + errorDescription = "NFC scan operation failed" + if (detail != null) ": $detail" else "", + ) +} diff --git a/app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/common/errors/SubscriptionError.kt b/app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/common/errors/SubscriptionError.kt new file mode 100644 index 0000000..06c5398 --- /dev/null +++ b/app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/common/errors/SubscriptionError.kt @@ -0,0 +1,17 @@ +package com.nativeapptemplate.nativeapptemplatefree.common.errors + +sealed class SubscriptionError( + override val errorCode: String, + override val errorDescription: String, +) : Exception(errorDescription), CodedError { + + class RestoreFailed(detail: String? = null) : SubscriptionError( + errorCode = "NATA-6001", + errorDescription = "Failed to restore purchases" + if (detail != null) ": $detail" else "", + ) + + class SubscriptionRequired : SubscriptionError( + errorCode = "NATA-6002", + errorDescription = "User needs an active subscription", + ) +} diff --git a/app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/data/login/LoginRepositoryImpl.kt b/app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/data/login/LoginRepositoryImpl.kt index afdacf8..5f5dc94 100644 --- a/app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/data/login/LoginRepositoryImpl.kt +++ b/app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/data/login/LoginRepositoryImpl.kt @@ -1,11 +1,11 @@ package com.nativeapptemplate.nativeapptemplatefree.data.login import androidx.annotation.VisibleForTesting +import com.nativeapptemplate.nativeapptemplatefree.common.errors.ApiException import com.nativeapptemplate.nativeapptemplatefree.datastore.NatPreferencesDataSource import com.nativeapptemplate.nativeapptemplatefree.model.* import com.nativeapptemplate.nativeapptemplatefree.model.LoggedInShopkeeper import com.nativeapptemplate.nativeapptemplatefree.model.Login -import com.nativeapptemplate.nativeapptemplatefree.network.ApiException import com.nativeapptemplate.nativeapptemplatefree.network.Dispatcher import com.nativeapptemplate.nativeapptemplatefree.network.NatDispatchers import com.nativeapptemplate.nativeapptemplatefree.network.emitApiResponse diff --git a/app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/network/ApiException.kt b/app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/network/ApiException.kt deleted file mode 100644 index 842d73b..0000000 --- a/app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/network/ApiException.kt +++ /dev/null @@ -1,23 +0,0 @@ -package com.nativeapptemplate.nativeapptemplatefree.network - -/** - * Base exception for API errors thrown by repository implementations. - */ -sealed class ApiException(message: String, cause: Throwable? = null) : Exception(message, cause) { - - /** - * The API returned a structured error response that was successfully deserialized. - */ - class ApiError( - val code: Int, - val apiMessage: String, - ) : ApiException("$apiMessage [Status: $code]") - - /** - * The API returned an error response that could not be deserialized. - */ - class UnprocessableError( - val rawMessage: String, - cause: Throwable? = null, - ) : ApiException("Not processable error($rawMessage).", cause) -} diff --git a/app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/network/ApiResponseExtensions.kt b/app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/network/ApiResponseExtensions.kt index c3a2004..51ab280 100644 --- a/app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/network/ApiResponseExtensions.kt +++ b/app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/network/ApiResponseExtensions.kt @@ -1,5 +1,6 @@ package com.nativeapptemplate.nativeapptemplatefree.network +import com.nativeapptemplate.nativeapptemplatefree.common.errors.ApiException import com.nativeapptemplate.nativeapptemplatefree.model.NativeAppTemplateApiError import com.skydoves.sandwich.ApiResponse import com.skydoves.sandwich.message diff --git a/app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/ui/app_root/AcceptPrivacyViewModel.kt b/app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/ui/app_root/AcceptPrivacyViewModel.kt index 6fce95a..7763e0d 100644 --- a/app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/ui/app_root/AcceptPrivacyViewModel.kt +++ b/app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/ui/app_root/AcceptPrivacyViewModel.kt @@ -2,6 +2,7 @@ package com.nativeapptemplate.nativeapptemplatefree.ui.app_root import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.nativeapptemplate.nativeapptemplatefree.common.errors.codedDescription import com.nativeapptemplate.nativeapptemplatefree.data.login.LoginRepository import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow @@ -41,10 +42,10 @@ class AcceptPrivacyViewModel @Inject constructor( booleanFlow .catch { exception -> - val message = exception.message + val message = exception.codedDescription _uiState.update { it.copy( - message = message ?: "Unknown Error", + message = message, isLoading = false, ) } diff --git a/app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/ui/app_root/AcceptTermsViewModel.kt b/app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/ui/app_root/AcceptTermsViewModel.kt index 2a78ec4..abd81f7 100644 --- a/app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/ui/app_root/AcceptTermsViewModel.kt +++ b/app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/ui/app_root/AcceptTermsViewModel.kt @@ -2,6 +2,7 @@ package com.nativeapptemplate.nativeapptemplatefree.ui.app_root import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.nativeapptemplate.nativeapptemplatefree.common.errors.codedDescription import com.nativeapptemplate.nativeapptemplatefree.data.login.LoginRepository import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow @@ -41,10 +42,10 @@ class AcceptTermsViewModel @Inject constructor( booleanFlow .catch { exception -> - val message = exception.message + val message = exception.codedDescription _uiState.update { it.copy( - message = message ?: "Unknown Error", + message = message, isLoading = false, ) } diff --git a/app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/ui/app_root/ForgotPasswordViewModel.kt b/app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/ui/app_root/ForgotPasswordViewModel.kt index caffd7a..1efef57 100644 --- a/app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/ui/app_root/ForgotPasswordViewModel.kt +++ b/app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/ui/app_root/ForgotPasswordViewModel.kt @@ -3,6 +3,7 @@ package com.nativeapptemplate.nativeapptemplatefree.ui.app_root import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.nativeapptemplate.nativeapptemplatefree.NatConstants +import com.nativeapptemplate.nativeapptemplatefree.common.errors.codedDescription import com.nativeapptemplate.nativeapptemplatefree.data.login.SignUpRepository import com.nativeapptemplate.nativeapptemplatefree.model.SendResetPassword import com.nativeapptemplate.nativeapptemplatefree.utils.Utility.validateEmail @@ -49,10 +50,10 @@ class ForgotPasswordViewModel @Inject constructor( booleanFlow .catch { exception -> - val message = exception.message + val message = exception.codedDescription _uiState.update { it.copy( - message = message ?: "Unknown Error", + message = message, isLoading = false, ) } diff --git a/app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/ui/app_root/ResendConfirmationInstructionsViewModel.kt b/app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/ui/app_root/ResendConfirmationInstructionsViewModel.kt index 8e7acb5..020ac31 100644 --- a/app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/ui/app_root/ResendConfirmationInstructionsViewModel.kt +++ b/app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/ui/app_root/ResendConfirmationInstructionsViewModel.kt @@ -3,6 +3,7 @@ package com.nativeapptemplate.nativeapptemplatefree.ui.app_root import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.nativeapptemplate.nativeapptemplatefree.NatConstants +import com.nativeapptemplate.nativeapptemplatefree.common.errors.codedDescription import com.nativeapptemplate.nativeapptemplatefree.data.login.SignUpRepository import com.nativeapptemplate.nativeapptemplatefree.model.SendConfirmation import com.nativeapptemplate.nativeapptemplatefree.utils.Utility.validateEmail @@ -49,10 +50,10 @@ class ResendConfirmationInstructionsViewModel @Inject constructor( booleanFlow .catch { exception -> - val message = exception.message + val message = exception.codedDescription _uiState.update { it.copy( - message = message ?: "Unknown Error", + message = message, isLoading = false, ) } diff --git a/app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/ui/app_root/SignInEmailAndPasswordViewModel.kt b/app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/ui/app_root/SignInEmailAndPasswordViewModel.kt index c1e6586..927244e 100644 --- a/app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/ui/app_root/SignInEmailAndPasswordViewModel.kt +++ b/app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/ui/app_root/SignInEmailAndPasswordViewModel.kt @@ -4,6 +4,7 @@ import android.util.Log import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.nativeapptemplate.nativeapptemplatefree.NatConstants +import com.nativeapptemplate.nativeapptemplatefree.common.errors.codedDescription import com.nativeapptemplate.nativeapptemplatefree.data.login.LoginRepository import com.nativeapptemplate.nativeapptemplatefree.model.LoggedInShopkeeper import com.nativeapptemplate.nativeapptemplatefree.model.Login @@ -49,10 +50,10 @@ class SignInEmailAndPasswordViewModel @Inject constructor( loggedInShopkeeperFlow .catch { exception -> - val message = exception.message + val message = exception.codedDescription _uiState.update { it.copy( - message = message ?: "Unknown Error", + message = message, isLoading = false, ) } diff --git a/app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/ui/app_root/SignUpViewModel.kt b/app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/ui/app_root/SignUpViewModel.kt index 203b815..9348f8e 100644 --- a/app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/ui/app_root/SignUpViewModel.kt +++ b/app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/ui/app_root/SignUpViewModel.kt @@ -3,6 +3,7 @@ package com.nativeapptemplate.nativeapptemplatefree.ui.app_root import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.nativeapptemplate.nativeapptemplatefree.NatConstants +import com.nativeapptemplate.nativeapptemplatefree.common.errors.codedDescription import com.nativeapptemplate.nativeapptemplatefree.data.login.SignUpRepository import com.nativeapptemplate.nativeapptemplatefree.model.SignUp import com.nativeapptemplate.nativeapptemplatefree.model.TimeZones @@ -56,10 +57,10 @@ class SignUpViewModel @Inject constructor( loggedInShopkeeperFlow .catch { exception -> - val message = exception.message + val message = exception.codedDescription _uiState.update { it.copy( - message = message ?: "Unknown Error", + message = message, isLoading = false, ) } diff --git a/app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/ui/scan/DoScanViewModel.kt b/app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/ui/scan/DoScanViewModel.kt index 6e9c6d6..1038fa9 100644 --- a/app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/ui/scan/DoScanViewModel.kt +++ b/app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/ui/scan/DoScanViewModel.kt @@ -5,6 +5,7 @@ import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import androidx.navigation.toRoute +import com.nativeapptemplate.nativeapptemplatefree.common.errors.codedDescription import com.nativeapptemplate.nativeapptemplatefree.data.login.LoginRepository import com.nativeapptemplate.nativeapptemplatefree.model.CompleteScanResult import com.nativeapptemplate.nativeapptemplatefree.model.CompleteScanResultType @@ -53,8 +54,8 @@ class DoScanViewModel @Inject constructor( try { loginRepository.setShowTagInfoScanResult(showTagInfoScanResult) } catch (exception: Exception) { - val message = exception.message - showTagInfoScanResult.message = message ?: "Unknown Error" + val message = exception.codedDescription + showTagInfoScanResult.message = message showTagInfoScanResult.showTagInfoScanResultType = ShowTagInfoScanResultType.Failed loginRepository.setShowTagInfoScanResult(showTagInfoScanResult) @@ -73,8 +74,8 @@ class DoScanViewModel @Inject constructor( try { loginRepository.setCompleteScanResult(completeScanResult) } catch (exception: Exception) { - val message = exception.message - completeScanResult.message = message ?: "Unknown Error" + val message = exception.codedDescription + completeScanResult.message = message completeScanResult.completeScanResultType = CompleteScanResultType.Failed loginRepository.setCompleteScanResult(completeScanResult) diff --git a/app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/ui/scan/ScanViewModel.kt b/app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/ui/scan/ScanViewModel.kt index f04a83c..a5a0c47 100644 --- a/app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/ui/scan/ScanViewModel.kt +++ b/app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/ui/scan/ScanViewModel.kt @@ -2,6 +2,7 @@ package com.nativeapptemplate.nativeapptemplatefree.ui.scan import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.nativeapptemplate.nativeapptemplatefree.common.errors.codedDescription import com.nativeapptemplate.nativeapptemplatefree.data.item_tag.ItemTagRepository import com.nativeapptemplate.nativeapptemplatefree.data.login.LoginRepository import com.nativeapptemplate.nativeapptemplatefree.model.CompleteScanResult @@ -86,10 +87,10 @@ class ScanViewModel @Inject constructor( ) } }.catch { exception -> - val message = exception.message + val message = exception.codedDescription _uiState.update { it.copy( - message = message ?: "Unknown Error", + message = message, isLoading = false, ) } @@ -109,10 +110,10 @@ class ScanViewModel @Inject constructor( itemTagFlow .catch { exception -> - val message = exception.message + val message = exception.codedDescription val showTagInfoScanResult = uiState.value.showTagInfoScanResult showTagInfoScanResult.showTagInfoScanResultType = ShowTagInfoScanResultType.Failed - showTagInfoScanResult.message = message ?: "Unknown Error" + showTagInfoScanResult.message = message loginRepository.setShowTagInfoScanResult(showTagInfoScanResult) @@ -160,10 +161,10 @@ class ScanViewModel @Inject constructor( itemTagFlow .catch { exception -> - val message = exception.message + val message = exception.codedDescription val completeScanResult = uiState.value.completeScanResult completeScanResult.completeScanResultType = CompleteScanResultType.Failed - completeScanResult.message = message ?: "Unknown Error" + completeScanResult.message = message loginRepository.setCompleteScanResult(completeScanResult) @@ -214,10 +215,10 @@ class ScanViewModel @Inject constructor( itemTagFlow .catch { exception -> - val message = exception.message + val message = exception.codedDescription val completeScanResult = uiState.value.completeScanResult completeScanResult.completeScanResultType = CompleteScanResultType.Failed - completeScanResult.message = message ?: "Unknown Error" + completeScanResult.message = message loginRepository.setCompleteScanResult(completeScanResult) diff --git a/app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/ui/settings/PasswordEditViewModel.kt b/app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/ui/settings/PasswordEditViewModel.kt index 4b953d2..acf1cda 100644 --- a/app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/ui/settings/PasswordEditViewModel.kt +++ b/app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/ui/settings/PasswordEditViewModel.kt @@ -3,6 +3,7 @@ package com.nativeapptemplate.nativeapptemplatefree.ui.settings import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.nativeapptemplate.nativeapptemplatefree.NatConstants +import com.nativeapptemplate.nativeapptemplatefree.common.errors.codedDescription import com.nativeapptemplate.nativeapptemplatefree.data.login.AccountPasswordRepository import com.nativeapptemplate.nativeapptemplatefree.model.UpdatePasswordBody import com.nativeapptemplate.nativeapptemplatefree.model.UpdatePasswordBodyDetail @@ -53,10 +54,10 @@ class PasswordEditViewModel @Inject constructor( booleanFlow .catch { exception -> - val message = exception.message + val message = exception.codedDescription _uiState.update { it.copy( - message = message ?: "Unknown Error", + message = message, isLoading = false, ) } diff --git a/app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/ui/settings/SettingsViewModel.kt b/app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/ui/settings/SettingsViewModel.kt index 8a549b6..ea0f7bd 100644 --- a/app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/ui/settings/SettingsViewModel.kt +++ b/app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/ui/settings/SettingsViewModel.kt @@ -3,6 +3,7 @@ package com.nativeapptemplate.nativeapptemplatefree.ui.settings import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.nativeapptemplate.nativeapptemplatefree.BuildConfig +import com.nativeapptemplate.nativeapptemplatefree.common.errors.codedDescription import com.nativeapptemplate.nativeapptemplatefree.data.login.LoginRepository import com.nativeapptemplate.nativeapptemplatefree.model.UserData import dagger.hilt.android.lifecycle.HiltViewModel @@ -45,10 +46,10 @@ class SettingsViewModel @Inject constructor( userDataFlow .catch { exception -> - val message = exception.message + val message = exception.codedDescription _uiState.update { it.copy( - message = message ?: "Unknown Error", + message = message, isLoading = false, ) } @@ -75,9 +76,9 @@ class SettingsViewModel @Inject constructor( booleanFlow .catch { exception -> - val message = exception.message + val message = exception.codedDescription if (BuildConfig.DEBUG) { - _uiState.update { it.copy(message = message ?: "Unknown Error") } + _uiState.update { it.copy(message = message) } } _uiState.update { it.copy(isLoading = false) } } diff --git a/app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/ui/settings/ShopkeeperEditViewModel.kt b/app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/ui/settings/ShopkeeperEditViewModel.kt index b3b05c3..3999a9c 100644 --- a/app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/ui/settings/ShopkeeperEditViewModel.kt +++ b/app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/ui/settings/ShopkeeperEditViewModel.kt @@ -3,6 +3,7 @@ package com.nativeapptemplate.nativeapptemplatefree.ui.settings import android.util.Log import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.nativeapptemplate.nativeapptemplatefree.common.errors.codedDescription import com.nativeapptemplate.nativeapptemplatefree.data.login.LoginRepository import com.nativeapptemplate.nativeapptemplatefree.data.login.SignUpRepository import com.nativeapptemplate.nativeapptemplatefree.model.SignUpForUpdate @@ -63,10 +64,10 @@ class ShopkeeperEditViewModel @Inject constructor( userDataFlow .catch { exception -> - val message = exception.message + val message = exception.codedDescription _uiState.update { it.copy( - message = message ?: "Unknown Error", + message = message, isLoading = false, ) } @@ -110,10 +111,10 @@ class ShopkeeperEditViewModel @Inject constructor( loggedInShopkeeperFlow .catch { exception -> - val message = exception.message + val message = exception.codedDescription _uiState.update { it.copy( - message = message ?: "Unknown Error", + message = message, isLoading = false, ) } @@ -162,10 +163,10 @@ class ShopkeeperEditViewModel @Inject constructor( booleanFlow .catch { exception -> - val message = exception.message + val message = exception.codedDescription _uiState.update { it.copy( - message = message ?: "Unknown Error", + message = message, ) } loginRepository.clearUserPreferences() diff --git a/app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/ui/shop_detail/ShopDetailViewModel.kt b/app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/ui/shop_detail/ShopDetailViewModel.kt index 6bd9d06..4601f86 100644 --- a/app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/ui/shop_detail/ShopDetailViewModel.kt +++ b/app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/ui/shop_detail/ShopDetailViewModel.kt @@ -4,6 +4,7 @@ import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import androidx.navigation.toRoute +import com.nativeapptemplate.nativeapptemplatefree.common.errors.codedDescription import com.nativeapptemplate.nativeapptemplatefree.data.item_tag.ItemTagRepository import com.nativeapptemplate.nativeapptemplatefree.data.login.LoginRepository import com.nativeapptemplate.nativeapptemplatefree.data.shop.ShopRepository @@ -79,10 +80,10 @@ class ShopDetailViewModel @Inject constructor( ) } }.catch { exception -> - val message = exception.message + val message = exception.codedDescription _uiState.update { it.copy( - message = message ?: "Unknown Error", + message = message, isLoading = false, ) } @@ -103,11 +104,11 @@ class ShopDetailViewModel @Inject constructor( itemTagFlow .catch { exception -> - val message = exception.message + val message = exception.codedDescription _uiState.update { it.copy( - message = message ?: "Unknown Error", + message = message, isLoading = false, ) } @@ -136,10 +137,10 @@ class ShopDetailViewModel @Inject constructor( itemTagFlow .catch { exception -> - val message = exception.message + val message = exception.codedDescription _uiState.update { it.copy( - message = message ?: "Unknown Error", + message = message, isLoading = false, ) } diff --git a/app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/ui/shop_settings/NumberTagsWebpageListViewModel.kt b/app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/ui/shop_settings/NumberTagsWebpageListViewModel.kt index 9bced6c..607d864 100644 --- a/app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/ui/shop_settings/NumberTagsWebpageListViewModel.kt +++ b/app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/ui/shop_settings/NumberTagsWebpageListViewModel.kt @@ -4,6 +4,7 @@ import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import androidx.navigation.toRoute +import com.nativeapptemplate.nativeapptemplatefree.common.errors.codedDescription import com.nativeapptemplate.nativeapptemplatefree.data.shop.ShopRepository import com.nativeapptemplate.nativeapptemplatefree.model.Shop import com.nativeapptemplate.nativeapptemplatefree.ui.shop_settings.navigation.NumberTagsWebpageListRoute @@ -55,10 +56,10 @@ class NumberTagsWebpageListViewModel @Inject constructor( shopFlow .catch { exception -> - val message = exception.message + val message = exception.codedDescription _uiState.update { it.copy( - message = message ?: "Unknown Error", + message = message, isLoading = false, ) } diff --git a/app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/ui/shop_settings/ShopBasicSettingsViewModel.kt b/app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/ui/shop_settings/ShopBasicSettingsViewModel.kt index 6093725..87519bf 100644 --- a/app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/ui/shop_settings/ShopBasicSettingsViewModel.kt +++ b/app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/ui/shop_settings/ShopBasicSettingsViewModel.kt @@ -4,6 +4,7 @@ import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import androidx.navigation.toRoute +import com.nativeapptemplate.nativeapptemplatefree.common.errors.codedDescription import com.nativeapptemplate.nativeapptemplatefree.data.shop.ShopRepository import com.nativeapptemplate.nativeapptemplatefree.model.Shop import com.nativeapptemplate.nativeapptemplatefree.model.ShopUpdateBody @@ -62,10 +63,10 @@ class ShopBasicSettingsViewModel @Inject constructor( shopFlow .catch { exception -> - val message = exception.message + val message = exception.codedDescription _uiState.update { it.copy( - message = message ?: "Unknown Error", + message = message, isLoading = false, ) } @@ -105,10 +106,10 @@ class ShopBasicSettingsViewModel @Inject constructor( shopFlow .catch { exception -> - val message = exception.message + val message = exception.codedDescription _uiState.update { it.copy( - message = message ?: "Unknown Error", + message = message, isLoading = false, ) } diff --git a/app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/ui/shop_settings/ShopSettingsViewModel.kt b/app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/ui/shop_settings/ShopSettingsViewModel.kt index 86b9da1..2439b3a 100644 --- a/app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/ui/shop_settings/ShopSettingsViewModel.kt +++ b/app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/ui/shop_settings/ShopSettingsViewModel.kt @@ -4,6 +4,7 @@ import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import androidx.navigation.toRoute +import com.nativeapptemplate.nativeapptemplatefree.common.errors.codedDescription import com.nativeapptemplate.nativeapptemplatefree.data.login.LoginRepository import com.nativeapptemplate.nativeapptemplatefree.data.shop.ShopRepository import com.nativeapptemplate.nativeapptemplatefree.model.Shop @@ -57,10 +58,10 @@ class ShopSettingsViewModel @Inject constructor( shopFlow .catch { exception -> - val message = exception.message + val message = exception.codedDescription _uiState.update { it.copy( - message = message ?: "Unknown Error", + message = message, isLoading = false, ) } @@ -90,10 +91,10 @@ class ShopSettingsViewModel @Inject constructor( booleanFlow .catch { exception -> - val message = exception.message + val message = exception.codedDescription _uiState.update { it.copy( - message = message ?: "Unknown Error", + message = message, isLoading = false, ) } @@ -121,10 +122,10 @@ class ShopSettingsViewModel @Inject constructor( booleanFlow .catch { exception -> - val message = exception.message + val message = exception.codedDescription _uiState.update { it.copy( - message = message ?: "Unknown Error", + message = message, isLoading = false, ) } diff --git a/app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/ui/shop_settings/item_tag_detail/ItemTagDetailViewModel.kt b/app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/ui/shop_settings/item_tag_detail/ItemTagDetailViewModel.kt index 2581c96..8fdf1b8 100644 --- a/app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/ui/shop_settings/item_tag_detail/ItemTagDetailViewModel.kt +++ b/app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/ui/shop_settings/item_tag_detail/ItemTagDetailViewModel.kt @@ -4,6 +4,7 @@ import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import androidx.navigation.toRoute +import com.nativeapptemplate.nativeapptemplatefree.common.errors.codedDescription import com.nativeapptemplate.nativeapptemplatefree.data.item_tag.ItemTagRepository import com.nativeapptemplate.nativeapptemplatefree.model.ItemTag import com.nativeapptemplate.nativeapptemplatefree.ui.shop_settings.navigation.ItemTagDetailRoute @@ -57,10 +58,10 @@ class ItemTagDetailViewModel @Inject constructor( itemTagFlow .catch { exception -> - val message = exception.message + val message = exception.codedDescription _uiState.update { it.copy( - message = message ?: "Unknown Error", + message = message, isLoading = false, ) } @@ -90,10 +91,10 @@ class ItemTagDetailViewModel @Inject constructor( booleanFlow .catch { exception -> - val message = exception.message + val message = exception.codedDescription _uiState.update { it.copy( - message = message ?: "Unknown Error", + message = message, isLoading = false, ) } diff --git a/app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/ui/shop_settings/item_tag_detail/ItemTagEditViewModel.kt b/app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/ui/shop_settings/item_tag_detail/ItemTagEditViewModel.kt index e76363e..182cf8e 100644 --- a/app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/ui/shop_settings/item_tag_detail/ItemTagEditViewModel.kt +++ b/app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/ui/shop_settings/item_tag_detail/ItemTagEditViewModel.kt @@ -4,6 +4,7 @@ import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import androidx.navigation.toRoute +import com.nativeapptemplate.nativeapptemplatefree.common.errors.codedDescription import com.nativeapptemplate.nativeapptemplatefree.data.item_tag.ItemTagRepository import com.nativeapptemplate.nativeapptemplatefree.data.login.LoginRepository import com.nativeapptemplate.nativeapptemplatefree.model.ItemTag @@ -73,10 +74,10 @@ class ItemTagEditViewModel @Inject constructor( ) } }.catch { exception -> - val message = exception.message + val message = exception.codedDescription _uiState.update { it.copy( - message = message ?: "Unknown Error", + message = message, isLoading = false, ) } @@ -103,10 +104,10 @@ class ItemTagEditViewModel @Inject constructor( itemTagStream .catch { exception -> - val message = exception.message + val message = exception.codedDescription _uiState.update { it.copy( - message = message ?: "Unknown Error", + message = message, isLoading = false, ) } diff --git a/app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/ui/shop_settings/item_tag_list/ItemTagCreateViewModel.kt b/app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/ui/shop_settings/item_tag_list/ItemTagCreateViewModel.kt index 31033ca..096391b 100644 --- a/app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/ui/shop_settings/item_tag_list/ItemTagCreateViewModel.kt +++ b/app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/ui/shop_settings/item_tag_list/ItemTagCreateViewModel.kt @@ -4,6 +4,7 @@ import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import androidx.navigation.toRoute +import com.nativeapptemplate.nativeapptemplatefree.common.errors.codedDescription import com.nativeapptemplate.nativeapptemplatefree.data.item_tag.ItemTagRepository import com.nativeapptemplate.nativeapptemplatefree.data.login.LoginRepository import com.nativeapptemplate.nativeapptemplatefree.model.ItemTag @@ -60,10 +61,10 @@ class ItemTagCreateViewModel @Inject constructor( maximumQueueNumberLengthFlow .catch { exception -> - val message = exception.message + val message = exception.codedDescription _uiState.update { it.copy( - message = message ?: "Unknown Error", + message = message, isLoading = false, ) } @@ -98,10 +99,10 @@ class ItemTagCreateViewModel @Inject constructor( itemTagFlow .catch { exception -> - val message = exception.message + val message = exception.codedDescription _uiState.update { it.copy( - message = message ?: "Unknown Error", + message = message, isLoading = false, ) } diff --git a/app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/ui/shop_settings/item_tag_list/ItemTagListViewModel.kt b/app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/ui/shop_settings/item_tag_list/ItemTagListViewModel.kt index 66952fa..3d65999 100644 --- a/app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/ui/shop_settings/item_tag_list/ItemTagListViewModel.kt +++ b/app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/ui/shop_settings/item_tag_list/ItemTagListViewModel.kt @@ -4,6 +4,7 @@ import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import androidx.navigation.toRoute +import com.nativeapptemplate.nativeapptemplatefree.common.errors.codedDescription import com.nativeapptemplate.nativeapptemplatefree.data.item_tag.ItemTagRepository import com.nativeapptemplate.nativeapptemplatefree.data.shop.ShopRepository import com.nativeapptemplate.nativeapptemplatefree.model.ItemTags @@ -84,10 +85,10 @@ class ItemTagListViewModel @Inject constructor( ) } }.catch { exception -> - val message = exception.message + val message = exception.codedDescription _uiState.update { it.copy( - message = message ?: "Unknown Error", + message = message, isLoading = false, ) } @@ -108,10 +109,10 @@ class ItemTagListViewModel @Inject constructor( booleanFlow .catch { exception -> - val message = exception.message + val message = exception.codedDescription _uiState.update { it.copy( - message = message ?: "Unknown Error", + message = message, isLoading = false, ) } diff --git a/app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/ui/shops/ShopCreateViewModel.kt b/app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/ui/shops/ShopCreateViewModel.kt index fc887eb..31cbd69 100644 --- a/app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/ui/shops/ShopCreateViewModel.kt +++ b/app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/ui/shops/ShopCreateViewModel.kt @@ -2,6 +2,7 @@ package com.nativeapptemplate.nativeapptemplatefree.ui.shops import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.nativeapptemplate.nativeapptemplatefree.common.errors.codedDescription import com.nativeapptemplate.nativeapptemplatefree.data.shop.ShopRepository import com.nativeapptemplate.nativeapptemplatefree.model.Shop import com.nativeapptemplate.nativeapptemplatefree.model.ShopBody @@ -51,10 +52,10 @@ class ShopCreateViewModel @Inject constructor( shopFlow .catch { exception -> - val message = exception.message + val message = exception.codedDescription _uiState.update { it.copy( - message = message ?: "Unknown Error", + message = message, isLoading = false, ) } diff --git a/app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/ui/shops/ShopListViewModel.kt b/app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/ui/shops/ShopListViewModel.kt index c14d13e..4a98840 100644 --- a/app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/ui/shops/ShopListViewModel.kt +++ b/app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/ui/shops/ShopListViewModel.kt @@ -2,6 +2,7 @@ package com.nativeapptemplate.nativeapptemplatefree.ui.shops import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.nativeapptemplate.nativeapptemplatefree.common.errors.codedDescription import com.nativeapptemplate.nativeapptemplatefree.data.login.LoginRepository import com.nativeapptemplate.nativeapptemplatefree.data.shop.ShopRepository import com.nativeapptemplate.nativeapptemplatefree.model.Shops @@ -83,10 +84,10 @@ class ShopListViewModel @Inject constructor( ) } }.catch { exception -> - val message = exception.message + val message = exception.codedDescription _uiState.update { it.copy( - message = message ?: "Unknown Error", + message = message, isLoading = false, ) } diff --git a/app/src/test/kotlin/com/nativeapptemplate/nativeapptemplatefree/common/errors/CodedErrorTest.kt b/app/src/test/kotlin/com/nativeapptemplate/nativeapptemplatefree/common/errors/CodedErrorTest.kt new file mode 100644 index 0000000..bc00c98 --- /dev/null +++ b/app/src/test/kotlin/com/nativeapptemplate/nativeapptemplatefree/common/errors/CodedErrorTest.kt @@ -0,0 +1,89 @@ +package com.nativeapptemplate.nativeapptemplatefree.common.errors + +import org.junit.Assert.assertEquals +import org.junit.Test + +class CodedErrorTest { + + @Test + fun apiError_hasCorrectErrorCode() { + val error = ApiException.ApiError(code = 422, apiMessage = "Validation failed") + assertEquals("NATA-2001", error.errorCode) + } + + @Test + fun apiError_formattedDescription_includesCodeAndMessage() { + val error = ApiException.ApiError(code = 422, apiMessage = "Validation failed") + assertEquals("[NATA-2001] Validation failed [Status: 422]", error.formattedDescription) + } + + @Test + fun unprocessableError_hasCorrectErrorCode() { + val error = ApiException.UnprocessableError(rawMessage = "timeout") + assertEquals("NATA-2002", error.errorCode) + } + + @Test + fun unprocessableError_formattedDescription_includesCodeAndMessage() { + val error = ApiException.UnprocessableError(rawMessage = "timeout") + assertEquals("[NATA-2002] Processing error: timeout", error.formattedDescription) + } + + @Test + fun appError_unexpected_hasCorrectCode() { + val error = AppError.Unexpected() + assertEquals("NATA-1001", error.errorCode) + assertEquals("[NATA-1001] Unexpected error", error.formattedDescription) + } + + @Test + fun appError_unexpected_withDetail_includesDetail() { + val error = AppError.Unexpected(detail = "null pointer") + assertEquals("[NATA-1001] Unexpected error: null pointer", error.formattedDescription) + } + + @Test + fun nfcError_scanFailed_hasCorrectCode() { + val error = NfcError.ScanFailed() + assertEquals("NATA-3001", error.errorCode) + assertEquals("[NATA-3001] NFC scan operation failed", error.formattedDescription) + } + + @Test + fun nfcError_scanFailed_withDetail_includesDetail() { + val error = NfcError.ScanFailed(detail = "tag lost") + assertEquals("[NATA-3001] NFC scan operation failed: tag lost", error.formattedDescription) + } + + @Test + fun subscriptionError_restoreFailed_hasCorrectCode() { + val error = SubscriptionError.RestoreFailed() + assertEquals("NATA-6001", error.errorCode) + assertEquals("[NATA-6001] Failed to restore purchases", error.formattedDescription) + } + + @Test + fun subscriptionError_subscriptionRequired_hasCorrectCode() { + val error = SubscriptionError.SubscriptionRequired() + assertEquals("NATA-6002", error.errorCode) + assertEquals("[NATA-6002] User needs an active subscription", error.formattedDescription) + } + + @Test + fun codedDescription_forCodedError_returnsFormattedDescription() { + val error: Throwable = ApiException.ApiError(code = 500, apiMessage = "Server error") + assertEquals("[NATA-2001] Server error [Status: 500]", error.codedDescription) + } + + @Test + fun codedDescription_forRegularException_returnsMessage() { + val error: Throwable = RuntimeException("something broke") + assertEquals("something broke", error.codedDescription) + } + + @Test + fun codedDescription_forExceptionWithNullMessage_returnsUnknownError() { + val error: Throwable = RuntimeException() + assertEquals("Unknown Error", error.codedDescription) + } +} diff --git a/app/src/test/kotlin/com/nativeapptemplate/nativeapptemplatefree/network/ApiResponseExtensionsTest.kt b/app/src/test/kotlin/com/nativeapptemplate/nativeapptemplatefree/network/ApiResponseExtensionsTest.kt index 763c3f1..526180b 100644 --- a/app/src/test/kotlin/com/nativeapptemplate/nativeapptemplatefree/network/ApiResponseExtensionsTest.kt +++ b/app/src/test/kotlin/com/nativeapptemplate/nativeapptemplatefree/network/ApiResponseExtensionsTest.kt @@ -1,5 +1,6 @@ package com.nativeapptemplate.nativeapptemplatefree.network +import com.nativeapptemplate.nativeapptemplatefree.common.errors.ApiException import com.skydoves.sandwich.ApiResponse import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flow From d072be8338d964d1b2dcefa421c85bf9fa2010d4 Mon Sep 17 00:00:00 2001 From: dadachi Date: Thu, 2 Apr 2026 18:02:14 +0900 Subject: [PATCH 2/3] Add CodedError system documentation to CLAUDE.md Co-Authored-By: Claude Opus 4.6 (1M context) --- CLAUDE.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/CLAUDE.md b/CLAUDE.md index 39752cd..443db56 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -48,6 +48,19 @@ MVVM layered architecture following [Android Modern App Architecture](https://de - **Navigation routes**: Defined as extension functions on `NavGraphBuilder` (e.g., `shopListView()`, `navigateToShopDetail()`). Routes use type-safe navigation. - **Proto DataStore**: User preferences and NFC scan state are persisted via Protocol Buffers (lite). +## Error Handling (CodedError System) +All errors should use the `CodedError` interface. Error codes use the `NATA-XXXX` prefix (NativeAppTemplate Android). + +| Range | Type | Description | +|-------|------|-------------| +| NATA-1xxx | App/general errors | Unexpected errors, catch-all | +| NATA-2xxx | API/network errors | HTTP request failures, parsing errors | +| NATA-3xxx | NFC/scan errors | NFC tag read/write/scan failures | +| NATA-6xxx | Subscription errors | Purchase, restore, offering failures | + +- New error types must implement `CodedError` +- Use `codedDescription` (not `message` or `localizedMessage`) in all user-facing error messages — this prepends `[NATA-XXXX]` for `CodedError` types + ## Testing - Tests use JUnit 4 with `kotlinx.coroutines.test` and Robolectric. From a903306752b952e5e3e0341e1453f5c996f04e1e Mon Sep 17 00:00:00 2001 From: dadachi Date: Thu, 2 Apr 2026 18:07:56 +0900 Subject: [PATCH 3/3] =?UTF-8?q?Remove=20SubscriptionError=20=E2=80=94=20no?= =?UTF-8?q?=20subscription=20feature=20in=20this=20app?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.6 (1M context) --- CLAUDE.md | 1 - .../common/errors/SubscriptionError.kt | 17 ----------------- .../common/errors/CodedErrorTest.kt | 14 -------------- 3 files changed, 32 deletions(-) delete mode 100644 app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/common/errors/SubscriptionError.kt diff --git a/CLAUDE.md b/CLAUDE.md index 443db56..0176605 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -56,7 +56,6 @@ All errors should use the `CodedError` interface. Error codes use the `NATA-XXXX | NATA-1xxx | App/general errors | Unexpected errors, catch-all | | NATA-2xxx | API/network errors | HTTP request failures, parsing errors | | NATA-3xxx | NFC/scan errors | NFC tag read/write/scan failures | -| NATA-6xxx | Subscription errors | Purchase, restore, offering failures | - New error types must implement `CodedError` - Use `codedDescription` (not `message` or `localizedMessage`) in all user-facing error messages — this prepends `[NATA-XXXX]` for `CodedError` types diff --git a/app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/common/errors/SubscriptionError.kt b/app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/common/errors/SubscriptionError.kt deleted file mode 100644 index 06c5398..0000000 --- a/app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/common/errors/SubscriptionError.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.nativeapptemplate.nativeapptemplatefree.common.errors - -sealed class SubscriptionError( - override val errorCode: String, - override val errorDescription: String, -) : Exception(errorDescription), CodedError { - - class RestoreFailed(detail: String? = null) : SubscriptionError( - errorCode = "NATA-6001", - errorDescription = "Failed to restore purchases" + if (detail != null) ": $detail" else "", - ) - - class SubscriptionRequired : SubscriptionError( - errorCode = "NATA-6002", - errorDescription = "User needs an active subscription", - ) -} diff --git a/app/src/test/kotlin/com/nativeapptemplate/nativeapptemplatefree/common/errors/CodedErrorTest.kt b/app/src/test/kotlin/com/nativeapptemplate/nativeapptemplatefree/common/errors/CodedErrorTest.kt index bc00c98..4f02eed 100644 --- a/app/src/test/kotlin/com/nativeapptemplate/nativeapptemplatefree/common/errors/CodedErrorTest.kt +++ b/app/src/test/kotlin/com/nativeapptemplate/nativeapptemplatefree/common/errors/CodedErrorTest.kt @@ -55,20 +55,6 @@ class CodedErrorTest { assertEquals("[NATA-3001] NFC scan operation failed: tag lost", error.formattedDescription) } - @Test - fun subscriptionError_restoreFailed_hasCorrectCode() { - val error = SubscriptionError.RestoreFailed() - assertEquals("NATA-6001", error.errorCode) - assertEquals("[NATA-6001] Failed to restore purchases", error.formattedDescription) - } - - @Test - fun subscriptionError_subscriptionRequired_hasCorrectCode() { - val error = SubscriptionError.SubscriptionRequired() - assertEquals("NATA-6002", error.errorCode) - assertEquals("[NATA-6002] User needs an active subscription", error.formattedDescription) - } - @Test fun codedDescription_forCodedError_returnsFormattedDescription() { val error: Throwable = ApiException.ApiError(code = 500, apiMessage = "Server error")