Skip to content
Closed
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
Expand Up @@ -10,7 +10,14 @@ import org.wordpress.android.fluxc.network.rest.wpcom.wc.WooResult
import org.wordpress.android.fluxc.network.rest.wpcom.wc.product.CoreProductStatus
import org.wordpress.android.fluxc.network.rest.wpcom.wc.product.ProductApiResponse
import org.wordpress.android.fluxc.network.rest.wpcom.wc.toWooError
import org.wordpress.android.util.AppLog
import java.time.DateTimeException
import java.time.LocalDateTime
import java.time.ZoneOffset
import java.time.format.DateTimeFormatter
import java.util.Locale
import javax.inject.Inject
import kotlin.time.Duration.Companion.hours

class WooPosProductRestClient @Inject constructor(
private val wooNetwork: WooNetwork,
Expand All @@ -24,6 +31,11 @@ class WooPosProductRestClient @Inject constructor(
private const val VARIATIONS_FIELDS = "id,parent_id,description,sku,global_unique_id,status,price," +
"regular_price,sale_price,date_modified,stock_quantity,stock_status,manage_stock," +
"backordered,attributes,image,downloadable,name,type"

private val SECONDS_PER_HOUR = 1.hours.inWholeSeconds

private val API_DATE_FORMATTER = DateTimeFormatter
.ofPattern("yyyy-MM-dd'T'HH:mm:ss", Locale.US)
}

suspend fun fetchProducts(
Expand All @@ -38,7 +50,7 @@ class WooPosProductRestClient @Inject constructor(
val params = buildBaseParams(
pageSize = pageSize,
page = page,
modifiedAfter = modifiedAfter,
modifiedAfter = modifiedAfter?.let { adjustUtcToSiteLocalTime(it, site.timezone) },
fields = PRODUCT_FIELDS,
includeStatus = includeStatus,
posProductsOnly = posProductsOnly
Expand Down Expand Up @@ -74,7 +86,7 @@ class WooPosProductRestClient @Inject constructor(
pageSize = pageSize,
page = page,
fields = VARIATIONS_FIELDS,
modifiedAfter = modifiedAfter,
modifiedAfter = modifiedAfter?.let { adjustUtcToSiteLocalTime(it, site.timezone) },
posProductsOnly = posProductsOnly
)

Expand Down Expand Up @@ -151,4 +163,27 @@ class WooPosProductRestClient @Inject constructor(
private fun statusListToString(statuses: List<CoreProductStatus>): String {
return statuses.joinToString(",") { it.value }
}

/**
* The WooCommerce REST API compares `modified_after` against `date_modified`, which is stored
* in the site's local timezone. Since our stored timestamps are in UTC (from server response
* headers), we must convert to site-local time before sending. Without this, sites with
* negative UTC offsets will miss recently modified products because the UTC value is always
* ahead of their local `date_modified`.
*/
internal fun adjustUtcToSiteLocalTime(utcDateString: String, siteGmtOffset: String?): String {
val offsetHours = siteGmtOffset?.toDoubleOrNull() ?: return utcDateString
if (offsetHours == 0.0) return utcDateString

return try {
val offsetSeconds = (offsetHours * SECONDS_PER_HOUR).toInt()
val zoneOffset = ZoneOffset.ofTotalSeconds(offsetSeconds)
val utcInstant = LocalDateTime.parse(utcDateString, API_DATE_FORMATTER)
.toInstant(ZoneOffset.UTC)
API_DATE_FORMATTER.withZone(zoneOffset).format(utcInstant)
} catch (e: DateTimeException) {
AppLog.e(AppLog.T.API, "Error adjusting UTC to site-local time- falling back to UTC.", e)
utcDateString
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package org.wordpress.android.fluxc.network.rest.wpcom.wc.product.pos

import org.assertj.core.api.Assertions.assertThat
import org.junit.Test
import org.mockito.kotlin.mock
import org.wordpress.android.fluxc.network.rest.wpcom.wc.WooNetwork

class WooPosProductRestClientTest {
private val wooNetwork: WooNetwork = mock()
private val sut = WooPosProductRestClient(wooNetwork)

@Test
fun `given site is in -6 timezone, when adjusting timestamp, then time is shifted backwards`() {
// WHEN
val result = sut.adjustUtcToSiteLocalTime("2024-01-15T06:30:00", "-6")

// THEN
assertThat(result).isEqualTo("2024-01-15T00:30:00")
}

@Test
fun `given site is in +5,5 timezone, when adjusting timestamp, then time is shifted forward`() {
// WHEN
val result = sut.adjustUtcToSiteLocalTime("2024-01-15T06:30:00", "5.5")

// THEN
assertThat(result).isEqualTo("2024-01-15T12:00:00")
}

@Test
fun `given zero UTC offset, when adjusting timestamp, then time is unchanged`() {
// WHEN
val result = sut.adjustUtcToSiteLocalTime("2024-01-15T06:30:00", "0")

// THEN
assertThat(result).isEqualTo("2024-01-15T06:30:00")
}

@Test
fun `given null offset, when adjusting timestamp, then time is unchanged`() {
// WHEN
val result = sut.adjustUtcToSiteLocalTime("2024-01-15T06:30:00", null)

// THEN
assertThat(result).isEqualTo("2024-01-15T06:30:00")
}

@Test
fun `given non-numeric offset, when adjusting timestamp, then time is unchanged`() {
// WHEN
val result = sut.adjustUtcToSiteLocalTime("2024-01-15T06:30:00", "America/Chicago")

// THEN
assertThat(result).isEqualTo("2024-01-15T06:30:00")
}

@Test
fun `given negative offset causing date rollback, when adjusting timestamp, then date changes correctly`() {
// WHEN
val result = sut.adjustUtcToSiteLocalTime("2024-01-15T02:00:00", "-6")

// THEN
assertThat(result).isEqualTo("2024-01-14T20:00:00")
}

@Test
fun `given positive offset causing date rollforward, when adjusting timestamp, then date changes correctly`() {
// WHEN
val result = sut.adjustUtcToSiteLocalTime("2024-01-15T22:00:00", "5.5")

// THEN
assertThat(result).isEqualTo("2024-01-16T03:30:00")
}
}
4 changes: 2 additions & 2 deletions version.properties
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
versionName=24.3-rc-1
versionCode=732
versionName=24.3-rc-2
versionCode=733
Loading