commit a172a588e4af32138009a32176a1df0fde0d2494 parent f147a929d07285b982481c9d9a636feb3f617eb4 Author: Bohdan Potuzhnyi <bohdan.potuzhnyi@gmail.com> Date: Sun, 30 Aug 2026 22:59:13 +0200 [merchant-terminal] small updates Diffstat:
17 files changed, 155 insertions(+), 25 deletions(-)
diff --git a/merchant-terminal/src/main/java/net/taler/merchantpos/history/HistoryFragment.kt b/merchant-terminal/src/main/java/net/taler/merchantpos/history/HistoryFragment.kt @@ -17,6 +17,7 @@ package net.taler.merchantpos.history import android.os.Bundle +import android.widget.Toast import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -49,6 +50,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels +import androidx.lifecycle.Lifecycle import androidx.compose.runtime.livedata.observeAsState import kotlinx.coroutines.flow.collectLatest import net.taler.lib.android.toRelativeTime @@ -124,7 +126,20 @@ class HistoryFragment : Fragment(), HistoryActionListener { if (model.configManager.merchantConfig?.baseUrl == null) { (requireActivity() as MainActivity).navigateTo(PosDestination.Config) } else { - historyManager.fetchHistory() + Toast.makeText( + requireContext(), + R.string.toast_history_updating, + Toast.LENGTH_SHORT, + ).show() + historyManager.fetchHistory { + if (isAdded && lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)) { + Toast.makeText( + requireContext(), + R.string.toast_history_updated, + Toast.LENGTH_SHORT, + ).show() + } + } } } diff --git a/merchant-terminal/src/main/java/net/taler/merchantpos/history/HistoryManager.kt b/merchant-terminal/src/main/java/net/taler/merchantpos/history/HistoryManager.kt @@ -21,6 +21,7 @@ import androidx.annotation.UiThread import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job import kotlinx.coroutines.launch import net.taler.lib.android.assertUiThread import net.taler.merchantlib.CheckPaymentResponse @@ -59,9 +60,13 @@ class HistoryManager( val error: LiveData<HistoryError?> = mError private val pagination = HistoryPagination() + private var historyLoadJob: Job? = null + private var historyLoadGeneration = 0 @UiThread - internal fun fetchHistory() = fetchHistoryPage(reset = true) + internal fun fetchHistory(onFinished: (() -> Unit)? = null) { + startHistoryLoad(reset = true, onFinished = onFinished) + } @UiThread internal fun loadMoreHistoryIfNeeded(lastVisibleIndex: Int) { @@ -69,11 +74,27 @@ class HistoryManager( if (mIsLoading.value == true || mIsLoadingMore.value == true || pagination.reachedEnd) return if (currentItems.isEmpty()) return if (lastVisibleIndex < currentItems.lastIndex - LOAD_MORE_THRESHOLD) return - fetchHistoryPage(reset = false) + startHistoryLoad(reset = false) } @UiThread - private fun fetchHistoryPage(reset: Boolean) = scope.launch { + private fun startHistoryLoad(reset: Boolean, onFinished: (() -> Unit)? = null) { + if (!reset && historyLoadJob?.isActive == true) return + + val generation = ++historyLoadGeneration + historyLoadJob?.cancel() + historyLoadJob = scope.launch { + val succeeded = fetchHistoryPages(reset, generation) + if (succeeded && generation == historyLoadGeneration) onFinished?.invoke() + } + } + + /** + * Publishes the first page immediately, then keeps loading the remaining + * history sequentially. This lets timestamp sorting cover the complete + * history without requiring the user to scroll through every page first. + */ + private suspend fun fetchHistoryPages(reset: Boolean, generation: Int): Boolean { if (reset) { mIsLoading.value = true mIsLoadingMore.value = false @@ -81,18 +102,34 @@ class HistoryManager( mIsLoadingMore.value = true } val merchantConfig = configManager.merchantConfig!! - val offset = if (reset) null else pagination.nextOffset - api.getOrderHistory( - merchantConfig = merchantConfig, - limit = -PAGE_SIZE, - offset = offset, - ).handle({ onError(R.string.error_history, it) }) { response -> - assertUiThread() - mIsLoading.value = false - mIsLoadingMore.value = false - val newItems = pagination.addPage(response.orders, reset) - publishItems() - enrichOrders(newItems) + + var resetPage = reset + while (true) { + val offset = if (resetPage) null else pagination.nextOffset + val response = api.getOrderHistory( + merchantConfig = merchantConfig, + limit = -PAGE_SIZE, + offset = offset, + ) + + // A refresh may have superseded this request while it was in flight. + if (generation != historyLoadGeneration) return false + + var succeeded = false + response.handle({ onError(R.string.error_history, it) }) { history -> + assertUiThread() + succeeded = true + val newItems = pagination.addPage(history.orders, resetPage) + val hasMorePages = !pagination.reachedEnd + mIsLoading.value = false + mIsLoadingMore.value = hasMorePages + publishItems() + enrichOrders(newItems) + } + + if (!succeeded) return false + if (pagination.reachedEnd) return true + resetPage = false } } @@ -186,6 +223,10 @@ class HistoryManager( } internal class HistoryPagination { + private val newestFirst = compareByDescending<OrderHistoryEntry> { it.timestamp.ms } + .thenByDescending { it.rowId ?: Long.MIN_VALUE } + .thenByDescending { it.orderId } + private val loadedItems = mutableListOf<OrderHistoryEntry>() var nextOffset: Long? = null @@ -215,7 +256,10 @@ internal class HistoryPagination { val existingOrderIds = loadedItems.mapTo(mutableSetOf()) { it.orderId } val newItems = page.filterNot { it.orderId in existingOrderIds } loadedItems += newItems + loadedItems.sortWith(newestFirst) + // Pagination follows the backend's raw page order, independently from + // the timestamp order used to present the combined list. val pageOffset = page.lastOrNull()?.rowId val cursorAdvanced = pageOffset != null && (reset || pageOffset != previousOffset) if (pageOffset != null) nextOffset = pageOffset @@ -227,6 +271,7 @@ internal class HistoryPagination { fun replaceItems(items: List<OrderHistoryEntry>) { loadedItems.clear() loadedItems.addAll(items) + loadedItems.sortWith(newestFirst) nextOffset = items.lastOrNull()?.rowId reachedEnd = false } @@ -238,6 +283,7 @@ internal class HistoryPagination { val index = loadedItems.indexOfFirst { it.orderId == orderId } if (index == -1) return false loadedItems[index] = transform(loadedItems[index]) + loadedItems.sortWith(newestFirst) return true } } diff --git a/merchant-terminal/src/main/res/drawable/ic_talerpos_logo.xml b/merchant-terminal/src/main/res/drawable/ic_talerpos_logo.xml @@ -16,21 +16,21 @@ android:fillType="evenOdd" android:pathData="M243.556,6.49023C249.725,6.49023 255.837,6.74707 261.887,7.2439C244.229,16.2686 227.672,27.3577 212.484,40.2222C113.1,56.2712 36.9116,148.1 36.9116,258.987C36.9116,381.182 129.43,480.241 243.556,480.241C316.584,480.241 380.762,439.678 417.517,378.451H451.357C411.559,457.645 333.433,511.484 243.556,511.484C113.314,511.484 7.73242,398.437 7.73242,258.987C7.73242,119.537 113.314,6.49023 243.556,6.49023ZM418.264,140.803C410.661,127.948 401.856,115.986 392.015,105.106C399.796,98.6167 408.078,92.7847 416.794,87.697C430.547,103.634 442.406,121.473 451.99,140.803H418.264Z" /> <path - android:fillColor="#000000" + android:fillColor="@color/talerPosLogoText" android:pathData="M442.782,199.19H495.88V169.995H360.027V199.19H413.126V349.256H442.782V199.19Z" /> <path - android:fillColor="#000000" + android:fillColor="@color/talerPosLogoText" android:pathData="M538.487,305.98H617.655L634.638,349.258H665.731L592.063,168.717H564.799L491.131,349.258H521.268L538.487,305.98ZM606.893,278.064H549.252L577.953,206.36L606.893,278.064Z" /> <path - android:fillColor="#000000" + android:fillColor="@color/talerPosLogoText" android:pathData="M719.083,169.997H692.535V349.258H811.628V320.833C780.78,320.833 749.932,320.833 719.083,320.833V169.997Z" /> <path - android:fillColor="#000000" + android:fillColor="@color/talerPosLogoText" android:pathData="M966.376,169.997H842.482V349.258H967.572V320.833H871.662V272.943H955.613V244.518H871.662V198.422H966.376V169.997Z" /> <path - android:fillColor="#000000" + android:fillColor="@color/talerPosLogoText" android:pathData="M1109.64,228.514C1109.64,237.773 1106.53,245.159 1100.28,250.621C1094.06,256.128 1085.65,258.858 1075.08,258.858H1031.91V198.422H1074.84C1085.88,198.422 1094.45,200.941 1100.52,206.019C1106.61,211.056 1109.64,218.567 1109.64,228.514ZM1144.8,349.258L1099.6,281.393C1105.5,279.687 1110.88,277.255 1115.74,274.096C1120.6,270.938 1124.79,267.096 1128.3,262.573C1131.81,258.047 1134.56,252.841 1136.55,246.952C1138.54,241.062 1139.54,234.36 1139.54,226.848C1139.54,218.141 1138.1,210.246 1135.23,203.16C1132.36,196.075 1128.26,190.099 1122.92,185.233C1117.58,180.368 1111.04,176.612 1103.31,173.965C1095.57,171.318 1086.92,169.997 1077.35,169.997H1002.73V349.258H1031.91V286.773H1068.86L1110.12,349.258H1144.8Z" /> <path - android:fillColor="#000000" + android:fillColor="@color/talerPosLogoText" android:pathData="M1215.18,352V167.4H1291.1C1298.9,167.4 1306.01,169.047 1312.42,172.34C1319.01,175.633 1324.73,180.053 1329.58,185.6C1334.43,190.973 1338.16,197.04 1340.76,203.8C1343.53,210.56 1344.92,217.493 1344.92,224.6C1344.92,234.48 1342.67,243.84 1338.16,252.68C1333.83,261.347 1327.76,268.453 1319.96,274C1312.33,279.547 1303.23,282.32 1292.66,282.32H1228.44V352H1215.18ZM1228.44,270.1H1292.14C1300.29,270.1 1307.31,267.933 1313.2,263.6C1319.09,259.267 1323.6,253.633 1326.72,246.7C1329.84,239.767 1331.4,232.4 1331.4,224.6C1331.4,216.453 1329.49,209 1325.68,202.24C1322.04,195.307 1317.1,189.76 1310.86,185.6C1304.79,181.44 1298.03,179.36 1290.58,179.36H1228.44V270.1ZM1427.16,354.6C1417.63,354.6 1408.79,352.78 1400.64,349.14C1392.67,345.5 1385.73,340.473 1379.84,334.06C1373.95,327.473 1369.35,319.933 1366.06,311.44C1362.77,302.947 1361.12,294.02 1361.12,284.66C1361.12,275.127 1362.77,266.2 1366.06,257.88C1369.35,249.387 1373.95,241.933 1379.84,235.52C1385.91,228.933 1392.93,223.82 1400.9,220.18C1409.05,216.367 1417.8,214.46 1427.16,214.46C1436.52,214.46 1445.19,216.367 1453.16,220.18C1461.13,223.82 1468.15,228.933 1474.22,235.52C1480.29,241.933 1484.97,249.387 1488.26,257.88C1491.55,266.2 1493.2,275.127 1493.2,284.66C1493.2,294.02 1491.55,302.947 1488.26,311.44C1484.97,319.933 1480.29,327.473 1474.22,334.06C1468.33,340.473 1461.31,345.5 1453.16,349.14C1445.19,352.78 1436.52,354.6 1427.16,354.6ZM1374.12,285.18C1374.12,295.753 1376.46,305.46 1381.14,314.3C1385.99,322.967 1392.41,329.9 1400.38,335.1C1408.35,340.3 1417.19,342.9 1426.9,342.9C1436.61,342.9 1445.45,340.3 1453.42,335.1C1461.57,329.727 1467.98,322.62 1472.66,313.78C1477.51,304.767 1479.94,294.973 1479.94,284.4C1479.94,273.827 1477.51,264.12 1472.66,255.28C1467.98,246.44 1461.57,239.42 1453.42,234.22C1445.45,228.847 1436.69,226.16 1427.16,226.16C1417.45,226.16 1408.61,228.847 1400.64,234.22C1392.67,239.593 1386.25,246.7 1381.4,255.54C1376.55,264.38 1374.12,274.26 1374.12,285.18ZM1634.1,198.08C1631.16,194.787 1627.86,191.927 1624.22,189.5C1620.58,186.9 1616.68,184.82 1612.52,183.26C1608.36,181.527 1603.86,180.227 1599,179.36C1594.15,178.493 1588.95,178.06 1583.4,178.06C1565.38,178.06 1552.2,181.527 1543.88,188.46C1535.74,195.22 1531.66,204.407 1531.66,216.02C1531.66,223.82 1533.48,230.06 1537.12,234.74C1540.94,239.247 1546.83,242.887 1554.8,245.66C1562.78,248.433 1573.09,251.12 1585.74,253.72C1599.09,256.493 1610.62,259.787 1620.32,263.6C1630.03,267.24 1637.48,272.267 1642.68,278.68C1648.06,285.093 1650.74,293.76 1650.74,304.68C1650.74,312.827 1649.18,320.02 1646.06,326.26C1642.94,332.327 1638.44,337.353 1632.54,341.34C1626.65,345.327 1619.63,348.36 1611.48,350.44C1603.51,352.52 1594.58,353.56 1584.7,353.56C1575.17,353.56 1566.07,352.52 1557.4,350.44C1548.74,348.36 1540.59,345.327 1532.96,341.34C1525.34,337.353 1518.23,332.24 1511.64,326L1518.66,315.34C1522.48,319.5 1526.72,323.227 1531.4,326.52C1536.26,329.64 1541.46,332.327 1547,334.58C1552.72,336.833 1558.79,338.653 1565.2,340.04C1571.62,341.253 1578.29,341.86 1585.22,341.86C1601.34,341.86 1613.91,338.913 1622.92,333.02C1632.11,326.953 1636.7,318.027 1636.7,306.24C1636.7,298.093 1634.62,291.507 1630.46,286.48C1626.3,281.453 1619.89,277.38 1611.22,274.26C1602.56,270.967 1591.72,267.933 1578.72,265.16C1565.72,262.387 1554.63,259.267 1545.44,255.8C1536.43,252.333 1529.58,247.653 1524.9,241.76C1520.4,235.867 1518.14,227.893 1518.14,217.84C1518.14,206.747 1520.83,197.387 1526.2,189.76C1531.75,181.96 1539.46,176.067 1549.34,172.08C1559.22,168.093 1570.58,166.1 1583.4,166.1C1591.38,166.1 1598.74,166.88 1605.5,168.44C1612.44,170 1618.76,172.427 1624.48,175.72C1630.38,178.84 1635.84,182.827 1640.86,187.68L1634.1,198.08Z" /> </vector> diff --git a/merchant-terminal/src/main/res/values-de/strings.xml b/merchant-terminal/src/main/res/values-de/strings.xml @@ -95,6 +95,8 @@ <string name="order_custom_product">Bezeichnung des Einzelartikels</string> <string name="host_apdu_service_desc">NFC-Zahlungen mit Taler Merchant</string> <string name="toast_reloading">Bestand wird neu geladen</string> + <string name="toast_history_updating">Bestellverlauf wird aktualisiert</string> + <string name="toast_history_updated">Bestellverlauf aktualisiert</string> <string name="config_old_deprecation">Diese Konfigurationsmethode ist veraltet; bitte die neue Merchant-API-Konfiguration verwenden.</string> <string name="product_image">Abbildung des Artikels</string> <string name="product_category_uncategorized">Ohne Kategorie</string> diff --git a/merchant-terminal/src/main/res/values-es/strings.xml b/merchant-terminal/src/main/res/values-es/strings.xml @@ -121,6 +121,8 @@ <string name="product_image">Imagen del producto</string> <string name="product_category_uncategorized">Sin categoría</string> <string name="toast_reloading">Recargando inventario</string> + <string name="toast_history_updating">Actualizando el historial de pedidos</string> + <string name="toast_history_updated">Historial de pedidos actualizado</string> <string name="config_qr_label">Configuración QR</string> <string name="scan_qr_hint">Escanea el código QR del backoffice del comercio</string> <string name="forever">Para siempre</string> diff --git a/merchant-terminal/src/main/res/values-fi/strings.xml b/merchant-terminal/src/main/res/values-fi/strings.xml @@ -134,6 +134,8 @@ <string name="config_fragment_camera_needed_text">QR-skannaus vaatii kameran käyttöoikeuden</string> <string name="host_apdu_service_desc">Taler Merchant NFC -maksut</string> <string name="toast_reloading">Ladataan varastoa uudelleen</string> + <string name="toast_history_updating">Tilaushistoriaa päivitetään</string> + <string name="toast_history_updated">Tilaushistoria päivitetty</string> <string name="product_unavailable">Ei saatavilla</string> <string name="product_out_of_stock">Loppu varastosta</string> <string name="order_delete">Poista tilaus</string> diff --git a/merchant-terminal/src/main/res/values-fr/strings.xml b/merchant-terminal/src/main/res/values-fr/strings.xml @@ -95,6 +95,8 @@ <string name="menu_reload">Recharger l’inventaire</string> <string name="config_old_deprecation">Cette méthode de configuration est obsolète, veuillez utiliser la nouvelle configuration de l\'API marchande.</string> <string name="toast_reloading">Rechargement de l\'inventaire</string> + <string name="toast_history_updating">Mise à jour de l\'historique des commandes</string> + <string name="toast_history_updated">Historique des commandes mis à jour</string> <string name="host_apdu_service_desc">Paiements NFC Taler Merchant</string> <string name="forever">Illimité</string> <string name="custom_duration">Durée personnalisée</string> diff --git a/merchant-terminal/src/main/res/values-it/strings.xml b/merchant-terminal/src/main/res/values-it/strings.xml @@ -141,6 +141,8 @@ <string name="error_cancelled">Pagamento annullato</string> <string name="error_history">Errore durante il recupero dello storico pagamenti</string> <string name="toast_reloading">Ricaricamento inventario</string> + <string name="toast_history_updating">Aggiornamento della cronologia degli ordini</string> + <string name="toast_history_updated">Cronologia degli ordini aggiornata</string> <string name="toast_back_to_exit">Premi di nuovo «indietro» per uscire</string> <string name="host_apdu_service_desc">Pagamenti NFC Taler Merchant</string> <string-array name="settings_language_values"><item></item><item>en</item><item>de</item><item>es</item><item>fi</item><item>fr</item><item>it</item><item>ru</item><item>sv</item><item>tr</item><item>uk</item><item>he</item></string-array> diff --git a/merchant-terminal/src/main/res/values-iw/strings.xml b/merchant-terminal/src/main/res/values-iw/strings.xml @@ -128,6 +128,8 @@ <string name="error_cancelled">התשלום בוטל</string> <string name="error_history">שגיאה בטעינת היסטוריית ההזמנות</string> <string name="toast_reloading">טוען מלאי מחדש</string> + <string name="toast_history_updating">היסטוריית ההזמנות מתעדכנת</string> + <string name="toast_history_updated">היסטוריית ההזמנות עודכנה</string> <string name="toast_back_to_exit">לחצו שוב על «חזרה» כדי לצאת</string> <string name="host_apdu_service_desc">תשלומי NFC של Taler Merchant</string> <string-array name="settings_language_values"><item></item><item>en</item><item>de</item><item>es</item><item>fi</item><item>fr</item><item>it</item><item>ru</item><item>sv</item><item>tr</item><item>uk</item><item>he</item></string-array> diff --git a/merchant-terminal/src/main/res/values-night/colors.xml b/merchant-terminal/src/main/res/values-night/colors.xml @@ -46,5 +46,7 @@ <color name="colorShadow">#000000</color> <color name="colorScrim">#000000</color> -</resources> + <!-- Matches the dark Taler POS logo supplied by the iOS asset catalog. --> + <color name="talerPosLogoText">#E5E2E1</color> +</resources> diff --git a/merchant-terminal/src/main/res/values-ru/strings.xml b/merchant-terminal/src/main/res/values-ru/strings.xml @@ -134,6 +134,8 @@ <string name="config_fragment_camera_needed_text">Для сканирования QR требуется разрешение на камеру</string> <string name="host_apdu_service_desc">NFC-платежи Taler Merchant</string> <string name="toast_reloading">Перезагрузка инвентаря</string> + <string name="toast_history_updating">История заказов обновляется</string> + <string name="toast_history_updated">История заказов обновлена</string> <string name="product_unavailable">Недоступно</string> <string name="product_out_of_stock">Нет в наличии</string> <string name="order_delete">Удалить заказ</string> diff --git a/merchant-terminal/src/main/res/values-sv/strings.xml b/merchant-terminal/src/main/res/values-sv/strings.xml @@ -133,6 +133,8 @@ <string name="config_fragment_camera_needed_text">Kamerabehörighet krävs för QR-skanning</string> <string name="host_apdu_service_desc">Taler Merchant NFC-betalningar</string> <string name="toast_reloading">Laddar om lager</string> + <string name="toast_history_updating">Uppdaterar orderhistoriken</string> + <string name="toast_history_updated">Orderhistoriken har uppdaterats</string> <string name="menu_reload">Ladda om lager</string> <string name="product_unavailable">Inte tillganglig</string> <string name="product_out_of_stock">Slut i lager</string> diff --git a/merchant-terminal/src/main/res/values-tr/strings.xml b/merchant-terminal/src/main/res/values-tr/strings.xml @@ -84,6 +84,8 @@ <string name="order_custom_add_button">Ekle</string> <string name="menu_reload">Envanteri yeniden yükle</string> <string name="toast_reloading">Envanter yeniden yükleniyor</string> + <string name="toast_history_updating">Sipariş geçmişi güncelleniyor</string> + <string name="toast_history_updated">Sipariş geçmişi güncellendi</string> <string name="config_old_label">JSON dosyası (eski)</string> <string name="config_manual_label">Manuel yapılandırma</string> <string name="config_setup_password">Satıcı (yeni)</string> diff --git a/merchant-terminal/src/main/res/values-uk/strings.xml b/merchant-terminal/src/main/res/values-uk/strings.xml @@ -121,6 +121,8 @@ <string name="product_image">Зображення продукту</string> <string name="product_category_uncategorized">Без категорії</string> <string name="toast_reloading">Перезавантаження інвентарю</string> + <string name="toast_history_updating">Історія замовлень оновлюється</string> + <string name="toast_history_updated">Історію замовлень оновлено</string> <string name="config_qr_label">QR-конфігурація</string> <string name="scan_qr_hint">Скануйте QR-код із бекофісу продавця</string> <string name="forever">Назавжди</string> diff --git a/merchant-terminal/src/main/res/values/colors.xml b/merchant-terminal/src/main/res/values/colors.xml @@ -44,4 +44,7 @@ <color name="colorShadow">#000000</color> <color name="colorScrim">#000000</color> + +<!-- Taler POS logo lettering; the blue mark remains unchanged in both themes. --> +<color name="talerPosLogoText">#000000</color> </resources> diff --git a/merchant-terminal/src/main/res/values/strings.xml b/merchant-terminal/src/main/res/values/strings.xml @@ -143,6 +143,8 @@ <string name="error_history">Error fetching the order history</string> <string name="toast_reloading">Reloading inventory</string> + <string name="toast_history_updating">Updating order history</string> + <string name="toast_history_updated">Order history updated</string> <string name="toast_back_to_exit">Click «back» again to exit</string> <string name="host_apdu_service_desc">Taler Merchant NFC payments</string> diff --git a/merchant-terminal/src/test/java/net/taler/merchantpos/history/HistoryPaginationTest.kt b/merchant-terminal/src/test/java/net/taler/merchantpos/history/HistoryPaginationTest.kt @@ -78,10 +78,52 @@ class HistoryPaginationTest { assertTrue(pagination.reachedEnd) } - private fun entry(rowId: Long) = OrderHistoryEntry( + @Test + fun `newer order from a later page moves to the top`() { + val pagination = HistoryPagination() + pagination.addPage( + listOf( + entry(rowId = 100, timestampMillis = 100_000), + entry(rowId = 99, timestampMillis = 90_000), + ), + reset = true, + ) + + pagination.addPage( + listOf( + entry(rowId = 98, timestampMillis = 110_000), + entry(rowId = 97, timestampMillis = 80_000), + ), + reset = false, + ) + + assertEquals(listOf(98L, 100L, 99L, 97L), pagination.items.map { it.rowId }) + assertEquals(97L, pagination.nextOffset) + } + + @Test + fun `equal timestamps use newest row first`() { + val pagination = HistoryPagination() + + pagination.addPage( + listOf( + entry(rowId = 9, timestampMillis = 1_000), + entry(rowId = 11, timestampMillis = 1_000), + entry(rowId = 10, timestampMillis = 1_000), + ), + reset = true, + ) + + assertEquals(listOf(11L, 10L, 9L), pagination.items.map { it.rowId }) + } + + private fun entry( + rowId: Long, + timestampMillis: Long = rowId * 1_000, + ) = OrderHistoryEntry( orderId = "order-$rowId", rowId = rowId, - timestamp = Timestamp.fromMillis(rowId * 1_000), + timestamp = Timestamp.fromMillis(timestampMillis), amount = Amount("KUDOS", rowId, 0), summary = "Order $rowId", paid = false,