From 62e308a1cf615bf03ff0ab2f7165dd624c65d5da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Arsic=CC=81?= Date: Tue, 1 Sep 2026 11:08:11 +0200 Subject: [PATCH 1/2] Fix print crash on short journal headers and add PDF diagnostics Replace hard subList indices in CreatePdf with drop/take so journal headers with fewer than 6 lines (e.g. proforma receipts) no longer throw IndexOutOfBoundsException on print. Add temporary [DEBUG-pdf] logging to Logcat and Crashlytics to identify the malformed journal shape. Bump version to 3.5.2. Claude-Session: https://claude.ai/code/session_01MqPHhmoisS6SCg3E6umn2L --- .gitignore | 3 + CHANGELOG.md | 10 +++ CLAUDE.md | 80 +++++++++++++++++++ app/build.gradle | 2 +- .../online/taxcore/pos/utils/CreatePdf.kt | 21 ++++- 5 files changed, 112 insertions(+), 4 deletions(-) create mode 100644 CLAUDE.md diff --git a/.gitignore b/.gitignore index 6edf947..dd57c76 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,6 @@ *.aab *.apk google-services.json +!CLAUDE.md + +.worktrees diff --git a/CHANGELOG.md b/CHANGELOG.md index 8ee181a..bcbb3cf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,16 @@ All notable changes to this project will be documented in this file. - **TBD** Configuration response error handling +## [3.5.2] - 2026-09-01 + +### Fixed + +- Fixed crash when printing invoices whose journal header has fewer lines than expected (e.g. proforma receipts) + +### Added + +- Temporary diagnostic logging of journal format during PDF printing to identify malformed journal payloads + ## [3.5.1] - 2025-12-16 ### Added diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..dcec4d4 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,80 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +TaxCore Mobile POS — an Android point-of-sale app for small/medium businesses. Handles invoice creation, catalog management, journal tracking, and tax compliance via the TaxCore API. + +## Build Commands + +```bash +./gradlew assembleDebug # Build debug APK +./gradlew test # Run unit tests +./gradlew clean # Clean build +./gradlew printVersionInformation # Print version info +``` + +## Tech Stack + +- **Kotlin 2.1.0**, JDK 17, Compile/Target SDK 36, Min SDK 24 +- **Single module** (`:app`), Groovy-based `app/build.gradle` +- **View Binding** (XML layouts, no Compose) +- **Realm 10.19.0** for local database (schema v3, UI-thread access enabled) +- **Retrofit 3.0.0** + Gson for networking with custom certificate pinning +- **Dagger 2.21** for dependency injection (KAPT) +- **Firebase Crashlytics** for crash reporting +- **EventBus** for inter-component communication +- **EncryptedSharedPreferences** for sensitive config storage + +## Architecture + +R +**MVP-lite pattern** with Dagger 2 DI: + +- Activities extend `BaseActivity` (implements `HasSupportFragmentInjector`) +- Fragments contain presentation logic directly (no separate Presenter classes) +- `AppComponent` → `AppModule` + `ActivityBuilder` → `MainActivityModule` (binds fragments) + +**Navigation flow**: `SplashActivity` → `DashboardActivity` → feature screens (Invoice, Catalog, Journal, Settings) + +**Data layer**: + +- `data/realm/` — Realm model classes (Item, Journal, Cashier, Taxes, Cert) +- `data/local/` — Manager classes for database operations (CatalogManager, InvoiceManager, JournalManager, TaxesManager, CertManager) +- `data/api/` — Retrofit API service and client with certificate handling +- `data/models/` — API response DTOs +- `data/params/` — API request objects +- `PrefService` — encrypted preferences wrapper + +**Key API endpoints** (v3): + +- `POST api/v3/invoices` — submit invoices +- `GET api/v3/status` — fetch taxes/status +- `GET api/v3/environment-parameters` — configuration + +## Global App State + +`AppSession` object holds runtime state: `isAppConfigured`, `shouldAskForConfiguration`, `pinCode`, `pacCode`. + +## Versioning + +Version defined in `app/build.gradle`: `versionMajor * 10000 + versionMinor * 100 + versionPatch * 10 + appVersionBuild`. Current: 3.5.1. + +## Localization + +Multi-language: French (fr), Serbian (sr), Bosnian (bs). Dynamic locale switching via `CtxUtils.updateLocale()` with context wrapper. + +## CI/CD + +GitHub Actions (`.github/workflows/v3-release.yml`) builds on pushes to `main-v3`/`purs` branches and PRs. Uploads to Firebase App Distribution. + +## Skills + +- `create-spec` — Create a feature specification at `docs/specs/-spec.md` with requirements interview and structured template + +## Conventions + +- Commit messages in present tense describing what the commit does to the code +- Debug keystore at `keys/taxcore-debug.jks` (credentials in `gradle.properties`) +- Release signing injected via CI/CD from GitHub Secrets diff --git a/app/build.gradle b/app/build.gradle index 32597be..1c5840b 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -8,7 +8,7 @@ apply plugin: 'com.google.firebase.crashlytics' def versionMajor = 3 def versionMinor = 5 -def versionPatch = 1 +def versionPatch = 2 def versionBuild = 1 def appVersionBuild = Integer.valueOf(System.env.GITHUB_RUN_NUMBER ?: versionBuild) diff --git a/app/src/main/java/online/taxcore/pos/utils/CreatePdf.kt b/app/src/main/java/online/taxcore/pos/utils/CreatePdf.kt index a8c3443..74fa3a2 100644 --- a/app/src/main/java/online/taxcore/pos/utils/CreatePdf.kt +++ b/app/src/main/java/online/taxcore/pos/utils/CreatePdf.kt @@ -1,5 +1,7 @@ package online.taxcore.pos.utils +import android.util.Log +import com.google.firebase.crashlytics.FirebaseCrashlytics import com.itextpdf.text.* import com.itextpdf.text.pdf.BaseFont import com.itextpdf.text.pdf.PdfPCell @@ -28,11 +30,24 @@ object CreatePdf { val delimiter = "========================================\r\n" val invoiceHeader = invoiceJournal.split(delimiter).first() + // [DEBUG-pdf] temporary diagnostics: which journal shape reaches the PDF parser + val debugMsg = "[DEBUG-pdf] len=${invoiceJournal.length}" + + " crlf=${invoiceJournal.contains("\r\n")}" + + " lfOnly=${!invoiceJournal.contains("\r\n") && invoiceJournal.contains("\n")}" + + " delimiterHits=${invoiceJournal.split(delimiter).size - 1}" + + " headerLines=${invoiceHeader.split("\r\n".toRegex()).size}" + + " first120='${invoiceJournal.take(120).replace("\r", "\\r").replace("\n", "\\n")}'" + Log.d("CreatePdf", debugMsg) + FirebaseCrashlytics.getInstance().log(debugMsg) + + // Header line count varies per taxpayer; drop/take instead of subList + // so short headers don't throw IndexOutOfBoundsException val headerElements = invoiceHeader.split("\r\n".toRegex()) + .dropLastWhile { it.isEmpty() } - val headerStart = headerElements.first() - val companyHeader = headerElements.subList(1, 6).joinToString("\r\n") { it.trim() } - val headerInfo = headerElements.subList(6, headerElements.size - 1).joinToString("\r\n") + val headerStart = headerElements.firstOrNull().orEmpty() + val companyHeader = headerElements.drop(1).take(5).joinToString("\r\n") { it.trim() } + val headerInfo = headerElements.drop(6).joinToString("\r\n") val invoiceFooter = invoiceJournal.split(delimiter).last() val invoiceMain = invoiceJournal From 5c2cf56b427be43a57f0efd17e050235ac2099b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Arsic=CC=81?= Date: Tue, 1 Sep 2026 12:11:19 +0200 Subject: [PATCH 2/2] Report malformed journals as non-fatals and limit content logging Send the PDF journal diagnostics to Crashlytics as a non-fatal only when the journal is malformed, include receipt content in the log solely on that path, and remove CLAUDE.md from version control. Claude-Session: https://claude.ai/code/session_01MqPHhmoisS6SCg3E6umn2L --- .gitignore | 1 - CLAUDE.md | 80 ------------------- .../online/taxcore/pos/utils/CreatePdf.kt | 26 +++--- 3 files changed, 17 insertions(+), 90 deletions(-) delete mode 100644 CLAUDE.md diff --git a/.gitignore b/.gitignore index dd57c76..000541f 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,5 @@ *.aab *.apk google-services.json -!CLAUDE.md .worktrees diff --git a/CLAUDE.md b/CLAUDE.md deleted file mode 100644 index dcec4d4..0000000 --- a/CLAUDE.md +++ /dev/null @@ -1,80 +0,0 @@ -# CLAUDE.md - -This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. - -## Project Overview - -TaxCore Mobile POS — an Android point-of-sale app for small/medium businesses. Handles invoice creation, catalog management, journal tracking, and tax compliance via the TaxCore API. - -## Build Commands - -```bash -./gradlew assembleDebug # Build debug APK -./gradlew test # Run unit tests -./gradlew clean # Clean build -./gradlew printVersionInformation # Print version info -``` - -## Tech Stack - -- **Kotlin 2.1.0**, JDK 17, Compile/Target SDK 36, Min SDK 24 -- **Single module** (`:app`), Groovy-based `app/build.gradle` -- **View Binding** (XML layouts, no Compose) -- **Realm 10.19.0** for local database (schema v3, UI-thread access enabled) -- **Retrofit 3.0.0** + Gson for networking with custom certificate pinning -- **Dagger 2.21** for dependency injection (KAPT) -- **Firebase Crashlytics** for crash reporting -- **EventBus** for inter-component communication -- **EncryptedSharedPreferences** for sensitive config storage - -## Architecture - -R -**MVP-lite pattern** with Dagger 2 DI: - -- Activities extend `BaseActivity` (implements `HasSupportFragmentInjector`) -- Fragments contain presentation logic directly (no separate Presenter classes) -- `AppComponent` → `AppModule` + `ActivityBuilder` → `MainActivityModule` (binds fragments) - -**Navigation flow**: `SplashActivity` → `DashboardActivity` → feature screens (Invoice, Catalog, Journal, Settings) - -**Data layer**: - -- `data/realm/` — Realm model classes (Item, Journal, Cashier, Taxes, Cert) -- `data/local/` — Manager classes for database operations (CatalogManager, InvoiceManager, JournalManager, TaxesManager, CertManager) -- `data/api/` — Retrofit API service and client with certificate handling -- `data/models/` — API response DTOs -- `data/params/` — API request objects -- `PrefService` — encrypted preferences wrapper - -**Key API endpoints** (v3): - -- `POST api/v3/invoices` — submit invoices -- `GET api/v3/status` — fetch taxes/status -- `GET api/v3/environment-parameters` — configuration - -## Global App State - -`AppSession` object holds runtime state: `isAppConfigured`, `shouldAskForConfiguration`, `pinCode`, `pacCode`. - -## Versioning - -Version defined in `app/build.gradle`: `versionMajor * 10000 + versionMinor * 100 + versionPatch * 10 + appVersionBuild`. Current: 3.5.1. - -## Localization - -Multi-language: French (fr), Serbian (sr), Bosnian (bs). Dynamic locale switching via `CtxUtils.updateLocale()` with context wrapper. - -## CI/CD - -GitHub Actions (`.github/workflows/v3-release.yml`) builds on pushes to `main-v3`/`purs` branches and PRs. Uploads to Firebase App Distribution. - -## Skills - -- `create-spec` — Create a feature specification at `docs/specs/-spec.md` with requirements interview and structured template - -## Conventions - -- Commit messages in present tense describing what the commit does to the code -- Debug keystore at `keys/taxcore-debug.jks` (credentials in `gradle.properties`) -- Release signing injected via CI/CD from GitHub Secrets diff --git a/app/src/main/java/online/taxcore/pos/utils/CreatePdf.kt b/app/src/main/java/online/taxcore/pos/utils/CreatePdf.kt index 74fa3a2..37f5cd0 100644 --- a/app/src/main/java/online/taxcore/pos/utils/CreatePdf.kt +++ b/app/src/main/java/online/taxcore/pos/utils/CreatePdf.kt @@ -30,20 +30,28 @@ object CreatePdf { val delimiter = "========================================\r\n" val invoiceHeader = invoiceJournal.split(delimiter).first() + // Header line count varies per taxpayer; drop/take instead of subList + // so short headers don't throw IndexOutOfBoundsException + val headerElements = invoiceHeader.split("\r\n".toRegex()) + .dropLastWhile { it.isEmpty() } + // [DEBUG-pdf] temporary diagnostics: which journal shape reaches the PDF parser + val delimiterHits = invoiceJournal.split(delimiter).size - 1 val debugMsg = "[DEBUG-pdf] len=${invoiceJournal.length}" + " crlf=${invoiceJournal.contains("\r\n")}" + " lfOnly=${!invoiceJournal.contains("\r\n") && invoiceJournal.contains("\n")}" + - " delimiterHits=${invoiceJournal.split(delimiter).size - 1}" + - " headerLines=${invoiceHeader.split("\r\n".toRegex()).size}" + - " first120='${invoiceJournal.take(120).replace("\r", "\\r").replace("\n", "\\n")}'" + " delimiterHits=$delimiterHits" + + " headerLines=${headerElements.size}" Log.d("CreatePdf", debugMsg) - FirebaseCrashlytics.getInstance().log(debugMsg) - - // Header line count varies per taxpayer; drop/take instead of subList - // so short headers don't throw IndexOutOfBoundsException - val headerElements = invoiceHeader.split("\r\n".toRegex()) - .dropLastWhile { it.isEmpty() } + if (delimiterHits == 0 || headerElements.size < 6) { + // journal content only on the malformed path, so receipt data + // isn't sent to Crashlytics for every print + val first120 = invoiceJournal.take(120) + .replace("\r", "\\r").replace("\n", "\\n") + FirebaseCrashlytics.getInstance().log("$debugMsg first120='$first120'") + FirebaseCrashlytics.getInstance() + .recordException(IllegalStateException("Malformed invoice journal: $debugMsg")) + } val headerStart = headerElements.firstOrNull().orEmpty() val companyHeader = headerElements.drop(1).take(5).joinToString("\r\n") { it.trim() }