fix: lifetime optional in validation + type — app no longer sends it

validateConsumption required lifetime, but the app stopped sending it
(backend now derives it). asObject(undefined) threw 'body is required',
rejecting every chunk. Now lifetime is optional in both the validator
and the DailyConsumption type.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
TCFDONMEZ
2026-10-09 23:49:57 +03:00
co-authored by Claude
parent 06265bacc9
commit ed5e38bdb8
3 changed files with 18 additions and 13 deletions
@@ -88,7 +88,7 @@ final class ConsumptionCloudClient {
void syncIfDue(ConsumptionTracker tracker, ChargeSessionTracker charges, long nowMs) {
long elapsed = android.os.SystemClock.elapsedRealtime();
if (endpoint.isEmpty() || token.isEmpty()) {
Log.d(TAG, "Sync skipped: API not configured");
Log.i(TAG, "Sync skipped: API not configured");
return;
}
syncPendingChargesIfDue(charges, elapsed);
@@ -107,10 +107,10 @@ final class ConsumptionCloudClient {
ConsumptionTracker.CloudSnapshot chunk = tracker.takeChunkDelta();
if (chunk == null) {
Log.d(TAG, "Sync due but no new consumption since last chunk");
Log.i(TAG, "Sync due but no new consumption since last chunk");
return; // no driving since the last chunk
}
Log.d(TAG, "New chunk for " + chunk.date + ": +" + String.format("%.1f", chunk.day.km)
Log.i(TAG, "New chunk for " + chunk.date + ": +" + String.format("%.1f", chunk.day.km)
+ "km, +" + String.format("%.2f", chunk.day.kwh) + "kWh");
try {
JSONObject body = consumptionBody(chunk, nowMs);
@@ -153,7 +153,7 @@ final class ConsumptionCloudClient {
String date = body.optString("date", "?");
if (send("/v1/consumption/daily", "PUT", body)) {
sent++;
Log.d(TAG, "Cached chunk for " + date + " sent");
Log.i(TAG, "Cached chunk for " + date + " sent");
} else {
// Server rejected — keep it, try the next one too.
remaining.put(body);
@@ -227,16 +227,16 @@ final class ConsumptionCloudClient {
private void syncPendingCharges(ChargeSessionTracker charges, List<JSONObject> pending)
throws Exception {
Log.d(TAG, "Found " + pending.size() + " pending charge sessions");
Log.i(TAG, "Found " + pending.size() + " pending charge sessions");
int sent = 0;
for (JSONObject session : pending) {
JSONObject body = new JSONObject(session.toString()).put("installationId", installationId);
String id = session.getString("sessionId");
Log.d(TAG, "Uploading charge session: " + id);
Log.i(TAG, "Uploading charge session: " + id);
if (send("/v1/charging-sessions/" + id, "PUT", body)) {
charges.markUploaded(id);
logEvent(true, 200, "Şarj session gönderildi: " + id);
Log.d(TAG, "Successfully uploaded charge session: " + id);
Log.i(TAG, "Successfully uploaded charge session: " + id);
} else {
logEvent(false, 0, "Şarj session gönderilemedi: " + id);
Log.w(TAG, "Failed to upload charge session: " + id);
@@ -251,7 +251,8 @@ final class ConsumptionCloudClient {
try {
byte[] bytes = body.toString().getBytes(StandardCharsets.UTF_8);
String url = trimSlash(endpoint) + path;
Log.d(TAG, "Sending " + method + " request to " + url + " (" + bytes.length + " bytes)");
Log.i(TAG, "Sending " + method + " request to " + url + " (" + bytes.length + " bytes)");
Log.i(TAG, "Body: " + body.toString());
connection = (HttpURLConnection) new URL(url).openConnection();
connection.setRequestMethod(method);
connection.setConnectTimeout(TIMEOUT_MS);
@@ -262,7 +263,7 @@ final class ConsumptionCloudClient {
connection.setRequestProperty("Authorization", "Bearer " + token);
try (OutputStream out = connection.getOutputStream()) { out.write(bytes); }
int status = connection.getResponseCode();
Log.d(TAG, "Response: HTTP " + status);
Log.i(TAG, "Response: HTTP " + status);
if (status < 200 || status >= 300) {
String errBody = readErrorBody(connection);
Log.w(TAG, "Sync rejected with HTTP " + status + (errBody.isEmpty() ? "" : ": " + errBody));
@@ -308,7 +309,7 @@ final class ConsumptionCloudClient {
try {
List<DayRecord> fresh = fetchHistory();
if (!fresh.isEmpty()) cachedHistory = fresh;
Log.d(TAG, "Fetched " + fresh.size() + " history records from cloud");
Log.i(TAG, "Fetched " + fresh.size() + " history records from cloud");
logEvent(true, 200, "History çekildi: " + fresh.size() + " gün");
} catch (Exception e) {
String msg = e.getMessage() == null ? e.getClass().getSimpleName() : e.getMessage();
@@ -363,7 +364,7 @@ final class ConsumptionCloudClient {
connection.setReadTimeout(TIMEOUT_MS);
connection.setRequestProperty("Authorization", "Bearer " + token);
int status = connection.getResponseCode();
Log.d(TAG, "History GET response: HTTP " + status);
Log.i(TAG, "History GET response: HTTP " + status);
if (status < 200 || status >= 300) {
String err = readErrorBody(connection);
Log.w(TAG, "History fetch rejected with HTTP " + status + (err.isEmpty() ? "" : ": " + err));
+2 -1
View File
@@ -22,7 +22,8 @@ export interface DailyConsumption {
date: string;
recordedAt: Date;
day: Totals;
lifetime: Totals;
/** Optional — the backend derives lifetime as the sum of all days' totals. */
lifetime?: Totals;
sohPercent?: number;
createdAt: Date;
updatedAt: Date;
+4 -1
View File
@@ -21,12 +21,15 @@ export function validateConsumption(body: unknown): ConsumptionInput {
const date = record.date;
if (typeof date !== "string" || !ISO_DATE.test(date)) throw new ValidationError("date is invalid");
const sohPercent = record.sohPercent;
const lifetime = record.lifetime;
return {
installationId: validateUuid(record.installationId, "installationId"),
date,
recordedAt: validateDate(record.recordedAt, "recordedAt"),
day: validateTotals(record.day, "day"),
lifetime: validateTotals(record.lifetime, "lifetime"),
// Lifetime is optional — the backend derives it from day totals, and the app no longer
// sends it. Legacy documents may still carry one; new chunks do not.
...(lifetime === undefined ? {} : { lifetime: validateTotals(lifetime, "lifetime") }),
...(sohPercent === undefined ? {} : { sohPercent: bounded(sohPercent, 0, 100, "sohPercent") }),
};
}