Skip to content

Commit 9eda8fa

Browse files
committed
Update WooPosSyncTimestampManager to handle different formats
1 parent 393a394 commit 9eda8fa

File tree

2 files changed

+92
-16
lines changed

2 files changed

+92
-16
lines changed

WooCommerce/src/main/kotlin/com/woocommerce/android/ui/woopos/util/datastore/WooPosSyncTimestampManager.kt

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,17 @@ package com.woocommerce.android.ui.woopos.util.datastore
22

33
import com.woocommerce.android.ui.woopos.common.util.WooPosLogWrapper
44
import java.time.Instant
5-
import java.time.LocalDateTime
65
import java.time.ZoneOffset
76
import java.time.format.DateTimeFormatter
87
import java.time.format.DateTimeParseException
8+
import java.time.temporal.TemporalAccessor
99
import java.util.Locale
1010
import javax.inject.Inject
1111

1212
class WooPosSyncTimestampManager @Inject constructor(
1313
private val timestampRepository: WooPosSyncTimestampRepository,
1414
private val logger: WooPosLogWrapper
1515
) {
16-
private val gmtFormatter = DateTimeFormatter.ofPattern(GMT_DATE_FORMAT, Locale.US).withZone(ZoneOffset.UTC)
1716

1817
suspend fun storeProductsLastSyncTimestamp(timestamp: Long) {
1918
timestampRepository.storeProductsLastSyncTimestamp(timestamp)
@@ -37,24 +36,41 @@ class WooPosSyncTimestampManager @Inject constructor(
3736
}
3837

3938
fun formatTimestampForApi(timestamp: Long): String {
40-
return gmtFormatter.format(Instant.ofEpochMilli(timestamp))
39+
return defaultApiDateFormatter.format(Instant.ofEpochMilli(timestamp))
4140
}
4241

4342
fun parseTimestampFromApi(dateFromApi: String): Long? {
4443
return parseGmtTimestamp(dateFromApi)
4544
}
4645

4746
private fun parseGmtTimestamp(dateFromApi: String): Long? {
48-
return try {
49-
val localDateTime = LocalDateTime.parse(dateFromApi, gmtFormatter)
50-
return localDateTime.toInstant(ZoneOffset.UTC).toEpochMilli()
51-
} catch (e: DateTimeParseException) {
52-
logger.e("Failed to parse GMT timestamp: '$dateFromApi'", e)
53-
null
47+
for (formatter in PARSING_FORMATTERS) {
48+
try {
49+
val temporal: TemporalAccessor = formatter.parse(dateFromApi)
50+
return Instant.from(temporal).toEpochMilli()
51+
} catch (_: Exception) {
52+
continue
53+
}
5454
}
55+
56+
logger.e(
57+
"Failed to parse GMT timestamp: '$dateFromApi' with any supported format",
58+
DateTimeParseException("None of the supported formats could parse the date", dateFromApi, 0)
59+
)
60+
return null
5561
}
5662

5763
private companion object {
58-
const val GMT_DATE_FORMAT = "yyyy-MM-dd'T'HH:mm:ss"
64+
private const val DEFAULT_API_DATE_FORMAT = "yyyy-MM-dd'T'HH:mm:ss"
65+
private val defaultApiDateFormatter = DateTimeFormatter.ofPattern(DEFAULT_API_DATE_FORMAT, Locale.US)
66+
.withZone(ZoneOffset.UTC)
67+
68+
private val PARSING_FORMATTERS = listOf(
69+
defaultApiDateFormatter,
70+
DateTimeFormatter.ofPattern("EEE, dd MMM yyyy HH:mm:ss 'GMT'", Locale.US).withZone(ZoneOffset.UTC),
71+
DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US).withZone(ZoneOffset.UTC),
72+
DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSS", Locale.US).withZone(ZoneOffset.UTC),
73+
DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US).withZone(ZoneOffset.UTC)
74+
)
5975
}
6076
}

WooCommerce/src/test/kotlin/com/woocommerce/android/ui/woopos/util/datastore/WooPosSyncTimestampManagerTest.kt

