Skip to content

Commit a2ab067

Browse files
fix(common): correct date format in formatDate() for API compatibility (#2616)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
1 parent d5530ef commit a2ab067

29 files changed

Lines changed: 382 additions & 77 deletions

File tree

Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
/*
2+
* Copyright 2025 Mifos Initiative
3+
*
4+
* This Source Code Form is subject to the terms of the Mozilla Public
5+
* License, v. 2.0. If a copy of the MPL was not distributed with this
6+
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
7+
*
8+
* See https://github.com/openMF/android-client/blob/master/LICENSE.md
9+
*/
10+
package com.mifos.core.common.utils
11+
12+
import com.mifos.core.model.utils.DateConstants
13+
import kotlinx.datetime.Instant
14+
import kotlinx.datetime.LocalDate
15+
import kotlinx.datetime.TimeZone
16+
import kotlinx.datetime.toLocalDateTime
17+
18+
/**
19+
* Centralized date formatter for Fineract API requests.
20+
*
21+
* This object provides standardized date formatting methods to ensure
22+
* consistency between the date strings sent to the API and the dateFormat
23+
* parameter that specifies how they should be parsed.
24+
*
25+
* Usage:
26+
* ```kotlin
27+
* val payload = ClientPayload(
28+
* activationDate = ApiDateFormatter.formatForApi(dateMillis),
29+
* dateFormat = ApiDateFormatter.DATE_FORMAT,
30+
* locale = ApiDateFormatter.LOCALE
31+
* )
32+
* ```
33+
*/
34+
object ApiDateFormatter {
35+
36+
/**
37+
* Standard date format for Fineract API: "dd MMMM yyyy"
38+
* Example: "18 February 2026"
39+
*/
40+
const val DATE_FORMAT = DateConstants.DATE_FORMAT
41+
42+
/**
43+
* Standard locale for Fineract API
44+
*/
45+
const val LOCALE = DateConstants.LOCALE
46+
47+
/**
48+
* Format epoch milliseconds to API date string using standard format.
49+
*
50+
* @param millis Epoch milliseconds
51+
* @return Date string in "dd MMMM yyyy" format (e.g., "18 February 2026")
52+
*/
53+
fun formatForApi(millis: Long): String {
54+
return formatForApi(millis, DateFormatPattern.FULL_MONTH)
55+
}
56+
57+
/**
58+
* Format epoch milliseconds to date string using specified pattern.
59+
*
60+
* @param millis Epoch milliseconds
61+
* @param pattern The date format pattern to use
62+
* @return Formatted date string
63+
*/
64+
fun formatForApi(millis: Long, pattern: DateFormatPattern): String {
65+
val dateTime = Instant
66+
.fromEpochMilliseconds(millis)
67+
.toLocalDateTime(TimeZone.currentSystemDefault())
68+
69+
return formatDateTime(dateTime, pattern)
70+
}
71+
72+
/**
73+
* Format LocalDate to API date string using standard format.
74+
*
75+
* @param date LocalDate to format
76+
* @return Date string in "dd MMMM yyyy" format (e.g., "18 February 2026")
77+
*/
78+
fun formatForApi(date: LocalDate): String {
79+
return formatForApi(date, DateFormatPattern.FULL_MONTH)
80+
}
81+
82+
/**
83+
* Format LocalDate to date string using specified pattern.
84+
*
85+
* @param date LocalDate to format
86+
* @param pattern The date format pattern to use
87+
* @return Formatted date string
88+
*/
89+
fun formatForApi(date: LocalDate, pattern: DateFormatPattern): String {
90+
val day = date.dayOfMonth.toString().padStart(2, '0')
91+
val month = date.month
92+
val monthNumber = date.monthNumber.toString().padStart(2, '0')
93+
val year = date.year
94+
95+
return when (pattern) {
96+
DateFormatPattern.FULL_MONTH -> "$day ${month.toFullName()} $year"
97+
DateFormatPattern.SHORT_MONTH -> "$day ${month.toShortName()} $year"
98+
DateFormatPattern.NUMERIC_DASH -> "$day-$monthNumber-$year"
99+
DateFormatPattern.NUMERIC_SLASH -> "$day/$monthNumber/$year"
100+
DateFormatPattern.ISO -> "$year-$monthNumber-$day"
101+
DateFormatPattern.FULL_MONTH_WITH_TIME -> "$day ${month.toFullName()} $year 00:00"
102+
}
103+
}
104+
105+
/**
106+
* Format date components (day, month, year) to API date string.
107+
*
108+
* @param day Day of month (1-31)
109+
* @param month Month number (1-12)
110+
* @param year Year
111+
* @return Date string in "dd MMMM yyyy" format
112+
*/
113+
fun formatForApi(day: Int, month: Int, year: Int): String {
114+
val dayStr = day.toString().padStart(2, '0')
115+
val monthName = getMonthName(month)
116+
return "$dayStr $monthName $year"
117+
}
118+
119+
/**
120+
* Format date from list of integers [year, month, day] to API date string.
121+
*
122+
* @param dateComponents List containing [year, month, day]
123+
* @return Date string in "dd MMMM yyyy" format
124+
*/
125+
fun formatFromList(dateComponents: List<Int>): String {
126+
require(dateComponents.size >= 3) { "Date components list must have at least 3 elements" }
127+
val year = dateComponents[0]
128+
val month = dateComponents[1]
129+
val day = dateComponents[2]
130+
return formatForApi(day, month, year)
131+
}
132+
133+
/**
134+
* Get the date format pattern string for a given pattern enum.
135+
*
136+
* @param pattern The DateFormatPattern enum value
137+
* @return The pattern string to send as dateFormat parameter
138+
*/
139+
fun getDateFormatString(pattern: DateFormatPattern): String {
140+
return pattern.pattern
141+
}
142+
143+
private fun formatDateTime(
144+
dateTime: kotlinx.datetime.LocalDateTime,
145+
pattern: DateFormatPattern,
146+
): String {
147+
val day = dateTime.dayOfMonth.toString().padStart(2, '0')
148+
val month = dateTime.month
149+
val monthNumber = dateTime.monthNumber.toString().padStart(2, '0')
150+
val year = dateTime.year
151+
val hour = dateTime.hour.toString().padStart(2, '0')
152+
val minute = dateTime.minute.toString().padStart(2, '0')
153+
154+
return when (pattern) {
155+
DateFormatPattern.FULL_MONTH -> "$day ${month.toFullName()} $year"
156+
DateFormatPattern.SHORT_MONTH -> "$day ${month.toShortName()} $year"
157+
DateFormatPattern.NUMERIC_DASH -> "$day-$monthNumber-$year"
158+
DateFormatPattern.NUMERIC_SLASH -> "$day/$monthNumber/$year"
159+
DateFormatPattern.ISO -> "$year-$monthNumber-$day"
160+
DateFormatPattern.FULL_MONTH_WITH_TIME -> "$day ${month.toFullName()} $year $hour:$minute"
161+
}
162+
}
163+
164+
private fun kotlinx.datetime.Month.toFullName(): String {
165+
return name.lowercase().replaceFirstChar { it.uppercase() }
166+
}
167+
168+
private fun kotlinx.datetime.Month.toShortName(): String {
169+
return name.take(3).lowercase().replaceFirstChar { it.uppercase() }
170+
}
171+
172+
private fun getMonthName(month: Int): String {
173+
return when (month) {
174+
1 -> "January"
175+
2 -> "February"
176+
3 -> "March"
177+
4 -> "April"
178+
5 -> "May"
179+
6 -> "June"
180+
7 -> "July"
181+
8 -> "August"
182+
9 -> "September"
183+
10 -> "October"
184+
11 -> "November"
185+
12 -> "December"
186+
else -> throw IllegalArgumentException("Invalid month: $month")
187+
}
188+
}
189+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
/*
2+
* Copyright 2025 Mifos Initiative
3+
*
4+
* This Source Code Form is subject to the terms of the Mozilla Public
5+
* License, v. 2.0. If a copy of the MPL was not distributed with this
6+
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
7+
*
8+
* See https://github.com/openMF/android-client/blob/master/LICENSE.md
9+
*/
10+
package com.mifos.core.common.utils
11+
12+
/**
13+
* Enum representing standard date format patterns used in Fineract API.
14+
*
15+
* The Fineract API accepts dates as strings with a dateFormat parameter
16+
* that specifies how the date should be parsed. This enum provides
17+
* standardized patterns to ensure consistency across the application.
18+
*/
19+
enum class DateFormatPattern(val pattern: String) {
20+
/**
21+
* Full month name format: "dd MMMM yyyy" (e.g., "18 February 2026")
22+
* This is the standard format used by Fineract API for most date fields.
23+
*/
24+
FULL_MONTH("dd MMMM yyyy"),
25+
26+
/**
27+
* Abbreviated month name format: "dd MMM yyyy" (e.g., "18 Feb 2026")
28+
* Used in some specific API endpoints.
29+
*/
30+
SHORT_MONTH("dd MMM yyyy"),
31+
32+
/**
33+
* Numeric format with dashes: "dd-MM-yyyy" (e.g., "18-02-2026")
34+
* Commonly used for display purposes.
35+
*/
36+
NUMERIC_DASH("dd-MM-yyyy"),
37+
38+
/**
39+
* Numeric format with slashes: "dd/MM/yyyy" (e.g., "18/02/2026")
40+
* Used for display purposes only, not recommended for API calls.
41+
*/
42+
NUMERIC_SLASH("dd/MM/yyyy"),
43+
44+
/**
45+
* ISO 8601 format: "yyyy-MM-dd" (e.g., "2026-02-18")
46+
* Standard international date format.
47+
*/
48+
ISO("yyyy-MM-dd"),
49+
50+
/**
51+
* Full month with time: "dd MMMM yyyy HH:mm" (e.g., "18 February 2026 14:30")
52+
* Used for timestamps with date and time.
53+
*/
54+
FULL_MONTH_WITH_TIME("dd MMMM yyyy HH:mm"),
55+
}

core/common/src/commonMain/kotlin/com/mifos/core/common/utils/FormatDate.kt

Lines changed: 22 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -9,20 +9,28 @@
99
*/
1010
package com.mifos.core.common.utils
1111

12-
import kotlinx.datetime.TimeZone
13-
import kotlinx.datetime.number
14-
import kotlinx.datetime.toLocalDateTime
15-
import kotlin.time.ExperimentalTime
16-
import kotlin.time.Instant
17-
18-
@OptIn(ExperimentalTime::class)
12+
/**
13+
* Format epoch milliseconds to API-compatible date string.
14+
*
15+
* Returns date in "dd MMMM yyyy" format (e.g., "18 February 2026")
16+
* which matches the standard Fineract API dateFormat parameter.
17+
*
18+
* @param millis Epoch milliseconds
19+
* @return Formatted date string for API usage
20+
* @see ApiDateFormatter for more formatting options
21+
*/
1922
fun formatDate(millis: Long): String {
20-
val dateTime = Instant
21-
.fromEpochMilliseconds(millis)
22-
.toLocalDateTime(TimeZone.currentSystemDefault())
23+
return ApiDateFormatter.formatForApi(millis)
24+
}
2325

24-
val day = dateTime.day.toString().padStart(2, '0')
25-
val month = dateTime.month.number.toString().padStart(2, '0')
26-
val year = dateTime.year
27-
return "$day/$month/$year"
26+
/**
27+
* Format epoch milliseconds to date string using specified pattern.
28+
*
29+
* @param millis Epoch milliseconds
30+
* @param pattern The date format pattern to use
31+
* @return Formatted date string
32+
* @see ApiDateFormatter for more formatting options
33+
*/
34+
fun formatDate(millis: Long, pattern: DateFormatPattern): String {
35+
return ApiDateFormatter.formatForApi(millis, pattern)
2836
}

core/database/src/commonMain/kotlin/com/mifos/room/entities/collectionsheet/CollectionSheetPayload.kt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
*/
1010
package com.mifos.room.entities.collectionsheet
1111

12+
import com.mifos.core.common.utils.ApiDateFormatter
1213
import com.mifos.core.model.objects.collectionsheets.BulkSavingsDueTransaction
1314
import com.mifos.core.model.utils.IgnoredOnParcel
1415
import com.mifos.core.model.utils.Parcelable
@@ -34,9 +35,9 @@ data class CollectionSheetPayload(
3435

3536
var clientsAttendance: MutableList<ClientsAttendance> = ArrayList(),
3637

37-
var dateFormat: String = "dd MMMM yyyy",
38+
var dateFormat: String = ApiDateFormatter.DATE_FORMAT,
3839

39-
var locale: String = "en",
40+
var locale: String = ApiDateFormatter.LOCALE,
4041

4142
var transactionDate: String? = null,
4243

core/database/src/commonMain/kotlin/com/mifos/room/entities/collectionsheet/ProductiveCollectionSheetPayload.kt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
*/
1010
package com.mifos.room.entities.collectionsheet
1111

12+
import com.mifos.core.common.utils.ApiDateFormatter
1213
import com.mifos.core.model.utils.Parcelable
1314
import com.mifos.core.model.utils.Parcelize
1415
import com.mifos.room.entities.noncore.BulkRepaymentTransactions
@@ -24,9 +25,9 @@ data class ProductiveCollectionSheetPayload(
2425

2526
var calendarId: Int? = 0,
2627

27-
var dateFormat: String? = "dd MMMM yyyy",
28+
var dateFormat: String? = ApiDateFormatter.DATE_FORMAT,
2829

29-
var locale: String? = "en",
30+
var locale: String? = ApiDateFormatter.LOCALE,
3031

3132
var transactionDate: String? = null,
3233
) : Parcelable

core/model/src/commonMain/kotlin/com/mifos/core/model/objects/account/loan/LoanApproval.kt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
*/
1010
package com.mifos.core.model.objects.account.loan
1111

12+
import com.mifos.core.model.utils.DateConstants
1213
import com.mifos.core.model.utils.Parcelable
1314
import com.mifos.core.model.utils.Parcelize
1415

@@ -22,7 +23,7 @@ class LoanApproval(
2223

2324
var note: String? = null,
2425

25-
var locale: String = "en",
26+
var locale: String = DateConstants.LOCALE,
2627

27-
var dateFormat: String = "dd MMMM yyyy",
28+
var dateFormat: String = DateConstants.DATE_FORMAT,
2829
) : Parcelable

core/model/src/commonMain/kotlin/com/mifos/core/model/objects/account/loan/LoanApprovalRequest.kt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
*/
1010
package com.mifos.core.model.objects.account.loan
1111

12+
import com.mifos.core.model.utils.DateConstants
1213
import com.mifos.core.model.utils.Parcelable
1314
import com.mifos.core.model.utils.Parcelize
1415

@@ -17,9 +18,9 @@ import com.mifos.core.model.utils.Parcelize
1718
*/
1819
@Parcelize
1920
data class LoanApprovalRequest(
20-
var locale: String = "en",
21+
var locale: String = DateConstants.LOCALE,
2122

22-
var dateFormat: String = "dd MM yyyy",
23+
var dateFormat: String = DateConstants.DATE_FORMAT,
2324

2425
var approvedOnDate: String? = null,
2526

core/model/src/commonMain/kotlin/com/mifos/core/model/objects/account/loan/LoanDisbursement.kt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
*/
1010
package com.mifos.core.model.objects.account.loan
1111

12+
import com.mifos.core.model.utils.DateConstants
1213
import com.mifos.core.model.utils.Parcelable
1314
import com.mifos.core.model.utils.Parcelize
1415

@@ -22,7 +23,7 @@ data class LoanDisbursement(
2223

2324
var paymentId: Int? = null,
2425

25-
var locale: String? = "en",
26+
var locale: String? = DateConstants.LOCALE,
2627

27-
var dateFormat: String? = "dd MMMM yyyy",
28+
var dateFormat: String? = DateConstants.DATE_FORMAT,
2829
) : Parcelable

0 commit comments

Comments
 (0)