Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
/*
* Copyright 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
* See https://github.com/openMF/android-client/blob/master/LICENSE.md
*/
package com.mifos.core.common.utils

import com.mifos.core.model.utils.DateConstants
import kotlinx.datetime.Instant
import kotlinx.datetime.LocalDate
import kotlinx.datetime.TimeZone
import kotlinx.datetime.toLocalDateTime

/**
* Centralized date formatter for Fineract API requests.
*
* This object provides standardized date formatting methods to ensure
* consistency between the date strings sent to the API and the dateFormat
* parameter that specifies how they should be parsed.
*
* Usage:
* ```kotlin
* val payload = ClientPayload(
* activationDate = ApiDateFormatter.formatForApi(dateMillis),
* dateFormat = ApiDateFormatter.DATE_FORMAT,
* locale = ApiDateFormatter.LOCALE
* )
* ```
*/
object ApiDateFormatter {

/**
* Standard date format for Fineract API: "dd MMMM yyyy"
* Example: "18 February 2026"
*/
const val DATE_FORMAT = DateConstants.DATE_FORMAT

/**
* Standard locale for Fineract API
*/
const val LOCALE = DateConstants.LOCALE

/**
* Format epoch milliseconds to API date string using standard format.
*
* @param millis Epoch milliseconds
* @return Date string in "dd MMMM yyyy" format (e.g., "18 February 2026")
*/
fun formatForApi(millis: Long): String {
return formatForApi(millis, DateFormatPattern.FULL_MONTH)
}

/**
* Format epoch milliseconds to date string using specified pattern.
*
* @param millis Epoch milliseconds
* @param pattern The date format pattern to use
* @return Formatted date string
*/
fun formatForApi(millis: Long, pattern: DateFormatPattern): String {
val dateTime = Instant
.fromEpochMilliseconds(millis)
.toLocalDateTime(TimeZone.currentSystemDefault())

return formatDateTime(dateTime, pattern)
}
Comment thread
therajanmaurya marked this conversation as resolved.

/**
* Format LocalDate to API date string using standard format.
*
* @param date LocalDate to format
* @return Date string in "dd MMMM yyyy" format (e.g., "18 February 2026")
*/
fun formatForApi(date: LocalDate): String {
return formatForApi(date, DateFormatPattern.FULL_MONTH)
}

/**
* Format LocalDate to date string using specified pattern.
*
* @param date LocalDate to format
* @param pattern The date format pattern to use
* @return Formatted date string
*/
fun formatForApi(date: LocalDate, pattern: DateFormatPattern): String {
val day = date.dayOfMonth.toString().padStart(2, '0')
val month = date.month
val monthNumber = date.monthNumber.toString().padStart(2, '0')
val year = date.year

return when (pattern) {
DateFormatPattern.FULL_MONTH -> "$day ${month.toFullName()} $year"
DateFormatPattern.SHORT_MONTH -> "$day ${month.toShortName()} $year"
DateFormatPattern.NUMERIC_DASH -> "$day-$monthNumber-$year"
DateFormatPattern.NUMERIC_SLASH -> "$day/$monthNumber/$year"
DateFormatPattern.ISO -> "$year-$monthNumber-$day"
DateFormatPattern.FULL_MONTH_WITH_TIME -> "$day ${month.toFullName()} $year 00:00"
}
}

/**
* Format date components (day, month, year) to API date string.
*
* @param day Day of month (1-31)
* @param month Month number (1-12)
* @param year Year
* @return Date string in "dd MMMM yyyy" format
*/
fun formatForApi(day: Int, month: Int, year: Int): String {
val dayStr = day.toString().padStart(2, '0')
val monthName = getMonthName(month)
return "$dayStr $monthName $year"
}
Comment thread
therajanmaurya marked this conversation as resolved.

/**
* Format date from list of integers [year, month, day] to API date string.
*
* @param dateComponents List containing [year, month, day]
* @return Date string in "dd MMMM yyyy" format
*/
fun formatFromList(dateComponents: List<Int>): String {
require(dateComponents.size >= 3) { "Date components list must have at least 3 elements" }
val year = dateComponents[0]
val month = dateComponents[1]
val day = dateComponents[2]
return formatForApi(day, month, year)
}

/**
* Get the date format pattern string for a given pattern enum.
*
* @param pattern The DateFormatPattern enum value
* @return The pattern string to send as dateFormat parameter
*/
fun getDateFormatString(pattern: DateFormatPattern): String {
return pattern.pattern
}

private fun formatDateTime(
dateTime: kotlinx.datetime.LocalDateTime,
pattern: DateFormatPattern,
): String {
val day = dateTime.dayOfMonth.toString().padStart(2, '0')
val month = dateTime.month
val monthNumber = dateTime.monthNumber.toString().padStart(2, '0')
val year = dateTime.year
val hour = dateTime.hour.toString().padStart(2, '0')
val minute = dateTime.minute.toString().padStart(2, '0')

return when (pattern) {
DateFormatPattern.FULL_MONTH -> "$day ${month.toFullName()} $year"
DateFormatPattern.SHORT_MONTH -> "$day ${month.toShortName()} $year"
DateFormatPattern.NUMERIC_DASH -> "$day-$monthNumber-$year"
DateFormatPattern.NUMERIC_SLASH -> "$day/$monthNumber/$year"
DateFormatPattern.ISO -> "$year-$monthNumber-$day"
DateFormatPattern.FULL_MONTH_WITH_TIME -> "$day ${month.toFullName()} $year $hour:$minute"
}
}

private fun kotlinx.datetime.Month.toFullName(): String {
return name.lowercase().replaceFirstChar { it.uppercase() }
}

private fun kotlinx.datetime.Month.toShortName(): String {
return name.take(3).lowercase().replaceFirstChar { it.uppercase() }
}

private fun getMonthName(month: Int): String {
return when (month) {
1 -> "January"
2 -> "February"
3 -> "March"
4 -> "April"
5 -> "May"
6 -> "June"
7 -> "July"
8 -> "August"
9 -> "September"
10 -> "October"
11 -> "November"
12 -> "December"
else -> throw IllegalArgumentException("Invalid month: $month")
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/*
* Copyright 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*
* See https://github.com/openMF/android-client/blob/master/LICENSE.md
*/
package com.mifos.core.common.utils

/**
* Enum representing standard date format patterns used in Fineract API.
*
* The Fineract API accepts dates as strings with a dateFormat parameter
* that specifies how the date should be parsed. This enum provides
* standardized patterns to ensure consistency across the application.
*/
enum class DateFormatPattern(val pattern: String) {
/**
* Full month name format: "dd MMMM yyyy" (e.g., "18 February 2026")
* This is the standard format used by Fineract API for most date fields.
*/
FULL_MONTH("dd MMMM yyyy"),

/**
* Abbreviated month name format: "dd MMM yyyy" (e.g., "18 Feb 2026")
* Used in some specific API endpoints.
*/
SHORT_MONTH("dd MMM yyyy"),

/**
* Numeric format with dashes: "dd-MM-yyyy" (e.g., "18-02-2026")
* Commonly used for display purposes.
*/
NUMERIC_DASH("dd-MM-yyyy"),

/**
* Numeric format with slashes: "dd/MM/yyyy" (e.g., "18/02/2026")
* Used for display purposes only, not recommended for API calls.
*/
NUMERIC_SLASH("dd/MM/yyyy"),

/**
* ISO 8601 format: "yyyy-MM-dd" (e.g., "2026-02-18")
* Standard international date format.
*/
ISO("yyyy-MM-dd"),

/**
* Full month with time: "dd MMMM yyyy HH:mm" (e.g., "18 February 2026 14:30")
* Used for timestamps with date and time.
*/
FULL_MONTH_WITH_TIME("dd MMMM yyyy HH:mm"),
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,20 +9,28 @@
*/
package com.mifos.core.common.utils

import kotlinx.datetime.TimeZone
import kotlinx.datetime.number
import kotlinx.datetime.toLocalDateTime
import kotlin.time.ExperimentalTime
import kotlin.time.Instant

@OptIn(ExperimentalTime::class)
/**
* Format epoch milliseconds to API-compatible date string.
*
* Returns date in "dd MMMM yyyy" format (e.g., "18 February 2026")
* which matches the standard Fineract API dateFormat parameter.
*
* @param millis Epoch milliseconds
* @return Formatted date string for API usage
* @see ApiDateFormatter for more formatting options
*/
fun formatDate(millis: Long): String {
val dateTime = Instant
.fromEpochMilliseconds(millis)
.toLocalDateTime(TimeZone.currentSystemDefault())
return ApiDateFormatter.formatForApi(millis)
}
Comment thread
therajanmaurya marked this conversation as resolved.

val day = dateTime.day.toString().padStart(2, '0')
val month = dateTime.month.number.toString().padStart(2, '0')
val year = dateTime.year
return "$day/$month/$year"
/**
* Format epoch milliseconds to date string using specified pattern.
*
* @param millis Epoch milliseconds
* @param pattern The date format pattern to use
* @return Formatted date string
* @see ApiDateFormatter for more formatting options
*/
fun formatDate(millis: Long, pattern: DateFormatPattern): String {
return ApiDateFormatter.formatForApi(millis, pattern)
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
*/
package com.mifos.room.entities.collectionsheet

import com.mifos.core.common.utils.ApiDateFormatter
import com.mifos.core.model.objects.collectionsheets.BulkSavingsDueTransaction
import com.mifos.core.model.utils.IgnoredOnParcel
import com.mifos.core.model.utils.Parcelable
Expand All @@ -34,9 +35,9 @@ data class CollectionSheetPayload(

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

var dateFormat: String = "dd MMMM yyyy",
var dateFormat: String = ApiDateFormatter.DATE_FORMAT,

var locale: String = "en",
var locale: String = ApiDateFormatter.LOCALE,

var transactionDate: String? = null,

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
*/
package com.mifos.room.entities.collectionsheet

import com.mifos.core.common.utils.ApiDateFormatter
import com.mifos.core.model.utils.Parcelable
import com.mifos.core.model.utils.Parcelize
import com.mifos.room.entities.noncore.BulkRepaymentTransactions
Expand All @@ -24,9 +25,9 @@ data class ProductiveCollectionSheetPayload(

var calendarId: Int? = 0,

var dateFormat: String? = "dd MMMM yyyy",
var dateFormat: String? = ApiDateFormatter.DATE_FORMAT,

var locale: String? = "en",
var locale: String? = ApiDateFormatter.LOCALE,

var transactionDate: String? = null,
) : Parcelable
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
*/
package com.mifos.core.model.objects.account.loan

import com.mifos.core.model.utils.DateConstants
import com.mifos.core.model.utils.Parcelable
import com.mifos.core.model.utils.Parcelize

Expand All @@ -22,7 +23,7 @@ class LoanApproval(

var note: String? = null,

var locale: String = "en",
var locale: String = DateConstants.LOCALE,

var dateFormat: String = "dd MMMM yyyy",
var dateFormat: String = DateConstants.DATE_FORMAT,
) : Parcelable
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
*/
package com.mifos.core.model.objects.account.loan

import com.mifos.core.model.utils.DateConstants
import com.mifos.core.model.utils.Parcelable
import com.mifos.core.model.utils.Parcelize

Expand All @@ -17,9 +18,9 @@ import com.mifos.core.model.utils.Parcelize
*/
@Parcelize
data class LoanApprovalRequest(
var locale: String = "en",
var locale: String = DateConstants.LOCALE,

var dateFormat: String = "dd MM yyyy",
var dateFormat: String = DateConstants.DATE_FORMAT,

var approvedOnDate: String? = null,

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
*/
package com.mifos.core.model.objects.account.loan

import com.mifos.core.model.utils.DateConstants
import com.mifos.core.model.utils.Parcelable
import com.mifos.core.model.utils.Parcelize

Expand All @@ -22,7 +23,7 @@ data class LoanDisbursement(

var paymentId: Int? = null,

var locale: String? = "en",
var locale: String? = DateConstants.LOCALE,

var dateFormat: String? = "dd MMMM yyyy",
var dateFormat: String? = DateConstants.DATE_FORMAT,
) : Parcelable
Loading