Lines changed: 66 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@ class WooPosSyncTimestampManagerTest {
2020
private val logger: WooPosLogWrapper = mock()
2121

2222
private lateinit var manager: WooPosSyncTimestampManager
23-
private val gmtFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss", Locale.US).withZone(ZoneOffset.UTC)
23+
private val gmtFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss", Locale.US)
24+
.withZone(ZoneOffset.UTC)
2425

2526
@Before
2627
fun setup() {
@@ -187,7 +188,7 @@ class WooPosSyncTimestampManagerTest {
187188

188189
// THEN
189190
assertThat(result).isNull()
190-
verify(logger).e(eq("Failed to parse GMT timestamp: '2024-15-45T25:70:90'"), any())
191+
verify(logger).e(eq("Failed to parse GMT timestamp: '2024-15-45T25:70:90' with any supported format"), any())
191192
}
192193

193194
@Test
@@ -214,7 +215,7 @@ class WooPosSyncTimestampManagerTest {
214215

215216
// THEN
216217
assertThat(result).isNull()
217-
verify(logger).e(eq("Failed to parse GMT timestamp: ''"), any())
218+
verify(logger).e(eq("Failed to parse GMT timestamp: '' with any supported format"), any())
218219
}
219220

220221
@Test
@@ -227,19 +228,78 @@ class WooPosSyncTimestampManagerTest {
227228

228229
// THEN
229230
assertThat(result).isNull()
230-
verify(logger).e(eq("Failed to parse GMT timestamp: ' '"), any())
231+
verify(logger).e(eq("Failed to parse GMT timestamp: ' ' with any supported format"), any())
231232
}
232233

233234
@Test
234-
fun `given timestamp with timezone suffix, when parsing from API, then null is returned`() {
235+
fun `given timestamp with timezone suffix Z, when parsing from API, then correct timestamp is returned`() {
235236
// GIVEN
236237
val timestampWithTZ = "2024-01-15T10:30:00Z"
238+
val expectedTimestamp = LocalDateTime.parse("2024-01-15T10:30:00", gmtFormatter)
239+
.toInstant(ZoneOffset.UTC).toEpochMilli()
237240

238241
// WHEN
239242
val result = manager.parseTimestampFromApi(timestampWithTZ)
240243

244+
// THEN
245+
assertThat(result).isEqualTo(expectedTimestamp)
246+
}
247+
248+
@Test
249+
fun `given RFC 2822 format timestamp, when parsing from API, then correct timestamp is returned`() {
250+
// GIVEN
251+
val rfc2822Timestamp = "Tue, 09 Sep 2025 08:40:42 GMT"
252+
// Create expected timestamp: September 9, 2025 08:40:42 GMT
253+
val expectedTimestamp = LocalDateTime.of(2025, 9, 9, 8, 40, 42)
254+
.toInstant(ZoneOffset.UTC).toEpochMilli()
255+
256+
// WHEN
257+
val result = manager.parseTimestampFromApi(rfc2822Timestamp)
258+
259+
// THEN
260+
assertThat(result).isEqualTo(expectedTimestamp)
261+
}
262+
263+
@Test
264+
fun `given timestamp with milliseconds, when parsing from API, then correct timestamp is returned`() {
265+
// GIVEN
266+
val timestampWithMillis = "2024-01-15T10:30:00.123"
267+
// Expected timestamp should preserve milliseconds
268+
val expectedTimestamp = LocalDateTime.of(2024, 1, 15, 10, 30, 0, 123_000_000)
269+
.toInstant(ZoneOffset.UTC).toEpochMilli()
270+
271+
// WHEN
272+
val result = manager.parseTimestampFromApi(timestampWithMillis)
273+
274+
// THEN
275+
assertThat(result).isEqualTo(expectedTimestamp)
276+
}
277+
278+
@Test
279+
fun `given timestamp with milliseconds and Z, when parsing from API, then correct timestamp is returned`() {
280+
// GIVEN
281+
val timestampWithMillisAndZ = "2024-01-15T10:30:00.456Z"
282+
// Expected timestamp should preserve milliseconds
283+
val expectedTimestamp = LocalDateTime.of(2024, 1, 15, 10, 30, 0, 456_000_000)
284+
.toInstant(ZoneOffset.UTC).toEpochMilli()
285+
286+
// WHEN
287+
val result = manager.parseTimestampFromApi(timestampWithMillisAndZ)
288+
289+
// THEN
290+
assertThat(result).isEqualTo(expectedTimestamp)
291+
}
292+
293+
@Test
294+
fun `given completely invalid format, when parsing from API, then null is returned and error logged`() {
295+
// GIVEN
296+
val invalidFormat = "not-a-valid-timestamp"
297+
298+
// WHEN
299+
val result = manager.parseTimestampFromApi(invalidFormat)
300+
241301
// THEN
242302
assertThat(result).isNull()
243-
verify(logger).e(eq("Failed to parse GMT timestamp: '2024-01-15T10:30:00Z'"), any())
303+
verify(logger).e(eq("Failed to parse GMT timestamp: 'not-a-valid-timestamp' with any supported format"), any())
244304
}
245305
}

0 commit comments

Comments
 (0)