M1: contracts and vectors (SPEC v0.1, schema 1)
contracts/openapi.yaml (OpenAPI 3.1, E-1..E-33, error shape and the exhaustive error code enum, device bearer scheme), contracts/schema/ server-v1.sql (SPEC §14.1 verbatim), seven test-vector files per §14.5.5, 80 examples (every JSON body of §14.3.4, every error code, sync pull/push, every FCM push type), and tools/contracts-check.py (stdlib-only reference of §9.1, §9.3, §9.4 and §14.5.4 plus shape checks; the M1 gate). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"status": "ok",
|
||||
"server_version": "0.1.0",
|
||||
"schema_version": 1,
|
||||
"db_ok": true
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"id": "0192a000-0000-7000-8000-000000000001",
|
||||
"role": "admin",
|
||||
"display_name": "Jacob",
|
||||
"picture_version": null,
|
||||
"created_at": "2026-10-17T06:12:40.512Z",
|
||||
"updated_at": "2026-10-17T09:12:00.000Z",
|
||||
"server_version": 42
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"kind": "partner",
|
||||
"target_user_id": null
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"id": "0192a002-0000-7000-8000-000000000001",
|
||||
"kind": "partner",
|
||||
"target_user_id": null,
|
||||
"code": "4N8RVT2KW6ZC",
|
||||
"link": "https://icehack.asuscomm.com:8443/i/4N8RVT2KW6ZC",
|
||||
"created_at": "2026-10-17T06:30:02.004Z",
|
||||
"expires_at": "2026-10-20T06:30:02.004Z"
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"invites": [
|
||||
{
|
||||
"id": "0192a002-0000-7000-8000-000000000002",
|
||||
"kind": "device",
|
||||
"target_user_id": "0192a000-0000-7000-8000-000000000002",
|
||||
"code": null,
|
||||
"link": null,
|
||||
"created_at": "2026-10-17T09:00:00.000Z",
|
||||
"expires_at": "2026-10-20T09:00:00.000Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"devices": [
|
||||
{
|
||||
"id": "0192a001-0000-7000-8000-000000000001",
|
||||
"user_id": "0192a000-0000-7000-8000-000000000001",
|
||||
"name": "Galaxy S26",
|
||||
"app_version_code": 41,
|
||||
"created_at": "2026-10-17T06:12:40.512Z",
|
||||
"last_seen_at": "2026-10-17T09:00:00.000Z",
|
||||
"revoked_at": null,
|
||||
"is_current": true
|
||||
},
|
||||
{
|
||||
"id": "0192a001-0000-7000-8000-000000000003",
|
||||
"user_id": "0192a000-0000-7000-8000-000000000001",
|
||||
"name": "Old phone",
|
||||
"app_version_code": null,
|
||||
"created_at": "2026-10-17T06:20:00.000Z",
|
||||
"last_seen_at": "2026-10-17T06:25:00.000Z",
|
||||
"revoked_at": "2026-10-17T06:40:00.000Z",
|
||||
"is_current": false
|
||||
},
|
||||
{
|
||||
"id": "0192a001-0000-7000-8000-000000000002",
|
||||
"user_id": "0192a000-0000-7000-8000-000000000002",
|
||||
"name": "Galaxy S26+",
|
||||
"app_version_code": 41,
|
||||
"created_at": "2026-10-17T07:01:15.000Z",
|
||||
"last_seen_at": "2026-10-17T08:40:12.000Z",
|
||||
"revoked_at": null,
|
||||
"is_current": false
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"fcm_token": "fGx1c2hUb2tlbjpBUEE5MWJIUG5vQ2tFeGFtcGxl"
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"name": "Vardag",
|
||||
"default_shares": [
|
||||
{
|
||||
"user_id": "0192a000-0000-7000-8000-000000000001",
|
||||
"share_bp": 5000
|
||||
},
|
||||
{
|
||||
"user_id": "0192a000-0000-7000-8000-000000000002",
|
||||
"share_bp": 5000
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"id": "0192a003-0000-7000-8000-000000000001",
|
||||
"name": "Vardag",
|
||||
"default_shares": [
|
||||
{
|
||||
"user_id": "0192a000-0000-7000-8000-000000000001",
|
||||
"share_bp": 5000
|
||||
},
|
||||
{
|
||||
"user_id": "0192a000-0000-7000-8000-000000000002",
|
||||
"share_bp": 5000
|
||||
}
|
||||
],
|
||||
"created_by_user_id": "0192a000-0000-7000-8000-000000000001",
|
||||
"created_at": "2026-10-17T07:10:00.000Z",
|
||||
"updated_at": "2026-10-17T07:10:00.000Z",
|
||||
"archived_at": null,
|
||||
"deleted_at": null,
|
||||
"server_version": 13
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"name": "Vardag & mat",
|
||||
"default_shares": [
|
||||
{
|
||||
"user_id": "0192a000-0000-7000-8000-000000000001",
|
||||
"share_bp": 6000
|
||||
},
|
||||
{
|
||||
"user_id": "0192a000-0000-7000-8000-000000000002",
|
||||
"share_bp": 4000
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"id": "0192a003-0000-7000-8000-000000000001",
|
||||
"name": "Vardag & mat",
|
||||
"default_shares": [
|
||||
{
|
||||
"user_id": "0192a000-0000-7000-8000-000000000001",
|
||||
"share_bp": 6000
|
||||
},
|
||||
{
|
||||
"user_id": "0192a000-0000-7000-8000-000000000002",
|
||||
"share_bp": 4000
|
||||
}
|
||||
],
|
||||
"created_by_user_id": "0192a000-0000-7000-8000-000000000001",
|
||||
"created_at": "2026-10-17T07:10:00.000Z",
|
||||
"updated_at": "2026-10-17T09:13:00.000Z",
|
||||
"archived_at": null,
|
||||
"deleted_at": null,
|
||||
"server_version": 43
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"initialized": true
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"id": "0192a003-0000-7000-8000-000000000002",
|
||||
"name": "Resor",
|
||||
"default_shares": [
|
||||
{
|
||||
"user_id": "0192a000-0000-7000-8000-000000000001",
|
||||
"share_bp": 5000
|
||||
},
|
||||
{
|
||||
"user_id": "0192a000-0000-7000-8000-000000000002",
|
||||
"share_bp": 5000
|
||||
}
|
||||
],
|
||||
"created_by_user_id": "0192a000-0000-7000-8000-000000000001",
|
||||
"created_at": "2026-10-17T07:10:00.000Z",
|
||||
"updated_at": "2026-10-17T09:14:00.000Z",
|
||||
"archived_at": "2026-10-17T09:14:00.000Z",
|
||||
"deleted_at": null,
|
||||
"server_version": 44
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"id": "0192a003-0000-7000-8000-000000000002",
|
||||
"name": "Resor",
|
||||
"default_shares": [
|
||||
{
|
||||
"user_id": "0192a000-0000-7000-8000-000000000001",
|
||||
"share_bp": 5000
|
||||
},
|
||||
{
|
||||
"user_id": "0192a000-0000-7000-8000-000000000002",
|
||||
"share_bp": 5000
|
||||
}
|
||||
],
|
||||
"created_by_user_id": "0192a000-0000-7000-8000-000000000001",
|
||||
"created_at": "2026-10-17T07:10:00.000Z",
|
||||
"updated_at": "2026-10-17T09:15:00.000Z",
|
||||
"archived_at": null,
|
||||
"deleted_at": null,
|
||||
"server_version": 45
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"id": "0192a003-0000-7000-8000-000000000002",
|
||||
"name": "Resor",
|
||||
"default_shares": [
|
||||
{
|
||||
"user_id": "0192a000-0000-7000-8000-000000000001",
|
||||
"share_bp": 5000
|
||||
},
|
||||
{
|
||||
"user_id": "0192a000-0000-7000-8000-000000000002",
|
||||
"share_bp": 5000
|
||||
}
|
||||
],
|
||||
"created_by_user_id": "0192a000-0000-7000-8000-000000000001",
|
||||
"created_at": "2026-10-17T07:10:00.000Z",
|
||||
"updated_at": "2026-10-17T09:16:00.000Z",
|
||||
"archived_at": null,
|
||||
"deleted_at": "2026-10-17T09:16:00.000Z",
|
||||
"server_version": 46
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"name": "Husdjur",
|
||||
"icon": "pets",
|
||||
"color": "lime"
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"id": "0192a007-0000-7000-8000-000000000001",
|
||||
"default_key": null,
|
||||
"name": "Husdjur",
|
||||
"icon": "pets",
|
||||
"color": "lime",
|
||||
"sort_order": 110,
|
||||
"created_at": "2026-10-17T09:17:00.000Z",
|
||||
"updated_at": "2026-10-17T09:17:00.000Z",
|
||||
"archived_at": null,
|
||||
"deleted_at": null,
|
||||
"server_version": 47
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"name": "Matvaror",
|
||||
"sort_order": 15
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"id": "01920000-0000-7000-8000-000000000001",
|
||||
"default_key": "groceries",
|
||||
"name": "Matvaror",
|
||||
"icon": "shopping_cart",
|
||||
"color": "green",
|
||||
"sort_order": 15,
|
||||
"created_at": "2026-10-17T00:00:00.000Z",
|
||||
"updated_at": "2026-10-17T09:18:00.000Z",
|
||||
"archived_at": null,
|
||||
"deleted_at": null,
|
||||
"server_version": 48
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"id": "0192a007-0000-7000-8000-000000000001",
|
||||
"default_key": null,
|
||||
"name": "Husdjur",
|
||||
"icon": "pets",
|
||||
"color": "lime",
|
||||
"sort_order": 110,
|
||||
"created_at": "2026-10-17T09:17:00.000Z",
|
||||
"updated_at": "2026-10-17T09:19:00.000Z",
|
||||
"archived_at": "2026-10-17T09:19:00.000Z",
|
||||
"deleted_at": null,
|
||||
"server_version": 49
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"id": "0192a007-0000-7000-8000-000000000001",
|
||||
"default_key": null,
|
||||
"name": "Husdjur",
|
||||
"icon": "pets",
|
||||
"color": "lime",
|
||||
"sort_order": 110,
|
||||
"created_at": "2026-10-17T09:17:00.000Z",
|
||||
"updated_at": "2026-10-17T09:20:00.000Z",
|
||||
"archived_at": null,
|
||||
"deleted_at": null,
|
||||
"server_version": 50
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"id": "0192a007-0000-7000-8000-000000000001",
|
||||
"default_key": null,
|
||||
"name": "Husdjur",
|
||||
"icon": "pets",
|
||||
"color": "lime",
|
||||
"sort_order": 110,
|
||||
"created_at": "2026-10-17T09:17:00.000Z",
|
||||
"updated_at": "2026-10-17T09:21:00.000Z",
|
||||
"archived_at": null,
|
||||
"deleted_at": "2026-10-17T09:21:00.000Z",
|
||||
"server_version": 51
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"account_id": "0192a003-0000-7000-8000-000000000001",
|
||||
"category_id": "01920000-0000-7000-8000-000000000003",
|
||||
"amount_ore": 1250000,
|
||||
"description": "Hyra",
|
||||
"notes": "",
|
||||
"payer_user_id": "0192a000-0000-7000-8000-000000000001",
|
||||
"split_mode": "ratio",
|
||||
"splits": [
|
||||
{
|
||||
"user_id": "0192a000-0000-7000-8000-000000000001",
|
||||
"share_bp": 6000,
|
||||
"share_ore": null
|
||||
},
|
||||
{
|
||||
"user_id": "0192a000-0000-7000-8000-000000000002",
|
||||
"share_bp": 4000,
|
||||
"share_ore": null
|
||||
}
|
||||
],
|
||||
"day_of_month": 25,
|
||||
"start_period": "2026-10",
|
||||
"end_period": null,
|
||||
"paused": false
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"id": "0192a004-0000-7000-8000-000000000001",
|
||||
"account_id": "0192a003-0000-7000-8000-000000000001",
|
||||
"category_id": "01920000-0000-7000-8000-000000000003",
|
||||
"amount_ore": 1250000,
|
||||
"description": "Hyra",
|
||||
"notes": "",
|
||||
"payer_user_id": "0192a000-0000-7000-8000-000000000001",
|
||||
"split_mode": "ratio",
|
||||
"splits": [
|
||||
{
|
||||
"user_id": "0192a000-0000-7000-8000-000000000001",
|
||||
"share_bp": 6000,
|
||||
"share_ore": null
|
||||
},
|
||||
{
|
||||
"user_id": "0192a000-0000-7000-8000-000000000002",
|
||||
"share_bp": 4000,
|
||||
"share_ore": null
|
||||
}
|
||||
],
|
||||
"day_of_month": 25,
|
||||
"start_period": "2026-10",
|
||||
"end_period": null,
|
||||
"paused": false,
|
||||
"last_generated_period": null,
|
||||
"next_occurrence_date": "2026-10-25",
|
||||
"created_by_user_id": "0192a000-0000-7000-8000-000000000001",
|
||||
"updated_by_user_id": "0192a000-0000-7000-8000-000000000001",
|
||||
"created_at": "2026-10-17T06:30:02.004Z",
|
||||
"updated_at": "2026-10-17T06:30:02.004Z",
|
||||
"deleted_at": null,
|
||||
"server_version": 30
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"paused": true
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"id": "0192a004-0000-7000-8000-000000000001",
|
||||
"account_id": "0192a003-0000-7000-8000-000000000001",
|
||||
"category_id": "01920000-0000-7000-8000-000000000003",
|
||||
"amount_ore": 1250000,
|
||||
"description": "Hyra",
|
||||
"notes": "",
|
||||
"payer_user_id": "0192a000-0000-7000-8000-000000000001",
|
||||
"split_mode": "ratio",
|
||||
"splits": [
|
||||
{
|
||||
"user_id": "0192a000-0000-7000-8000-000000000001",
|
||||
"share_bp": 6000,
|
||||
"share_ore": null
|
||||
},
|
||||
{
|
||||
"user_id": "0192a000-0000-7000-8000-000000000002",
|
||||
"share_bp": 4000,
|
||||
"share_ore": null
|
||||
}
|
||||
],
|
||||
"day_of_month": 25,
|
||||
"start_period": "2026-10",
|
||||
"end_period": null,
|
||||
"paused": true,
|
||||
"last_generated_period": null,
|
||||
"next_occurrence_date": null,
|
||||
"created_by_user_id": "0192a000-0000-7000-8000-000000000001",
|
||||
"updated_by_user_id": "0192a000-0000-7000-8000-000000000001",
|
||||
"created_at": "2026-10-17T06:30:02.004Z",
|
||||
"updated_at": "2026-10-17T09:22:00.000Z",
|
||||
"deleted_at": null,
|
||||
"server_version": 52
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"code": "7K3MQX9D2HPA",
|
||||
"device_name": "Galaxy S26",
|
||||
"display_name": "Jacob"
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"token": "kvt_q7Zm0yHc1u2R9vXgB4kLw8sTn3eP6aJdF5oQ1iYbV0c",
|
||||
"user": {
|
||||
"id": "0192a000-0000-7000-8000-000000000001",
|
||||
"role": "admin",
|
||||
"display_name": "Jacob",
|
||||
"picture_version": null,
|
||||
"created_at": "2026-10-17T06:12:40.512Z",
|
||||
"updated_at": "2026-10-17T06:12:40.512Z",
|
||||
"server_version": 11
|
||||
},
|
||||
"device": {
|
||||
"id": "0192a001-0000-7000-8000-000000000001",
|
||||
"user_id": "0192a000-0000-7000-8000-000000000001",
|
||||
"name": "Galaxy S26",
|
||||
"app_version_code": 41,
|
||||
"created_at": "2026-10-17T06:12:40.512Z",
|
||||
"last_seen_at": "2026-10-17T09:00:00.000Z",
|
||||
"revoked_at": null,
|
||||
"is_current": true
|
||||
},
|
||||
"household": [
|
||||
{
|
||||
"id": "0192a000-0000-7000-8000-000000000001",
|
||||
"role": "admin",
|
||||
"display_name": "Jacob",
|
||||
"picture_version": null,
|
||||
"created_at": "2026-10-17T06:12:40.512Z",
|
||||
"updated_at": "2026-10-17T06:12:40.512Z",
|
||||
"server_version": 11
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"id": "0192a004-0000-7000-8000-000000000001",
|
||||
"account_id": "0192a003-0000-7000-8000-000000000001",
|
||||
"category_id": "01920000-0000-7000-8000-000000000003",
|
||||
"amount_ore": 1250000,
|
||||
"description": "Hyra",
|
||||
"notes": "",
|
||||
"payer_user_id": "0192a000-0000-7000-8000-000000000001",
|
||||
"split_mode": "ratio",
|
||||
"splits": [
|
||||
{
|
||||
"user_id": "0192a000-0000-7000-8000-000000000001",
|
||||
"share_bp": 6000,
|
||||
"share_ore": null
|
||||
},
|
||||
{
|
||||
"user_id": "0192a000-0000-7000-8000-000000000002",
|
||||
"share_bp": 4000,
|
||||
"share_ore": null
|
||||
}
|
||||
],
|
||||
"day_of_month": 25,
|
||||
"start_period": "2026-10",
|
||||
"end_period": null,
|
||||
"paused": false,
|
||||
"last_generated_period": null,
|
||||
"next_occurrence_date": null,
|
||||
"created_by_user_id": "0192a000-0000-7000-8000-000000000001",
|
||||
"updated_by_user_id": "0192a000-0000-7000-8000-000000000001",
|
||||
"created_at": "2026-10-17T06:30:02.004Z",
|
||||
"updated_at": "2026-10-17T09:23:00.000Z",
|
||||
"deleted_at": "2026-10-17T09:23:00.000Z",
|
||||
"server_version": 53
|
||||
}
|
||||
@@ -0,0 +1,323 @@
|
||||
{
|
||||
"epoch": "30b85fb529aac0016dc91650841005b9",
|
||||
"since": 0,
|
||||
"next_since": 921,
|
||||
"has_more": true,
|
||||
"server_time": "2026-10-25T12:00:00.000Z",
|
||||
"users": [
|
||||
{
|
||||
"id": "0192a000-0000-7000-8000-000000000001",
|
||||
"role": "admin",
|
||||
"display_name": "Jacob",
|
||||
"picture_version": null,
|
||||
"created_at": "2026-10-17T06:12:40.512Z",
|
||||
"updated_at": "2026-10-17T06:12:40.512Z",
|
||||
"server_version": 11
|
||||
},
|
||||
{
|
||||
"id": "0192a000-0000-7000-8000-000000000002",
|
||||
"role": "member",
|
||||
"display_name": "Anna",
|
||||
"picture_version": 2,
|
||||
"created_at": "2026-10-17T07:01:15.000Z",
|
||||
"updated_at": "2026-10-17T07:05:00.250Z",
|
||||
"server_version": 12
|
||||
}
|
||||
],
|
||||
"categories": [
|
||||
{
|
||||
"id": "01920000-0000-7000-8000-000000000001",
|
||||
"default_key": "groceries",
|
||||
"name": null,
|
||||
"icon": "shopping_cart",
|
||||
"color": "green",
|
||||
"sort_order": 10,
|
||||
"created_at": "2026-10-17T00:00:00.000Z",
|
||||
"updated_at": "2026-10-17T00:00:00.000Z",
|
||||
"archived_at": null,
|
||||
"deleted_at": null,
|
||||
"server_version": 1
|
||||
},
|
||||
{
|
||||
"id": "01920000-0000-7000-8000-000000000002",
|
||||
"default_key": "restaurants",
|
||||
"name": null,
|
||||
"icon": "restaurant",
|
||||
"color": "orange",
|
||||
"sort_order": 20,
|
||||
"created_at": "2026-10-17T00:00:00.000Z",
|
||||
"updated_at": "2026-10-17T00:00:00.000Z",
|
||||
"archived_at": null,
|
||||
"deleted_at": null,
|
||||
"server_version": 2
|
||||
}
|
||||
],
|
||||
"accounts": [
|
||||
{
|
||||
"id": "0192a003-0000-7000-8000-000000000001",
|
||||
"name": "Vardag",
|
||||
"default_shares": [
|
||||
{
|
||||
"user_id": "0192a000-0000-7000-8000-000000000001",
|
||||
"share_bp": 5000
|
||||
},
|
||||
{
|
||||
"user_id": "0192a000-0000-7000-8000-000000000002",
|
||||
"share_bp": 5000
|
||||
}
|
||||
],
|
||||
"created_by_user_id": "0192a000-0000-7000-8000-000000000001",
|
||||
"created_at": "2026-10-17T07:10:00.000Z",
|
||||
"updated_at": "2026-10-17T07:10:00.000Z",
|
||||
"archived_at": null,
|
||||
"deleted_at": null,
|
||||
"server_version": 13
|
||||
}
|
||||
],
|
||||
"recurring_templates": [
|
||||
{
|
||||
"id": "0192a004-0000-7000-8000-000000000001",
|
||||
"account_id": "0192a003-0000-7000-8000-000000000001",
|
||||
"category_id": "01920000-0000-7000-8000-000000000003",
|
||||
"amount_ore": 1250000,
|
||||
"description": "Hyra",
|
||||
"notes": "",
|
||||
"payer_user_id": "0192a000-0000-7000-8000-000000000001",
|
||||
"split_mode": "ratio",
|
||||
"splits": [
|
||||
{
|
||||
"user_id": "0192a000-0000-7000-8000-000000000001",
|
||||
"share_bp": 6000,
|
||||
"share_ore": null
|
||||
},
|
||||
{
|
||||
"user_id": "0192a000-0000-7000-8000-000000000002",
|
||||
"share_bp": 4000,
|
||||
"share_ore": null
|
||||
}
|
||||
],
|
||||
"day_of_month": 25,
|
||||
"start_period": "2026-10",
|
||||
"end_period": null,
|
||||
"paused": false,
|
||||
"last_generated_period": "2026-10",
|
||||
"next_occurrence_date": "2026-11-25",
|
||||
"created_by_user_id": "0192a000-0000-7000-8000-000000000001",
|
||||
"updated_by_user_id": "0192a000-0000-7000-8000-000000000001",
|
||||
"created_at": "2026-10-17T06:30:02.004Z",
|
||||
"updated_at": "2026-10-25T05:00:00.031Z",
|
||||
"deleted_at": null,
|
||||
"server_version": 921
|
||||
}
|
||||
],
|
||||
"expenses": [
|
||||
{
|
||||
"id": "01929c5e-1111-7aaa-8bbb-000000000001",
|
||||
"account_id": "0192a003-0000-7000-8000-000000000001",
|
||||
"category_id": "01920000-0000-7000-8000-000000000001",
|
||||
"amount_ore": 41200,
|
||||
"description": "ICA Maxi",
|
||||
"notes": "",
|
||||
"expense_date": "2026-10-16",
|
||||
"payer_user_id": "0192a000-0000-7000-8000-000000000001",
|
||||
"split_mode": "default",
|
||||
"splits": [
|
||||
{
|
||||
"user_id": "0192a000-0000-7000-8000-000000000001",
|
||||
"share_ore": 20600,
|
||||
"share_bp": 5000
|
||||
},
|
||||
{
|
||||
"user_id": "0192a000-0000-7000-8000-000000000002",
|
||||
"share_ore": 20600,
|
||||
"share_bp": 5000
|
||||
}
|
||||
],
|
||||
"recurring_template_id": null,
|
||||
"recurring_period": null,
|
||||
"created_by_user_id": "0192a000-0000-7000-8000-000000000001",
|
||||
"updated_by_user_id": "0192a000-0000-7000-8000-000000000001",
|
||||
"created_at": "2026-10-16T17:02:11.123Z",
|
||||
"updated_at": "2026-10-16T17:02:11.123Z",
|
||||
"deleted_at": null,
|
||||
"server_version": 913
|
||||
},
|
||||
{
|
||||
"id": "01929c5e-1111-7aaa-8bbb-000000000002",
|
||||
"account_id": "0192a003-0000-7000-8000-000000000001",
|
||||
"category_id": "01920000-0000-7000-8000-000000000002",
|
||||
"amount_ore": 1001,
|
||||
"description": "Fika",
|
||||
"notes": "Kanelbullar\noch kaffe",
|
||||
"expense_date": "2026-10-15",
|
||||
"payer_user_id": "0192a000-0000-7000-8000-000000000002",
|
||||
"split_mode": "exact",
|
||||
"splits": [
|
||||
{
|
||||
"user_id": "0192a000-0000-7000-8000-000000000001",
|
||||
"share_ore": 701,
|
||||
"share_bp": null
|
||||
},
|
||||
{
|
||||
"user_id": "0192a000-0000-7000-8000-000000000002",
|
||||
"share_ore": 300,
|
||||
"share_bp": null
|
||||
}
|
||||
],
|
||||
"recurring_template_id": null,
|
||||
"recurring_period": null,
|
||||
"created_by_user_id": "0192a000-0000-7000-8000-000000000002",
|
||||
"updated_by_user_id": "0192a000-0000-7000-8000-000000000002",
|
||||
"created_at": "2026-10-15T10:00:00.000Z",
|
||||
"updated_at": "2026-10-15T10:00:00.000Z",
|
||||
"deleted_at": null,
|
||||
"server_version": 914
|
||||
},
|
||||
{
|
||||
"id": "01929c5e-1111-7aaa-8bbb-000000000003",
|
||||
"account_id": "0192a003-0000-7000-8000-000000000001",
|
||||
"category_id": "01920000-0000-7000-8000-000000000003",
|
||||
"amount_ore": 1250000,
|
||||
"description": "Hyra",
|
||||
"notes": "",
|
||||
"expense_date": "2026-10-25",
|
||||
"payer_user_id": "0192a000-0000-7000-8000-000000000001",
|
||||
"split_mode": "ratio",
|
||||
"splits": [
|
||||
{
|
||||
"user_id": "0192a000-0000-7000-8000-000000000001",
|
||||
"share_ore": 750000,
|
||||
"share_bp": 6000
|
||||
},
|
||||
{
|
||||
"user_id": "0192a000-0000-7000-8000-000000000002",
|
||||
"share_ore": 500000,
|
||||
"share_bp": 4000
|
||||
}
|
||||
],
|
||||
"recurring_template_id": "0192a004-0000-7000-8000-000000000001",
|
||||
"recurring_period": "2026-10",
|
||||
"created_by_user_id": "0192a000-0000-7000-8000-000000000001",
|
||||
"updated_by_user_id": "0192a000-0000-7000-8000-000000000001",
|
||||
"created_at": "2026-10-25T05:00:00.031Z",
|
||||
"updated_at": "2026-10-25T05:00:00.031Z",
|
||||
"deleted_at": null,
|
||||
"server_version": 919
|
||||
}
|
||||
],
|
||||
"settlements": [
|
||||
{
|
||||
"id": "0192a005-0000-7000-8000-000000000001",
|
||||
"from_user_id": "0192a000-0000-7000-8000-000000000002",
|
||||
"to_user_id": "0192a000-0000-7000-8000-000000000001",
|
||||
"amount_ore": 50000,
|
||||
"settlement_date": "2026-10-16",
|
||||
"note": "Swish",
|
||||
"allocations": [
|
||||
{
|
||||
"account_id": "0192a003-0000-7000-8000-000000000001",
|
||||
"amount_ore": 30000
|
||||
},
|
||||
{
|
||||
"account_id": "0192a003-0000-7000-8000-000000000002",
|
||||
"amount_ore": 20000
|
||||
}
|
||||
],
|
||||
"created_by_user_id": "0192a000-0000-7000-8000-000000000002",
|
||||
"updated_by_user_id": "0192a000-0000-7000-8000-000000000002",
|
||||
"created_at": "2026-10-16T18:00:00.000Z",
|
||||
"updated_at": "2026-10-16T18:00:00.000Z",
|
||||
"deleted_at": null,
|
||||
"server_version": 915
|
||||
}
|
||||
],
|
||||
"audit_entries": [
|
||||
{
|
||||
"id": "0192a006-0000-7000-8000-000000000001",
|
||||
"entity_type": "expense",
|
||||
"entity_id": "01929c5e-1111-7aaa-8bbb-000000000001",
|
||||
"action": "create",
|
||||
"source": "app",
|
||||
"actor_user_id": "0192a000-0000-7000-8000-000000000001",
|
||||
"client_at": "2026-10-16T17:02:11.123Z",
|
||||
"server_at": "2026-10-16T17:02:12.480Z",
|
||||
"changes": [
|
||||
{
|
||||
"field": "account_id",
|
||||
"old": null,
|
||||
"new": "0192a003-0000-7000-8000-000000000001"
|
||||
},
|
||||
{
|
||||
"field": "category_id",
|
||||
"old": null,
|
||||
"new": "01920000-0000-7000-8000-000000000001"
|
||||
},
|
||||
{
|
||||
"field": "payer_user_id",
|
||||
"old": null,
|
||||
"new": "0192a000-0000-7000-8000-000000000001"
|
||||
},
|
||||
{
|
||||
"field": "amount_ore",
|
||||
"old": null,
|
||||
"new": 41200
|
||||
},
|
||||
{
|
||||
"field": "description",
|
||||
"old": null,
|
||||
"new": "ICA Maxi"
|
||||
},
|
||||
{
|
||||
"field": "notes",
|
||||
"old": null,
|
||||
"new": ""
|
||||
},
|
||||
{
|
||||
"field": "expense_date",
|
||||
"old": null,
|
||||
"new": "2026-10-16"
|
||||
},
|
||||
{
|
||||
"field": "split_mode",
|
||||
"old": null,
|
||||
"new": "default"
|
||||
},
|
||||
{
|
||||
"field": "splits",
|
||||
"old": null,
|
||||
"new": [
|
||||
{
|
||||
"user_id": "0192a000-0000-7000-8000-000000000001",
|
||||
"share_ore": 20600,
|
||||
"share_bp": 5000
|
||||
},
|
||||
{
|
||||
"user_id": "0192a000-0000-7000-8000-000000000002",
|
||||
"share_ore": 20600,
|
||||
"share_bp": 5000
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"server_version": 916
|
||||
},
|
||||
{
|
||||
"id": "0192a006-0000-7000-8000-000000000002",
|
||||
"entity_type": "settlement",
|
||||
"entity_id": "0192a005-0000-7000-8000-000000000001",
|
||||
"action": "update",
|
||||
"source": "app",
|
||||
"actor_user_id": "0192a000-0000-7000-8000-000000000002",
|
||||
"client_at": "2026-10-16T18:05:00.000Z",
|
||||
"server_at": "2026-10-16T18:05:01.200Z",
|
||||
"changes": [
|
||||
{
|
||||
"field": "note",
|
||||
"old": "",
|
||||
"new": "Swish"
|
||||
}
|
||||
],
|
||||
"server_version": 917
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
{
|
||||
"mutations": [
|
||||
{
|
||||
"mutation_id": "01929c5e-6c1f-7d3a-9b2e-4f5a6b7c8d9e",
|
||||
"entity_type": "expense",
|
||||
"entity": {
|
||||
"id": "01929c5e-1111-7aaa-8bbb-000000000001",
|
||||
"account_id": "0192a003-0000-7000-8000-000000000001",
|
||||
"category_id": "01920000-0000-7000-8000-000000000001",
|
||||
"amount_ore": 41200,
|
||||
"description": "ICA Maxi",
|
||||
"notes": "",
|
||||
"expense_date": "2026-10-16",
|
||||
"payer_user_id": "0192a000-0000-7000-8000-000000000001",
|
||||
"split_mode": "default",
|
||||
"splits": [
|
||||
{
|
||||
"user_id": "0192a000-0000-7000-8000-000000000001",
|
||||
"share_ore": 20600,
|
||||
"share_bp": 5000
|
||||
},
|
||||
{
|
||||
"user_id": "0192a000-0000-7000-8000-000000000002",
|
||||
"share_ore": 20600,
|
||||
"share_bp": 5000
|
||||
}
|
||||
],
|
||||
"created_at": "2026-10-16T17:02:11.123Z",
|
||||
"updated_at": "2026-10-16T17:02:11.123Z",
|
||||
"deleted_at": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"mutation_id": "01929c5e-6c1f-7d3a-9b2e-4f5a6b7c8d9f",
|
||||
"entity_type": "settlement",
|
||||
"entity": {
|
||||
"id": "0192a005-0000-7000-8000-000000000001",
|
||||
"from_user_id": "0192a000-0000-7000-8000-000000000002",
|
||||
"to_user_id": "0192a000-0000-7000-8000-000000000001",
|
||||
"amount_ore": 50000,
|
||||
"settlement_date": "2026-10-16",
|
||||
"note": "Swish",
|
||||
"allocations": [
|
||||
{
|
||||
"account_id": "0192a003-0000-7000-8000-000000000001",
|
||||
"amount_ore": 30000
|
||||
},
|
||||
{
|
||||
"account_id": "0192a003-0000-7000-8000-000000000002",
|
||||
"amount_ore": 20000
|
||||
}
|
||||
],
|
||||
"created_at": "2026-10-16T18:00:00.000Z",
|
||||
"updated_at": "2026-10-16T18:00:00.000Z",
|
||||
"deleted_at": null
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
{
|
||||
"server_time": "2026-10-17T12:00:00.000Z",
|
||||
"results": [
|
||||
{
|
||||
"mutation_id": "01929c5e-6c1f-7d3a-9b2e-4f5a6b7c8d9e",
|
||||
"entity_type": "expense",
|
||||
"entity_id": "01929c5e-1111-7aaa-8bbb-000000000001",
|
||||
"status": "accepted",
|
||||
"server_version": 913,
|
||||
"entity": {
|
||||
"id": "01929c5e-1111-7aaa-8bbb-000000000001",
|
||||
"account_id": "0192a003-0000-7000-8000-000000000001",
|
||||
"category_id": "01920000-0000-7000-8000-000000000001",
|
||||
"amount_ore": 41200,
|
||||
"description": "ICA Maxi",
|
||||
"notes": "",
|
||||
"expense_date": "2026-10-16",
|
||||
"payer_user_id": "0192a000-0000-7000-8000-000000000001",
|
||||
"split_mode": "default",
|
||||
"splits": [
|
||||
{
|
||||
"user_id": "0192a000-0000-7000-8000-000000000001",
|
||||
"share_ore": 20600,
|
||||
"share_bp": 5000
|
||||
},
|
||||
{
|
||||
"user_id": "0192a000-0000-7000-8000-000000000002",
|
||||
"share_ore": 20600,
|
||||
"share_bp": 5000
|
||||
}
|
||||
],
|
||||
"recurring_template_id": null,
|
||||
"recurring_period": null,
|
||||
"created_by_user_id": "0192a000-0000-7000-8000-000000000001",
|
||||
"updated_by_user_id": "0192a000-0000-7000-8000-000000000001",
|
||||
"created_at": "2026-10-16T17:02:11.123Z",
|
||||
"updated_at": "2026-10-16T17:02:11.123Z",
|
||||
"deleted_at": null,
|
||||
"server_version": 913
|
||||
}
|
||||
},
|
||||
{
|
||||
"mutation_id": "01929c5e-6c1f-7d3a-9b2e-4f5a6b7c8d9f",
|
||||
"entity_type": "settlement",
|
||||
"entity_id": "0192a005-0000-7000-8000-000000000001",
|
||||
"status": "rejected",
|
||||
"reason": "stale",
|
||||
"message": "a newer version exists",
|
||||
"current": {
|
||||
"id": "0192a005-0000-7000-8000-000000000001",
|
||||
"from_user_id": "0192a000-0000-7000-8000-000000000002",
|
||||
"to_user_id": "0192a000-0000-7000-8000-000000000001",
|
||||
"amount_ore": 50000,
|
||||
"settlement_date": "2026-10-16",
|
||||
"note": "Swish okt",
|
||||
"allocations": [
|
||||
{
|
||||
"account_id": "0192a003-0000-7000-8000-000000000001",
|
||||
"amount_ore": 30000
|
||||
},
|
||||
{
|
||||
"account_id": "0192a003-0000-7000-8000-000000000002",
|
||||
"amount_ore": 20000
|
||||
}
|
||||
],
|
||||
"created_by_user_id": "0192a000-0000-7000-8000-000000000002",
|
||||
"updated_by_user_id": "0192a000-0000-7000-8000-000000000002",
|
||||
"created_at": "2026-10-16T18:00:00.000Z",
|
||||
"updated_at": "2026-10-16T19:00:00.000Z",
|
||||
"deleted_at": null,
|
||||
"server_version": 918
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"payer_user_id": "0192a000-0000-7000-8000-000000000002",
|
||||
"amount_ore": 60000,
|
||||
"exclude_settlement_id": null
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"allocations": [
|
||||
{
|
||||
"account_id": "0192a003-0000-7000-8000-000000000001",
|
||||
"amount_ore": 30000
|
||||
},
|
||||
{
|
||||
"account_id": "0192a003-0000-7000-8000-000000000002",
|
||||
"amount_ore": 30000
|
||||
}
|
||||
],
|
||||
"excess_ore": 10000,
|
||||
"over_settled_account_ids": [
|
||||
"0192a003-0000-7000-8000-000000000002"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"version_code": 412,
|
||||
"version_name": "2026.11.02-1a2b3c4",
|
||||
"file_name": "kvitt-412.apk",
|
||||
"size_bytes": 18234567,
|
||||
"sha256": "9f2c4d1e8b7a6f5e4d3c2b1a0f9e8d7c6b5a4f3e2d1c0b9a8f7e6d5c4b3a2f1e",
|
||||
"released_at": "2026-11-02T19:40:00.000Z"
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"user": {
|
||||
"id": "0192a000-0000-7000-8000-000000000001",
|
||||
"role": "admin",
|
||||
"display_name": "Jacob",
|
||||
"picture_version": null,
|
||||
"created_at": "2026-10-17T06:12:40.512Z",
|
||||
"updated_at": "2026-10-17T06:12:40.512Z",
|
||||
"server_version": 11
|
||||
},
|
||||
"device": {
|
||||
"id": "0192a001-0000-7000-8000-000000000001",
|
||||
"user_id": "0192a000-0000-7000-8000-000000000001",
|
||||
"name": "Galaxy S26",
|
||||
"app_version_code": 41,
|
||||
"created_at": "2026-10-17T06:12:40.512Z",
|
||||
"last_seen_at": "2026-10-17T09:00:00.000Z",
|
||||
"revoked_at": null,
|
||||
"is_current": true
|
||||
},
|
||||
"household": [
|
||||
{
|
||||
"id": "0192a000-0000-7000-8000-000000000001",
|
||||
"role": "admin",
|
||||
"display_name": "Jacob",
|
||||
"picture_version": null,
|
||||
"created_at": "2026-10-17T06:12:40.512Z",
|
||||
"updated_at": "2026-10-17T06:12:40.512Z",
|
||||
"server_version": 11
|
||||
},
|
||||
{
|
||||
"id": "0192a000-0000-7000-8000-000000000002",
|
||||
"role": "member",
|
||||
"display_name": "Anna",
|
||||
"picture_version": 2,
|
||||
"created_at": "2026-10-17T07:01:15.000Z",
|
||||
"updated_at": "2026-10-17T07:05:00.250Z",
|
||||
"server_version": 12
|
||||
}
|
||||
],
|
||||
"public_url": "https://icehack.asuscomm.com:8443"
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"display_name": "Jacob H"
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"id": "0192a000-0000-7000-8000-000000000001",
|
||||
"role": "admin",
|
||||
"display_name": "Jacob H",
|
||||
"picture_version": null,
|
||||
"created_at": "2026-10-17T06:12:40.512Z",
|
||||
"updated_at": "2026-10-17T09:10:00.000Z",
|
||||
"server_version": 40
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"id": "0192a000-0000-7000-8000-000000000001",
|
||||
"role": "admin",
|
||||
"display_name": "Jacob",
|
||||
"picture_version": 1,
|
||||
"created_at": "2026-10-17T06:12:40.512Z",
|
||||
"updated_at": "2026-10-17T09:11:00.000Z",
|
||||
"server_version": 41
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"code": "account_not_empty",
|
||||
"message": "account is referenced by expenses, settlements or templates",
|
||||
"field": null
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"code": "batch_too_large",
|
||||
"message": "more than 100 mutations",
|
||||
"field": "mutations"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"code": "cannot_revoke_current_device",
|
||||
"message": "cannot revoke the calling device",
|
||||
"field": null
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"code": "category_in_use",
|
||||
"message": "category is used by expenses or templates",
|
||||
"field": null
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"code": "category_is_default",
|
||||
"message": "default categories cannot be deleted",
|
||||
"field": null
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"code": "cursor_ahead",
|
||||
"message": "since is greater than the current sequence value",
|
||||
"field": "since"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"code": "device_revoked",
|
||||
"message": "device is revoked",
|
||||
"field": null
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"code": "forbidden",
|
||||
"message": "role member may not manage invites",
|
||||
"field": null
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"code": "household_full",
|
||||
"message": "the household already has two users",
|
||||
"field": null
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"code": "household_incomplete",
|
||||
"message": "the partner has not joined yet",
|
||||
"field": null
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"code": "image_invalid",
|
||||
"message": "picture cannot be decoded",
|
||||
"field": null
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"code": "internal",
|
||||
"message": "internal error, request id 0192a0ff-0000-7000-8000-000000000001",
|
||||
"field": null
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"code": "invalid_request",
|
||||
"message": "missing field `device_name`",
|
||||
"field": "device_name"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"code": "invite_invalid",
|
||||
"message": "invite code is not valid",
|
||||
"field": null
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"code": "name_taken",
|
||||
"message": "an account with this name exists",
|
||||
"field": "name"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"code": "no_accounts",
|
||||
"message": "no_accounts",
|
||||
"field": null
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"code": "not_found",
|
||||
"message": "no such account",
|
||||
"field": null
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"code": "not_implemented",
|
||||
"message": "not implemented",
|
||||
"field": null
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"code": "payload_too_large",
|
||||
"message": "body exceeds 1 MiB",
|
||||
"field": null
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"code": "protocol_unsupported",
|
||||
"message": "X-Kvitt-Protocol must be 1",
|
||||
"field": null
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"code": "rate_limited",
|
||||
"message": "too many requests",
|
||||
"field": null
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"code": "unauthorized",
|
||||
"message": "missing or unknown bearer token",
|
||||
"field": null
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"code": "unavailable",
|
||||
"message": "database busy",
|
||||
"field": null
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"code": "unsupported_media_type",
|
||||
"message": "picture must be image/jpeg, image/png or image/webp",
|
||||
"field": null
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"code": "validation_failed",
|
||||
"message": "amount_ore must be between 1 and 1000000000",
|
||||
"field": "amount_ore"
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"message": {
|
||||
"token": "dGhpcyBpcyBhbiBleGFtcGxlIGZjbSB0b2tlbg",
|
||||
"android": {
|
||||
"priority": "high",
|
||||
"ttl": "86400s"
|
||||
},
|
||||
"data": {
|
||||
"v": "1",
|
||||
"type": "expense_created",
|
||||
"entity_type": "expense",
|
||||
"entity_id": "01929c5e-1111-7aaa-8bbb-000000000001",
|
||||
"actor_user_id": "0192a000-0000-7000-8000-000000000001",
|
||||
"count": "1"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"message": {
|
||||
"token": "dGhpcyBpcyBhbiBleGFtcGxlIGZjbSB0b2tlbg",
|
||||
"android": {
|
||||
"priority": "high",
|
||||
"ttl": "86400s"
|
||||
},
|
||||
"data": {
|
||||
"v": "1",
|
||||
"type": "expense_deleted",
|
||||
"entity_type": "expense",
|
||||
"entity_id": "01929c5e-1111-7aaa-8bbb-000000000001",
|
||||
"actor_user_id": "0192a000-0000-7000-8000-000000000001",
|
||||
"count": "1"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"message": {
|
||||
"token": "dGhpcyBpcyBhbiBleGFtcGxlIGZjbSB0b2tlbg",
|
||||
"android": {
|
||||
"priority": "high",
|
||||
"ttl": "86400s"
|
||||
},
|
||||
"data": {
|
||||
"v": "1",
|
||||
"type": "expense_updated",
|
||||
"entity_type": "expense",
|
||||
"entity_id": "01929c5e-1111-7aaa-8bbb-000000000001",
|
||||
"actor_user_id": "0192a000-0000-7000-8000-000000000002",
|
||||
"count": "1"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"message": {
|
||||
"token": "dGhpcyBpcyBhbiBleGFtcGxlIGZjbSB0b2tlbg",
|
||||
"android": {
|
||||
"priority": "high",
|
||||
"ttl": "86400s"
|
||||
},
|
||||
"data": {
|
||||
"v": "1",
|
||||
"type": "recurring_expense_created",
|
||||
"entity_type": "expense",
|
||||
"entity_id": "01929c5e-1111-7aaa-8bbb-000000000003",
|
||||
"actor_user_id": "0192a000-0000-7000-8000-000000000001",
|
||||
"count": "1"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"message": {
|
||||
"token": "dGhpcyBpcyBhbiBleGFtcGxlIGZjbSB0b2tlbg",
|
||||
"android": {
|
||||
"priority": "high",
|
||||
"ttl": "86400s"
|
||||
},
|
||||
"data": {
|
||||
"v": "1",
|
||||
"type": "settlement_created",
|
||||
"entity_type": "settlement",
|
||||
"entity_id": "0192a005-0000-7000-8000-000000000001",
|
||||
"actor_user_id": "0192a000-0000-7000-8000-000000000002",
|
||||
"count": "1"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"message": {
|
||||
"token": "dGhpcyBpcyBhbiBleGFtcGxlIGZjbSB0b2tlbg",
|
||||
"android": {
|
||||
"priority": "high",
|
||||
"ttl": "86400s"
|
||||
},
|
||||
"data": {
|
||||
"v": "1",
|
||||
"type": "settlement_deleted",
|
||||
"entity_type": "settlement",
|
||||
"entity_id": "0192a005-0000-7000-8000-000000000001",
|
||||
"actor_user_id": "0192a000-0000-7000-8000-000000000001",
|
||||
"count": "1"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"message": {
|
||||
"token": "dGhpcyBpcyBhbiBleGFtcGxlIGZjbSB0b2tlbg",
|
||||
"android": {
|
||||
"priority": "high",
|
||||
"ttl": "86400s"
|
||||
},
|
||||
"data": {
|
||||
"v": "1",
|
||||
"type": "settlement_updated",
|
||||
"entity_type": "settlement",
|
||||
"entity_id": "0192a005-0000-7000-8000-000000000001",
|
||||
"actor_user_id": "0192a000-0000-7000-8000-000000000002",
|
||||
"count": "1"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"message": {
|
||||
"token": "dGhpcyBpcyBhbiBleGFtcGxlIGZjbSB0b2tlbg",
|
||||
"android": {
|
||||
"priority": "high",
|
||||
"ttl": "86400s"
|
||||
},
|
||||
"data": {
|
||||
"v": "1",
|
||||
"type": "sync_batch",
|
||||
"entity_type": "",
|
||||
"entity_id": "",
|
||||
"actor_user_id": "0192a000-0000-7000-8000-000000000002",
|
||||
"count": "4"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"epoch": "30b85fb529aac0016dc91650841005b9",
|
||||
"since": 921,
|
||||
"next_since": 921,
|
||||
"has_more": false,
|
||||
"server_time": "2026-10-25T12:00:05.000Z",
|
||||
"users": [],
|
||||
"categories": [],
|
||||
"accounts": [],
|
||||
"recurring_templates": [],
|
||||
"expenses": [],
|
||||
"settlements": [],
|
||||
"audit_entries": []
|
||||
}
|
||||
@@ -0,0 +1,323 @@
|
||||
{
|
||||
"epoch": "30b85fb529aac0016dc91650841005b9",
|
||||
"since": 0,
|
||||
"next_since": 921,
|
||||
"has_more": true,
|
||||
"server_time": "2026-10-25T12:00:00.000Z",
|
||||
"users": [
|
||||
{
|
||||
"id": "0192a000-0000-7000-8000-000000000001",
|
||||
"role": "admin",
|
||||
"display_name": "Jacob",
|
||||
"picture_version": null,
|
||||
"created_at": "2026-10-17T06:12:40.512Z",
|
||||
"updated_at": "2026-10-17T06:12:40.512Z",
|
||||
"server_version": 11
|
||||
},
|
||||
{
|
||||
"id": "0192a000-0000-7000-8000-000000000002",
|
||||
"role": "member",
|
||||
"display_name": "Anna",
|
||||
"picture_version": 2,
|
||||
"created_at": "2026-10-17T07:01:15.000Z",
|
||||
"updated_at": "2026-10-17T07:05:00.250Z",
|
||||
"server_version": 12
|
||||
}
|
||||
],
|
||||
"categories": [
|
||||
{
|
||||
"id": "01920000-0000-7000-8000-000000000001",
|
||||
"default_key": "groceries",
|
||||
"name": null,
|
||||
"icon": "shopping_cart",
|
||||
"color": "green",
|
||||
"sort_order": 10,
|
||||
"created_at": "2026-10-17T00:00:00.000Z",
|
||||
"updated_at": "2026-10-17T00:00:00.000Z",
|
||||
"archived_at": null,
|
||||
"deleted_at": null,
|
||||
"server_version": 1
|
||||
},
|
||||
{
|
||||
"id": "01920000-0000-7000-8000-000000000002",
|
||||
"default_key": "restaurants",
|
||||
"name": null,
|
||||
"icon": "restaurant",
|
||||
"color": "orange",
|
||||
"sort_order": 20,
|
||||
"created_at": "2026-10-17T00:00:00.000Z",
|
||||
"updated_at": "2026-10-17T00:00:00.000Z",
|
||||
"archived_at": null,
|
||||
"deleted_at": null,
|
||||
"server_version": 2
|
||||
}
|
||||
],
|
||||
"accounts": [
|
||||
{
|
||||
"id": "0192a003-0000-7000-8000-000000000001",
|
||||
"name": "Vardag",
|
||||
"default_shares": [
|
||||
{
|
||||
"user_id": "0192a000-0000-7000-8000-000000000001",
|
||||
"share_bp": 5000
|
||||
},
|
||||
{
|
||||
"user_id": "0192a000-0000-7000-8000-000000000002",
|
||||
"share_bp": 5000
|
||||
}
|
||||
],
|
||||
"created_by_user_id": "0192a000-0000-7000-8000-000000000001",
|
||||
"created_at": "2026-10-17T07:10:00.000Z",
|
||||
"updated_at": "2026-10-17T07:10:00.000Z",
|
||||
"archived_at": null,
|
||||
"deleted_at": null,
|
||||
"server_version": 13
|
||||
}
|
||||
],
|
||||
"recurring_templates": [
|
||||
{
|
||||
"id": "0192a004-0000-7000-8000-000000000001",
|
||||
"account_id": "0192a003-0000-7000-8000-000000000001",
|
||||
"category_id": "01920000-0000-7000-8000-000000000003",
|
||||
"amount_ore": 1250000,
|
||||
"description": "Hyra",
|
||||
"notes": "",
|
||||
"payer_user_id": "0192a000-0000-7000-8000-000000000001",
|
||||
"split_mode": "ratio",
|
||||
"splits": [
|
||||
{
|
||||
"user_id": "0192a000-0000-7000-8000-000000000001",
|
||||
"share_bp": 6000,
|
||||
"share_ore": null
|
||||
},
|
||||
{
|
||||
"user_id": "0192a000-0000-7000-8000-000000000002",
|
||||
"share_bp": 4000,
|
||||
"share_ore": null
|
||||
}
|
||||
],
|
||||
"day_of_month": 25,
|
||||
"start_period": "2026-10",
|
||||
"end_period": null,
|
||||
"paused": false,
|
||||
"last_generated_period": "2026-10",
|
||||
"next_occurrence_date": "2026-11-25",
|
||||
"created_by_user_id": "0192a000-0000-7000-8000-000000000001",
|
||||
"updated_by_user_id": "0192a000-0000-7000-8000-000000000001",
|
||||
"created_at": "2026-10-17T06:30:02.004Z",
|
||||
"updated_at": "2026-10-25T05:00:00.031Z",
|
||||
"deleted_at": null,
|
||||
"server_version": 921
|
||||
}
|
||||
],
|
||||
"expenses": [
|
||||
{
|
||||
"id": "01929c5e-1111-7aaa-8bbb-000000000001",
|
||||
"account_id": "0192a003-0000-7000-8000-000000000001",
|
||||
"category_id": "01920000-0000-7000-8000-000000000001",
|
||||
"amount_ore": 41200,
|
||||
"description": "ICA Maxi",
|
||||
"notes": "",
|
||||
"expense_date": "2026-10-16",
|
||||
"payer_user_id": "0192a000-0000-7000-8000-000000000001",
|
||||
"split_mode": "default",
|
||||
"splits": [
|
||||
{
|
||||
"user_id": "0192a000-0000-7000-8000-000000000001",
|
||||
"share_ore": 20600,
|
||||
"share_bp": 5000
|
||||
},
|
||||
{
|
||||
"user_id": "0192a000-0000-7000-8000-000000000002",
|
||||
"share_ore": 20600,
|
||||
"share_bp": 5000
|
||||
}
|
||||
],
|
||||
"recurring_template_id": null,
|
||||
"recurring_period": null,
|
||||
"created_by_user_id": "0192a000-0000-7000-8000-000000000001",
|
||||
"updated_by_user_id": "0192a000-0000-7000-8000-000000000001",
|
||||
"created_at": "2026-10-16T17:02:11.123Z",
|
||||
"updated_at": "2026-10-16T17:02:11.123Z",
|
||||
"deleted_at": null,
|
||||
"server_version": 913
|
||||
},
|
||||
{
|
||||
"id": "01929c5e-1111-7aaa-8bbb-000000000002",
|
||||
"account_id": "0192a003-0000-7000-8000-000000000001",
|
||||
"category_id": "01920000-0000-7000-8000-000000000002",
|
||||
"amount_ore": 1001,
|
||||
"description": "Fika",
|
||||
"notes": "Kanelbullar\noch kaffe",
|
||||
"expense_date": "2026-10-15",
|
||||
"payer_user_id": "0192a000-0000-7000-8000-000000000002",
|
||||
"split_mode": "exact",
|
||||
"splits": [
|
||||
{
|
||||
"user_id": "0192a000-0000-7000-8000-000000000001",
|
||||
"share_ore": 701,
|
||||
"share_bp": null
|
||||
},
|
||||
{
|
||||
"user_id": "0192a000-0000-7000-8000-000000000002",
|
||||
"share_ore": 300,
|
||||
"share_bp": null
|
||||
}
|
||||
],
|
||||
"recurring_template_id": null,
|
||||
"recurring_period": null,
|
||||
"created_by_user_id": "0192a000-0000-7000-8000-000000000002",
|
||||
"updated_by_user_id": "0192a000-0000-7000-8000-000000000002",
|
||||
"created_at": "2026-10-15T10:00:00.000Z",
|
||||
"updated_at": "2026-10-15T10:00:00.000Z",
|
||||
"deleted_at": null,
|
||||
"server_version": 914
|
||||
},
|
||||
{
|
||||
"id": "01929c5e-1111-7aaa-8bbb-000000000003",
|
||||
"account_id": "0192a003-0000-7000-8000-000000000001",
|
||||
"category_id": "01920000-0000-7000-8000-000000000003",
|
||||
"amount_ore": 1250000,
|
||||
"description": "Hyra",
|
||||
"notes": "",
|
||||
"expense_date": "2026-10-25",
|
||||
"payer_user_id": "0192a000-0000-7000-8000-000000000001",
|
||||
"split_mode": "ratio",
|
||||
"splits": [
|
||||
{
|
||||
"user_id": "0192a000-0000-7000-8000-000000000001",
|
||||
"share_ore": 750000,
|
||||
"share_bp": 6000
|
||||
},
|
||||
{
|
||||
"user_id": "0192a000-0000-7000-8000-000000000002",
|
||||
"share_ore": 500000,
|
||||
"share_bp": 4000
|
||||
}
|
||||
],
|
||||
"recurring_template_id": "0192a004-0000-7000-8000-000000000001",
|
||||
"recurring_period": "2026-10",
|
||||
"created_by_user_id": "0192a000-0000-7000-8000-000000000001",
|
||||
"updated_by_user_id": "0192a000-0000-7000-8000-000000000001",
|
||||
"created_at": "2026-10-25T05:00:00.031Z",
|
||||
"updated_at": "2026-10-25T05:00:00.031Z",
|
||||
"deleted_at": null,
|
||||
"server_version": 919
|
||||
}
|
||||
],
|
||||
"settlements": [
|
||||
{
|
||||
"id": "0192a005-0000-7000-8000-000000000001",
|
||||
"from_user_id": "0192a000-0000-7000-8000-000000000002",
|
||||
"to_user_id": "0192a000-0000-7000-8000-000000000001",
|
||||
"amount_ore": 50000,
|
||||
"settlement_date": "2026-10-16",
|
||||
"note": "Swish",
|
||||
"allocations": [
|
||||
{
|
||||
"account_id": "0192a003-0000-7000-8000-000000000001",
|
||||
"amount_ore": 30000
|
||||
},
|
||||
{
|
||||
"account_id": "0192a003-0000-7000-8000-000000000002",
|
||||
"amount_ore": 20000
|
||||
}
|
||||
],
|
||||
"created_by_user_id": "0192a000-0000-7000-8000-000000000002",
|
||||
"updated_by_user_id": "0192a000-0000-7000-8000-000000000002",
|
||||
"created_at": "2026-10-16T18:00:00.000Z",
|
||||
"updated_at": "2026-10-16T18:00:00.000Z",
|
||||
"deleted_at": null,
|
||||
"server_version": 915
|
||||
}
|
||||
],
|
||||
"audit_entries": [
|
||||
{
|
||||
"id": "0192a006-0000-7000-8000-000000000001",
|
||||
"entity_type": "expense",
|
||||
"entity_id": "01929c5e-1111-7aaa-8bbb-000000000001",
|
||||
"action": "create",
|
||||
"source": "app",
|
||||
"actor_user_id": "0192a000-0000-7000-8000-000000000001",
|
||||
"client_at": "2026-10-16T17:02:11.123Z",
|
||||
"server_at": "2026-10-16T17:02:12.480Z",
|
||||
"changes": [
|
||||
{
|
||||
"field": "account_id",
|
||||
"old": null,
|
||||
"new": "0192a003-0000-7000-8000-000000000001"
|
||||
},
|
||||
{
|
||||
"field": "category_id",
|
||||
"old": null,
|
||||
"new": "01920000-0000-7000-8000-000000000001"
|
||||
},
|
||||
{
|
||||
"field": "payer_user_id",
|
||||
"old": null,
|
||||
"new": "0192a000-0000-7000-8000-000000000001"
|
||||
},
|
||||
{
|
||||
"field": "amount_ore",
|
||||
"old": null,
|
||||
"new": 41200
|
||||
},
|
||||
{
|
||||
"field": "description",
|
||||
"old": null,
|
||||
"new": "ICA Maxi"
|
||||
},
|
||||
{
|
||||
"field": "notes",
|
||||
"old": null,
|
||||
"new": ""
|
||||
},
|
||||
{
|
||||
"field": "expense_date",
|
||||
"old": null,
|
||||
"new": "2026-10-16"
|
||||
},
|
||||
{
|
||||
"field": "split_mode",
|
||||
"old": null,
|
||||
"new": "default"
|
||||
},
|
||||
{
|
||||
"field": "splits",
|
||||
"old": null,
|
||||
"new": [
|
||||
{
|
||||
"user_id": "0192a000-0000-7000-8000-000000000001",
|
||||
"share_ore": 20600,
|
||||
"share_bp": 5000
|
||||
},
|
||||
{
|
||||
"user_id": "0192a000-0000-7000-8000-000000000002",
|
||||
"share_ore": 20600,
|
||||
"share_bp": 5000
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"server_version": 916
|
||||
},
|
||||
{
|
||||
"id": "0192a006-0000-7000-8000-000000000002",
|
||||
"entity_type": "settlement",
|
||||
"entity_id": "0192a005-0000-7000-8000-000000000001",
|
||||
"action": "update",
|
||||
"source": "app",
|
||||
"actor_user_id": "0192a000-0000-7000-8000-000000000002",
|
||||
"client_at": "2026-10-16T18:05:00.000Z",
|
||||
"server_at": "2026-10-16T18:05:01.200Z",
|
||||
"changes": [
|
||||
{
|
||||
"field": "note",
|
||||
"old": "",
|
||||
"new": "Swish"
|
||||
}
|
||||
],
|
||||
"server_version": 917
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"mutations": [
|
||||
{
|
||||
"mutation_id": "01929c5e-6c1f-7d3a-9b2e-4f5a6b7c8da0",
|
||||
"entity_type": "expense",
|
||||
"entity": {
|
||||
"id": "01929c5e-1111-7aaa-8bbb-000000000002",
|
||||
"account_id": "0192a003-0000-7000-8000-000000000001",
|
||||
"category_id": "01920000-0000-7000-8000-000000000002",
|
||||
"amount_ore": 1001,
|
||||
"description": "Fika",
|
||||
"notes": "Kanelbullar\noch kaffe",
|
||||
"expense_date": "2026-10-15",
|
||||
"payer_user_id": "0192a000-0000-7000-8000-000000000002",
|
||||
"split_mode": "exact",
|
||||
"splits": [
|
||||
{
|
||||
"user_id": "0192a000-0000-7000-8000-000000000001",
|
||||
"share_ore": 701,
|
||||
"share_bp": null
|
||||
},
|
||||
{
|
||||
"user_id": "0192a000-0000-7000-8000-000000000002",
|
||||
"share_ore": 300,
|
||||
"share_bp": null
|
||||
}
|
||||
],
|
||||
"created_at": "2026-10-15T10:00:00.000Z",
|
||||
"updated_at": "2026-10-17T08:00:00.000Z",
|
||||
"deleted_at": "2026-10-17T08:00:00.000Z"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
{
|
||||
"mutations": [
|
||||
{
|
||||
"mutation_id": "01929c5e-6c1f-7d3a-9b2e-4f5a6b7c8da3",
|
||||
"entity_type": "expense",
|
||||
"entity": {
|
||||
"id": "01929c5e-1111-7aaa-8bbb-000000000002",
|
||||
"account_id": "0192a003-0000-7000-8000-000000000001",
|
||||
"category_id": "01920000-0000-7000-8000-000000000002",
|
||||
"amount_ore": 1001,
|
||||
"description": "Fika på stan",
|
||||
"notes": "Kanelbullar\noch kaffe",
|
||||
"expense_date": "2026-10-15",
|
||||
"payer_user_id": "0192a000-0000-7000-8000-000000000002",
|
||||
"split_mode": "exact",
|
||||
"splits": [
|
||||
{
|
||||
"user_id": "0192a000-0000-7000-8000-000000000001",
|
||||
"share_ore": 701,
|
||||
"share_bp": null
|
||||
},
|
||||
{
|
||||
"user_id": "0192a000-0000-7000-8000-000000000002",
|
||||
"share_ore": 300,
|
||||
"share_bp": null
|
||||
}
|
||||
],
|
||||
"created_at": "2026-10-15T10:00:00.000Z",
|
||||
"updated_at": "2026-10-17T09:00:00.000Z",
|
||||
"deleted_at": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"mutation_id": "01929c5e-6c1f-7d3a-9b2e-4f5a6b7c8da4",
|
||||
"entity_type": "expense",
|
||||
"entity": {
|
||||
"id": "01929c5e-1111-7aaa-8bbb-000000000004",
|
||||
"account_id": "0192a003-0000-7000-8000-000000000001",
|
||||
"category_id": "01920000-0000-7000-8000-000000000001",
|
||||
"amount_ore": 41200,
|
||||
"description": "ICA Maxi",
|
||||
"notes": "",
|
||||
"expense_date": "2026-10-16",
|
||||
"payer_user_id": "0192a000-0000-7000-8000-000000000001",
|
||||
"split_mode": "default",
|
||||
"splits": [
|
||||
{
|
||||
"user_id": "0192a000-0000-7000-8000-000000000001",
|
||||
"share_ore": 20000,
|
||||
"share_bp": 5000
|
||||
},
|
||||
{
|
||||
"user_id": "0192a000-0000-7000-8000-000000000002",
|
||||
"share_ore": 21200,
|
||||
"share_bp": 5000
|
||||
}
|
||||
],
|
||||
"created_at": "2026-10-16T17:02:11.123Z",
|
||||
"updated_at": "2026-10-16T17:02:11.123Z",
|
||||
"deleted_at": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"mutation_id": "01929c5e-6c1f-7d3a-9b2e-4f5a6b7c8da1",
|
||||
"entity_type": "expense",
|
||||
"entity": {
|
||||
"id": "01929c5e-1111-7aaa-8bbb-000000000005",
|
||||
"account_id": "0192a003-0000-7000-8000-000000000002",
|
||||
"category_id": "01920000-0000-7000-8000-000000000001",
|
||||
"amount_ore": 41200,
|
||||
"description": "ICA Maxi",
|
||||
"notes": "",
|
||||
"expense_date": "2026-10-16",
|
||||
"payer_user_id": "0192a000-0000-7000-8000-000000000001",
|
||||
"split_mode": "default",
|
||||
"splits": [
|
||||
{
|
||||
"user_id": "0192a000-0000-7000-8000-000000000001",
|
||||
"share_ore": 20600,
|
||||
"share_bp": 5000
|
||||
},
|
||||
{
|
||||
"user_id": "0192a000-0000-7000-8000-000000000002",
|
||||
"share_ore": 20600,
|
||||
"share_bp": 5000
|
||||
}
|
||||
],
|
||||
"created_at": "2026-10-16T17:02:11.123Z",
|
||||
"updated_at": "2026-10-16T17:02:11.123Z",
|
||||
"deleted_at": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"mutation_id": "01929c5e-6c1f-7d3a-9b2e-4f5a6b7c8da2",
|
||||
"entity_type": "expense",
|
||||
"entity": {
|
||||
"id": "01929c5e-1111-7aaa-8bbb-000000000006",
|
||||
"account_id": "0192a003-0000-7000-8000-000000000001",
|
||||
"category_id": "01920000-0000-7000-8000-000000000001",
|
||||
"amount_ore": 41200,
|
||||
"description": "ICA Maxi",
|
||||
"notes": "",
|
||||
"expense_date": "2026-10-16",
|
||||
"payer_user_id": "0192a000-0000-7000-8000-000000000001",
|
||||
"split_mode": "default",
|
||||
"splits": [
|
||||
{
|
||||
"user_id": "0192a000-0000-7000-8000-000000000001",
|
||||
"share_ore": 20600,
|
||||
"share_bp": 5000
|
||||
},
|
||||
{
|
||||
"user_id": "0192a000-0000-7000-8000-000000000002",
|
||||
"share_ore": 20600,
|
||||
"share_bp": 5000
|
||||
}
|
||||
],
|
||||
"created_at": "2026-10-18T12:00:00.000Z",
|
||||
"updated_at": "2026-10-18T12:00:00.000Z",
|
||||
"deleted_at": null
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
{
|
||||
"mutations": [
|
||||
{
|
||||
"mutation_id": "01929c5e-6c1f-7d3a-9b2e-4f5a6b7c8d9e",
|
||||
"entity_type": "expense",
|
||||
"entity": {
|
||||
"id": "01929c5e-1111-7aaa-8bbb-000000000001",
|
||||
"account_id": "0192a003-0000-7000-8000-000000000001",
|
||||
"category_id": "01920000-0000-7000-8000-000000000001",
|
||||
"amount_ore": 41200,
|
||||
"description": "ICA Maxi",
|
||||
"notes": "",
|
||||
"expense_date": "2026-10-16",
|
||||
"payer_user_id": "0192a000-0000-7000-8000-000000000001",
|
||||
"split_mode": "default",
|
||||
"splits": [
|
||||
{
|
||||
"user_id": "0192a000-0000-7000-8000-000000000001",
|
||||
"share_ore": 20600,
|
||||
"share_bp": 5000
|
||||
},
|
||||
{
|
||||
"user_id": "0192a000-0000-7000-8000-000000000002",
|
||||
"share_ore": 20600,
|
||||
"share_bp": 5000
|
||||
}
|
||||
],
|
||||
"created_at": "2026-10-16T17:02:11.123Z",
|
||||
"updated_at": "2026-10-16T17:02:11.123Z",
|
||||
"deleted_at": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"mutation_id": "01929c5e-6c1f-7d3a-9b2e-4f5a6b7c8d9f",
|
||||
"entity_type": "settlement",
|
||||
"entity": {
|
||||
"id": "0192a005-0000-7000-8000-000000000001",
|
||||
"from_user_id": "0192a000-0000-7000-8000-000000000002",
|
||||
"to_user_id": "0192a000-0000-7000-8000-000000000001",
|
||||
"amount_ore": 50000,
|
||||
"settlement_date": "2026-10-16",
|
||||
"note": "Swish",
|
||||
"allocations": [
|
||||
{
|
||||
"account_id": "0192a003-0000-7000-8000-000000000001",
|
||||
"amount_ore": 30000
|
||||
},
|
||||
{
|
||||
"account_id": "0192a003-0000-7000-8000-000000000002",
|
||||
"amount_ore": 20000
|
||||
}
|
||||
],
|
||||
"created_at": "2026-10-16T18:00:00.000Z",
|
||||
"updated_at": "2026-10-16T18:00:00.000Z",
|
||||
"deleted_at": null
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"server_time": "2026-10-17T08:00:01.000Z",
|
||||
"results": [
|
||||
{
|
||||
"mutation_id": "01929c5e-6c1f-7d3a-9b2e-4f5a6b7c8da0",
|
||||
"entity_type": "expense",
|
||||
"entity_id": "01929c5e-1111-7aaa-8bbb-000000000002",
|
||||
"status": "accepted",
|
||||
"server_version": 925,
|
||||
"entity": {
|
||||
"id": "01929c5e-1111-7aaa-8bbb-000000000002",
|
||||
"account_id": "0192a003-0000-7000-8000-000000000001",
|
||||
"category_id": "01920000-0000-7000-8000-000000000002",
|
||||
"amount_ore": 1001,
|
||||
"description": "Fika",
|
||||
"notes": "Kanelbullar\noch kaffe",
|
||||
"expense_date": "2026-10-15",
|
||||
"payer_user_id": "0192a000-0000-7000-8000-000000000002",
|
||||
"split_mode": "exact",
|
||||
"splits": [
|
||||
{
|
||||
"user_id": "0192a000-0000-7000-8000-000000000001",
|
||||
"share_ore": 701,
|
||||
"share_bp": null
|
||||
},
|
||||
{
|
||||
"user_id": "0192a000-0000-7000-8000-000000000002",
|
||||
"share_ore": 300,
|
||||
"share_bp": null
|
||||
}
|
||||
],
|
||||
"recurring_template_id": null,
|
||||
"recurring_period": null,
|
||||
"created_by_user_id": "0192a000-0000-7000-8000-000000000002",
|
||||
"updated_by_user_id": "0192a000-0000-7000-8000-000000000001",
|
||||
"created_at": "2026-10-15T10:00:00.000Z",
|
||||
"updated_at": "2026-10-17T08:00:00.000Z",
|
||||
"deleted_at": "2026-10-17T08:00:00.000Z",
|
||||
"server_version": 925
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
{
|
||||
"server_time": "2026-10-17T12:00:00.000Z",
|
||||
"results": [
|
||||
{
|
||||
"mutation_id": "01929c5e-6c1f-7d3a-9b2e-4f5a6b7c8da3",
|
||||
"entity_type": "expense",
|
||||
"entity_id": "01929c5e-1111-7aaa-8bbb-000000000002",
|
||||
"status": "rejected",
|
||||
"reason": "deleted",
|
||||
"message": "the row is deleted",
|
||||
"current": {
|
||||
"id": "01929c5e-1111-7aaa-8bbb-000000000002",
|
||||
"account_id": "0192a003-0000-7000-8000-000000000001",
|
||||
"category_id": "01920000-0000-7000-8000-000000000002",
|
||||
"amount_ore": 1001,
|
||||
"description": "Fika",
|
||||
"notes": "Kanelbullar\noch kaffe",
|
||||
"expense_date": "2026-10-15",
|
||||
"payer_user_id": "0192a000-0000-7000-8000-000000000002",
|
||||
"split_mode": "exact",
|
||||
"splits": [
|
||||
{
|
||||
"user_id": "0192a000-0000-7000-8000-000000000001",
|
||||
"share_ore": 701,
|
||||
"share_bp": null
|
||||
},
|
||||
{
|
||||
"user_id": "0192a000-0000-7000-8000-000000000002",
|
||||
"share_ore": 300,
|
||||
"share_bp": null
|
||||
}
|
||||
],
|
||||
"recurring_template_id": null,
|
||||
"recurring_period": null,
|
||||
"created_by_user_id": "0192a000-0000-7000-8000-000000000002",
|
||||
"updated_by_user_id": "0192a000-0000-7000-8000-000000000001",
|
||||
"created_at": "2026-10-15T10:00:00.000Z",
|
||||
"updated_at": "2026-10-17T08:00:00.000Z",
|
||||
"deleted_at": "2026-10-17T08:00:00.000Z",
|
||||
"server_version": 925
|
||||
}
|
||||
},
|
||||
{
|
||||
"mutation_id": "01929c5e-6c1f-7d3a-9b2e-4f5a6b7c8da4",
|
||||
"entity_type": "expense",
|
||||
"entity_id": "01929c5e-1111-7aaa-8bbb-000000000004",
|
||||
"status": "rejected",
|
||||
"reason": "validation_failed",
|
||||
"message": "share_ore does not match the split rule",
|
||||
"current": null
|
||||
},
|
||||
{
|
||||
"mutation_id": "01929c5e-6c1f-7d3a-9b2e-4f5a6b7c8da1",
|
||||
"entity_type": "expense",
|
||||
"entity_id": "01929c5e-1111-7aaa-8bbb-000000000005",
|
||||
"status": "rejected",
|
||||
"reason": "reference_not_found",
|
||||
"message": "account_id",
|
||||
"current": null
|
||||
},
|
||||
{
|
||||
"mutation_id": "01929c5e-6c1f-7d3a-9b2e-4f5a6b7c8da2",
|
||||
"entity_type": "expense",
|
||||
"entity_id": "01929c5e-1111-7aaa-8bbb-000000000006",
|
||||
"status": "rejected",
|
||||
"reason": "clock_skew",
|
||||
"message": "updated_at is more than 10 minutes ahead of server time",
|
||||
"current": null
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
{
|
||||
"server_time": "2026-10-17T12:00:00.000Z",
|
||||
"results": [
|
||||
{
|
||||
"mutation_id": "01929c5e-6c1f-7d3a-9b2e-4f5a6b7c8d9e",
|
||||
"entity_type": "expense",
|
||||
"entity_id": "01929c5e-1111-7aaa-8bbb-000000000001",
|
||||
"status": "accepted",
|
||||
"server_version": 913,
|
||||
"entity": {
|
||||
"id": "01929c5e-1111-7aaa-8bbb-000000000001",
|
||||
"account_id": "0192a003-0000-7000-8000-000000000001",
|
||||
"category_id": "01920000-0000-7000-8000-000000000001",
|
||||
"amount_ore": 41200,
|
||||
"description": "ICA Maxi",
|
||||
"notes": "",
|
||||
"expense_date": "2026-10-16",
|
||||
"payer_user_id": "0192a000-0000-7000-8000-000000000001",
|
||||
"split_mode": "default",
|
||||
"splits": [
|
||||
{
|
||||
"user_id": "0192a000-0000-7000-8000-000000000001",
|
||||
"share_ore": 20600,
|
||||
"share_bp": 5000
|
||||
},
|
||||
{
|
||||
"user_id": "0192a000-0000-7000-8000-000000000002",
|
||||
"share_ore": 20600,
|
||||
"share_bp": 5000
|
||||
}
|
||||
],
|
||||
"recurring_template_id": null,
|
||||
"recurring_period": null,
|
||||
"created_by_user_id": "0192a000-0000-7000-8000-000000000001",
|
||||
"updated_by_user_id": "0192a000-0000-7000-8000-000000000001",
|
||||
"created_at": "2026-10-16T17:02:11.123Z",
|
||||
"updated_at": "2026-10-16T17:02:11.123Z",
|
||||
"deleted_at": null,
|
||||
"server_version": 913
|
||||
}
|
||||
},
|
||||
{
|
||||
"mutation_id": "01929c5e-6c1f-7d3a-9b2e-4f5a6b7c8d9f",
|
||||
"entity_type": "settlement",
|
||||
"entity_id": "0192a005-0000-7000-8000-000000000001",
|
||||
"status": "rejected",
|
||||
"reason": "stale",
|
||||
"message": "a newer version exists",
|
||||
"current": {
|
||||
"id": "0192a005-0000-7000-8000-000000000001",
|
||||
"from_user_id": "0192a000-0000-7000-8000-000000000002",
|
||||
"to_user_id": "0192a000-0000-7000-8000-000000000001",
|
||||
"amount_ore": 50000,
|
||||
"settlement_date": "2026-10-16",
|
||||
"note": "Swish okt",
|
||||
"allocations": [
|
||||
{
|
||||
"account_id": "0192a003-0000-7000-8000-000000000001",
|
||||
"amount_ore": 30000
|
||||
},
|
||||
{
|
||||
"account_id": "0192a003-0000-7000-8000-000000000002",
|
||||
"amount_ore": 20000
|
||||
}
|
||||
],
|
||||
"created_by_user_id": "0192a000-0000-7000-8000-000000000002",
|
||||
"updated_by_user_id": "0192a000-0000-7000-8000-000000000002",
|
||||
"created_at": "2026-10-16T18:00:00.000Z",
|
||||
"updated_at": "2026-10-16T19:00:00.000Z",
|
||||
"deleted_at": null,
|
||||
"server_version": 918
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,230 @@
|
||||
-- Kvitt server schema v1. Do not edit; add a new migration and bump the schema version (SPEC §14.8).
|
||||
|
||||
CREATE TABLE meta (
|
||||
key TEXT PRIMARY KEY NOT NULL,
|
||||
value TEXT NOT NULL
|
||||
) STRICT;
|
||||
|
||||
-- One global, strictly increasing counter for server_version. Exactly one row.
|
||||
CREATE TABLE sync_sequence (
|
||||
id INTEGER PRIMARY KEY CHECK (id = 1),
|
||||
value INTEGER NOT NULL
|
||||
) STRICT;
|
||||
|
||||
CREATE TABLE users (
|
||||
id TEXT PRIMARY KEY NOT NULL,
|
||||
role TEXT NOT NULL CHECK (role IN ('admin', 'member')),
|
||||
display_name TEXT NOT NULL CHECK (length(display_name) BETWEEN 1 AND 40),
|
||||
picture_version INTEGER CHECK (picture_version IS NULL OR picture_version >= 1),
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL,
|
||||
server_version INTEGER NOT NULL UNIQUE
|
||||
) STRICT;
|
||||
CREATE UNIQUE INDEX users_single_admin ON users (role) WHERE role = 'admin';
|
||||
|
||||
CREATE TABLE devices (
|
||||
id TEXT PRIMARY KEY NOT NULL,
|
||||
user_id TEXT NOT NULL REFERENCES users (id),
|
||||
name TEXT NOT NULL CHECK (length(name) BETWEEN 1 AND 60),
|
||||
token_hash TEXT NOT NULL UNIQUE,
|
||||
fcm_token TEXT,
|
||||
app_version_code INTEGER,
|
||||
created_at TEXT NOT NULL,
|
||||
last_seen_at TEXT NOT NULL,
|
||||
revoked_at TEXT,
|
||||
revoked_by_user_id TEXT REFERENCES users (id)
|
||||
) STRICT;
|
||||
CREATE INDEX devices_user ON devices (user_id);
|
||||
|
||||
CREATE TABLE invites (
|
||||
id TEXT PRIMARY KEY NOT NULL,
|
||||
kind TEXT NOT NULL CHECK (kind IN ('admin_setup', 'partner', 'device')),
|
||||
code_hash TEXT NOT NULL UNIQUE,
|
||||
target_user_id TEXT REFERENCES users (id),
|
||||
created_by_user_id TEXT REFERENCES users (id),
|
||||
created_at TEXT NOT NULL,
|
||||
expires_at TEXT NOT NULL,
|
||||
accepted_at TEXT,
|
||||
accepted_device_id TEXT REFERENCES devices (id),
|
||||
cancelled_at TEXT,
|
||||
CHECK ((kind = 'device') = (target_user_id IS NOT NULL)),
|
||||
CHECK ((kind = 'admin_setup') = (created_by_user_id IS NULL) OR kind = 'device')
|
||||
) STRICT;
|
||||
|
||||
CREATE TABLE accounts (
|
||||
id TEXT PRIMARY KEY NOT NULL,
|
||||
name TEXT NOT NULL CHECK (length(name) BETWEEN 1 AND 60),
|
||||
name_key TEXT NOT NULL,
|
||||
created_by_user_id TEXT NOT NULL REFERENCES users (id),
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL,
|
||||
archived_at TEXT,
|
||||
deleted_at TEXT,
|
||||
server_version INTEGER NOT NULL UNIQUE
|
||||
) STRICT;
|
||||
-- name_key = Unicode-lowercased, trimmed name, computed in Rust.
|
||||
CREATE UNIQUE INDEX accounts_name_live ON accounts (name_key) WHERE deleted_at IS NULL;
|
||||
|
||||
CREATE TABLE account_default_shares (
|
||||
account_id TEXT NOT NULL REFERENCES accounts (id),
|
||||
user_id TEXT NOT NULL REFERENCES users (id),
|
||||
share_bp INTEGER NOT NULL CHECK (share_bp BETWEEN 0 AND 10000),
|
||||
PRIMARY KEY (account_id, user_id)
|
||||
) STRICT, WITHOUT ROWID;
|
||||
|
||||
CREATE TABLE categories (
|
||||
id TEXT PRIMARY KEY NOT NULL,
|
||||
default_key TEXT UNIQUE CHECK (default_key IS NULL OR default_key IN
|
||||
('groceries', 'restaurants', 'home', 'utilities', 'transport',
|
||||
'travel', 'entertainment', 'health', 'shopping', 'other')),
|
||||
name TEXT CHECK (name IS NULL OR length(name) BETWEEN 1 AND 40),
|
||||
icon TEXT NOT NULL,
|
||||
color TEXT NOT NULL,
|
||||
sort_order INTEGER NOT NULL,
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL,
|
||||
archived_at TEXT,
|
||||
deleted_at TEXT,
|
||||
server_version INTEGER NOT NULL UNIQUE,
|
||||
CHECK (name IS NOT NULL OR default_key IS NOT NULL)
|
||||
) STRICT;
|
||||
|
||||
CREATE TABLE recurring_templates (
|
||||
id TEXT PRIMARY KEY NOT NULL,
|
||||
account_id TEXT NOT NULL REFERENCES accounts (id),
|
||||
category_id TEXT NOT NULL REFERENCES categories (id),
|
||||
amount_ore INTEGER NOT NULL CHECK (amount_ore BETWEEN 1 AND 1000000000),
|
||||
description TEXT NOT NULL CHECK (length(description) BETWEEN 1 AND 100),
|
||||
notes TEXT NOT NULL DEFAULT '' CHECK (length(notes) <= 1000),
|
||||
payer_user_id TEXT NOT NULL REFERENCES users (id),
|
||||
split_mode TEXT NOT NULL CHECK (split_mode IN ('default', 'ratio', 'exact', 'full')),
|
||||
day_of_month INTEGER NOT NULL CHECK (day_of_month BETWEEN 1 AND 31),
|
||||
start_period TEXT NOT NULL,
|
||||
end_period TEXT,
|
||||
paused INTEGER NOT NULL DEFAULT 0 CHECK (paused IN (0, 1)),
|
||||
last_generated_period TEXT,
|
||||
created_by_user_id TEXT NOT NULL REFERENCES users (id),
|
||||
updated_by_user_id TEXT NOT NULL REFERENCES users (id),
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL,
|
||||
deleted_at TEXT,
|
||||
server_version INTEGER NOT NULL UNIQUE
|
||||
) STRICT;
|
||||
CREATE INDEX recurring_templates_account ON recurring_templates (account_id);
|
||||
|
||||
-- Modes ratio and full: share_bp set, share_ore NULL. Mode exact: share_ore set, share_bp NULL. Mode default: no rows.
|
||||
CREATE TABLE recurring_template_splits (
|
||||
template_id TEXT NOT NULL REFERENCES recurring_templates (id),
|
||||
user_id TEXT NOT NULL REFERENCES users (id),
|
||||
share_bp INTEGER CHECK (share_bp IS NULL OR share_bp BETWEEN 0 AND 10000),
|
||||
share_ore INTEGER CHECK (share_ore IS NULL OR share_ore >= 0),
|
||||
PRIMARY KEY (template_id, user_id),
|
||||
CHECK ((share_bp IS NULL) <> (share_ore IS NULL))
|
||||
) STRICT, WITHOUT ROWID;
|
||||
|
||||
CREATE TABLE expenses (
|
||||
id TEXT PRIMARY KEY NOT NULL,
|
||||
account_id TEXT NOT NULL REFERENCES accounts (id),
|
||||
category_id TEXT NOT NULL REFERENCES categories (id),
|
||||
amount_ore INTEGER NOT NULL CHECK (amount_ore BETWEEN 1 AND 1000000000),
|
||||
description TEXT NOT NULL CHECK (length(description) BETWEEN 1 AND 100),
|
||||
notes TEXT NOT NULL DEFAULT '' CHECK (length(notes) <= 1000),
|
||||
expense_date TEXT NOT NULL,
|
||||
payer_user_id TEXT NOT NULL REFERENCES users (id),
|
||||
split_mode TEXT NOT NULL CHECK (split_mode IN ('default', 'ratio', 'exact', 'full')),
|
||||
recurring_template_id TEXT REFERENCES recurring_templates (id),
|
||||
recurring_period TEXT,
|
||||
import_key TEXT UNIQUE,
|
||||
created_by_user_id TEXT NOT NULL REFERENCES users (id),
|
||||
updated_by_user_id TEXT NOT NULL REFERENCES users (id),
|
||||
updated_by_device_id TEXT REFERENCES devices (id),
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL,
|
||||
deleted_at TEXT,
|
||||
server_received_at TEXT NOT NULL,
|
||||
server_version INTEGER NOT NULL UNIQUE,
|
||||
CHECK ((recurring_template_id IS NULL) = (recurring_period IS NULL))
|
||||
) STRICT;
|
||||
CREATE UNIQUE INDEX expenses_recurring_once ON expenses (recurring_template_id, recurring_period)
|
||||
WHERE recurring_template_id IS NOT NULL;
|
||||
CREATE INDEX expenses_account ON expenses (account_id);
|
||||
CREATE INDEX expenses_category ON expenses (category_id);
|
||||
|
||||
-- Exactly two rows per expense, one per household user. share_bp NULL for mode exact.
|
||||
CREATE TABLE expense_splits (
|
||||
expense_id TEXT NOT NULL REFERENCES expenses (id),
|
||||
user_id TEXT NOT NULL REFERENCES users (id),
|
||||
share_ore INTEGER NOT NULL CHECK (share_ore >= 0),
|
||||
share_bp INTEGER CHECK (share_bp IS NULL OR share_bp BETWEEN 0 AND 10000),
|
||||
PRIMARY KEY (expense_id, user_id)
|
||||
) STRICT, WITHOUT ROWID;
|
||||
|
||||
CREATE TABLE settlements (
|
||||
id TEXT PRIMARY KEY NOT NULL,
|
||||
from_user_id TEXT NOT NULL REFERENCES users (id),
|
||||
to_user_id TEXT NOT NULL REFERENCES users (id),
|
||||
amount_ore INTEGER NOT NULL CHECK (amount_ore BETWEEN 1 AND 1000000000),
|
||||
settlement_date TEXT NOT NULL,
|
||||
note TEXT NOT NULL DEFAULT '' CHECK (length(note) <= 200),
|
||||
import_key TEXT UNIQUE,
|
||||
created_by_user_id TEXT NOT NULL REFERENCES users (id),
|
||||
updated_by_user_id TEXT NOT NULL REFERENCES users (id),
|
||||
updated_by_device_id TEXT REFERENCES devices (id),
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL,
|
||||
deleted_at TEXT,
|
||||
server_received_at TEXT NOT NULL,
|
||||
server_version INTEGER NOT NULL UNIQUE,
|
||||
CHECK (from_user_id <> to_user_id)
|
||||
) STRICT;
|
||||
|
||||
CREATE TABLE settlement_allocations (
|
||||
settlement_id TEXT NOT NULL REFERENCES settlements (id),
|
||||
account_id TEXT NOT NULL REFERENCES accounts (id),
|
||||
amount_ore INTEGER NOT NULL CHECK (amount_ore >= 1),
|
||||
PRIMARY KEY (settlement_id, account_id)
|
||||
) STRICT, WITHOUT ROWID;
|
||||
CREATE INDEX settlement_allocations_account ON settlement_allocations (account_id);
|
||||
|
||||
-- Append-only. changes is a JSON array of {"field","old","new"} (SPEC §14.4.7).
|
||||
CREATE TABLE audit_log (
|
||||
id TEXT PRIMARY KEY NOT NULL,
|
||||
entity_type TEXT NOT NULL CHECK (entity_type IN ('expense', 'settlement')),
|
||||
entity_id TEXT NOT NULL,
|
||||
action TEXT NOT NULL CHECK (action IN ('create', 'update', 'delete')),
|
||||
source TEXT NOT NULL CHECK (source IN ('app', 'recurring', 'import')),
|
||||
actor_user_id TEXT NOT NULL REFERENCES users (id),
|
||||
actor_device_id TEXT REFERENCES devices (id),
|
||||
client_at TEXT NOT NULL,
|
||||
server_at TEXT NOT NULL,
|
||||
changes TEXT NOT NULL,
|
||||
server_version INTEGER NOT NULL UNIQUE
|
||||
) STRICT;
|
||||
CREATE INDEX audit_log_entity ON audit_log (entity_type, entity_id);
|
||||
|
||||
-- Idempotency of POST /sync. Rows older than 90 days are pruned daily.
|
||||
CREATE TABLE sync_mutations (
|
||||
mutation_id TEXT PRIMARY KEY NOT NULL,
|
||||
device_id TEXT NOT NULL REFERENCES devices (id),
|
||||
entity_type TEXT NOT NULL CHECK (entity_type IN ('expense', 'settlement')),
|
||||
entity_id TEXT NOT NULL,
|
||||
result TEXT NOT NULL,
|
||||
received_at TEXT NOT NULL
|
||||
) STRICT;
|
||||
CREATE INDEX sync_mutations_received ON sync_mutations (received_at);
|
||||
|
||||
INSERT INTO sync_sequence (id, value) VALUES (1, 10);
|
||||
INSERT INTO meta (key, value) VALUES ('schema_version', '1');
|
||||
INSERT INTO meta (key, value) VALUES ('sync_epoch', lower(hex(randomblob(16))));
|
||||
|
||||
INSERT INTO categories (id, default_key, name, icon, color, sort_order, created_at, updated_at, archived_at, deleted_at, server_version) VALUES
|
||||
('01920000-0000-7000-8000-000000000001', 'groceries', NULL, 'shopping_cart', 'green', 10, '2026-10-17T00:00:00.000Z', '2026-10-17T00:00:00.000Z', NULL, NULL, 1),
|
||||
('01920000-0000-7000-8000-000000000002', 'restaurants', NULL, 'restaurant', 'orange', 20, '2026-10-17T00:00:00.000Z', '2026-10-17T00:00:00.000Z', NULL, NULL, 2),
|
||||
('01920000-0000-7000-8000-000000000003', 'home', NULL, 'home', 'brown', 30, '2026-10-17T00:00:00.000Z', '2026-10-17T00:00:00.000Z', NULL, NULL, 3),
|
||||
('01920000-0000-7000-8000-000000000004', 'utilities', NULL, 'bolt', 'yellow', 40, '2026-10-17T00:00:00.000Z', '2026-10-17T00:00:00.000Z', NULL, NULL, 4),
|
||||
('01920000-0000-7000-8000-000000000005', 'transport', NULL, 'directions_car', 'blue', 50, '2026-10-17T00:00:00.000Z', '2026-10-17T00:00:00.000Z', NULL, NULL, 5),
|
||||
('01920000-0000-7000-8000-000000000006', 'travel', NULL, 'flight', 'teal', 60, '2026-10-17T00:00:00.000Z', '2026-10-17T00:00:00.000Z', NULL, NULL, 6),
|
||||
('01920000-0000-7000-8000-000000000007', 'entertainment', NULL, 'movie', 'purple', 70, '2026-10-17T00:00:00.000Z', '2026-10-17T00:00:00.000Z', NULL, NULL, 7),
|
||||
('01920000-0000-7000-8000-000000000008', 'health', NULL, 'medical_services', 'red', 80, '2026-10-17T00:00:00.000Z', '2026-10-17T00:00:00.000Z', NULL, NULL, 8),
|
||||
('01920000-0000-7000-8000-000000000009', 'shopping', NULL, 'shopping_bag', 'pink', 90, '2026-10-17T00:00:00.000Z', '2026-10-17T00:00:00.000Z', NULL, NULL, 9),
|
||||
('01920000-0000-7000-8000-00000000000a', 'other', NULL, 'category', 'grey', 100, '2026-10-17T00:00:00.000Z', '2026-10-17T00:00:00.000Z', NULL, NULL, 10);
|
||||
@@ -0,0 +1,141 @@
|
||||
{
|
||||
"format": 1,
|
||||
"function": "compute_balances",
|
||||
"cases": [
|
||||
{
|
||||
"name": "empty: no accounts, no rows",
|
||||
"input": { "user_ids": ["a", "b"], "account_ids": [], "expenses": [], "settlements": [] },
|
||||
"expected": { "ok": { "per_account": {}, "global": { "a": 0, "b": 0 } } }
|
||||
},
|
||||
{
|
||||
"name": "one account, (1001, payer a, 50/50) and (100, payer b, a 6000 bp): a +440, b -440",
|
||||
"input": {
|
||||
"user_ids": ["a", "b"], "account_ids": ["x"],
|
||||
"expenses": [
|
||||
{ "account_id": "x", "amount_ore": 1001, "payer_user_id": "a", "shares": { "a": 501, "b": 500 } },
|
||||
{ "account_id": "x", "amount_ore": 100, "payer_user_id": "b", "shares": { "a": 60, "b": 40 } }
|
||||
],
|
||||
"settlements": []
|
||||
},
|
||||
"expected": { "ok": { "per_account": { "x": { "a": 440, "b": -440 } }, "global": { "a": 440, "b": -440 } } }
|
||||
},
|
||||
{
|
||||
"name": "same account plus settlement b -> a 440: all zero",
|
||||
"input": {
|
||||
"user_ids": ["a", "b"], "account_ids": ["x"],
|
||||
"expenses": [
|
||||
{ "account_id": "x", "amount_ore": 1001, "payer_user_id": "a", "shares": { "a": 501, "b": 500 } },
|
||||
{ "account_id": "x", "amount_ore": 100, "payer_user_id": "b", "shares": { "a": 60, "b": 40 } }
|
||||
],
|
||||
"settlements": [ { "from_user_id": "b", "to_user_id": "a", "allocations": { "x": 440 } } ]
|
||||
},
|
||||
"expected": { "ok": { "per_account": { "x": { "a": 0, "b": 0 } }, "global": { "a": 0, "b": 0 } } }
|
||||
},
|
||||
{
|
||||
"name": "two accounts with opposite signs: global is the sum",
|
||||
"input": {
|
||||
"user_ids": ["a", "b"], "account_ids": ["x", "y"],
|
||||
"expenses": [
|
||||
{ "account_id": "x", "amount_ore": 2000, "payer_user_id": "a", "shares": { "a": 1000, "b": 1000 } },
|
||||
{ "account_id": "y", "amount_ore": 600, "payer_user_id": "b", "shares": { "a": 300, "b": 300 } }
|
||||
],
|
||||
"settlements": []
|
||||
},
|
||||
"expected": { "ok": { "per_account": { "x": { "a": 1000, "b": -1000 }, "y": { "a": -300, "b": 300 } }, "global": { "a": 700, "b": -700 } } }
|
||||
},
|
||||
{
|
||||
"name": "an account in account_ids with no rows has zeros",
|
||||
"input": {
|
||||
"user_ids": ["a", "b"], "account_ids": ["x", "y"],
|
||||
"expenses": [ { "account_id": "x", "amount_ore": 1001, "payer_user_id": "a", "shares": { "a": 501, "b": 500 } } ],
|
||||
"settlements": []
|
||||
},
|
||||
"expected": { "ok": { "per_account": { "x": { "a": 500, "b": -500 }, "y": { "a": 0, "b": 0 } }, "global": { "a": 500, "b": -500 } } }
|
||||
},
|
||||
{
|
||||
"name": "archived accounts are included: the archived account is in account_ids and counts toward global",
|
||||
"input": {
|
||||
"user_ids": ["a", "b"], "account_ids": ["live", "arch"],
|
||||
"expenses": [
|
||||
{ "account_id": "live", "amount_ore": 1000, "payer_user_id": "a", "shares": { "a": 500, "b": 500 } },
|
||||
{ "account_id": "arch", "amount_ore": 5000, "payer_user_id": "b", "shares": { "a": 2500, "b": 2500 } }
|
||||
],
|
||||
"settlements": []
|
||||
},
|
||||
"expected": { "ok": { "per_account": { "live": { "a": 500, "b": -500 }, "arch": { "a": -2500, "b": 2500 } }, "global": { "a": -2000, "b": 2000 } } }
|
||||
},
|
||||
{
|
||||
"name": "account ids referenced by rows but not in account_ids still count",
|
||||
"input": {
|
||||
"user_ids": ["a", "b"], "account_ids": ["x"],
|
||||
"expenses": [ { "account_id": "z", "amount_ore": 100, "payer_user_id": "b", "shares": { "a": 100, "b": 0 } } ],
|
||||
"settlements": [ { "from_user_id": "a", "to_user_id": "b", "allocations": { "w": 30 } } ]
|
||||
},
|
||||
"expected": { "ok": { "per_account": { "x": { "a": 0, "b": 0 }, "z": { "a": -100, "b": 100 }, "w": { "a": 30, "b": -30 } }, "global": { "a": -70, "b": 70 } } }
|
||||
},
|
||||
{
|
||||
"name": "settlement with allocations to two accounts",
|
||||
"input": {
|
||||
"user_ids": ["a", "b"], "account_ids": ["x", "y"],
|
||||
"expenses": [
|
||||
{ "account_id": "x", "amount_ore": 10000, "payer_user_id": "a", "shares": { "a": 5000, "b": 5000 } },
|
||||
{ "account_id": "y", "amount_ore": 3000, "payer_user_id": "a", "shares": { "a": 0, "b": 3000 } }
|
||||
],
|
||||
"settlements": [ { "from_user_id": "b", "to_user_id": "a", "allocations": { "x": 5000, "y": 1000 } } ]
|
||||
},
|
||||
"expected": { "ok": { "per_account": { "x": { "a": 0, "b": 0 }, "y": { "a": 2000, "b": -2000 } }, "global": { "a": 2000, "b": -2000 } } }
|
||||
},
|
||||
{
|
||||
"name": "over-settling flips the account's sign",
|
||||
"input": {
|
||||
"user_ids": ["a", "b"], "account_ids": ["x"],
|
||||
"expenses": [ { "account_id": "x", "amount_ore": 1000, "payer_user_id": "a", "shares": { "a": 500, "b": 500 } } ],
|
||||
"settlements": [ { "from_user_id": "b", "to_user_id": "a", "allocations": { "x": 800 } } ]
|
||||
},
|
||||
"expected": { "ok": { "per_account": { "x": { "a": -300, "b": 300 } }, "global": { "a": -300, "b": 300 } } }
|
||||
},
|
||||
{
|
||||
"name": "partial settlement in the other direction",
|
||||
"input": {
|
||||
"user_ids": ["a", "b"], "account_ids": ["x"],
|
||||
"expenses": [ { "account_id": "x", "amount_ore": 1000, "payer_user_id": "b", "shares": { "a": 500, "b": 500 } } ],
|
||||
"settlements": [ { "from_user_id": "a", "to_user_id": "b", "allocations": { "x": 200 } } ]
|
||||
},
|
||||
"expected": { "ok": { "per_account": { "x": { "a": -300, "b": 300 } }, "global": { "a": -300, "b": 300 } } }
|
||||
},
|
||||
{
|
||||
"name": "expense fully borne by the payer leaves the balance unchanged",
|
||||
"input": {
|
||||
"user_ids": ["a", "b"], "account_ids": ["x"],
|
||||
"expenses": [
|
||||
{ "account_id": "x", "amount_ore": 1001, "payer_user_id": "a", "shares": { "a": 501, "b": 500 } },
|
||||
{ "account_id": "x", "amount_ore": 7000, "payer_user_id": "a", "shares": { "a": 7000, "b": 0 } }
|
||||
],
|
||||
"settlements": []
|
||||
},
|
||||
"expected": { "ok": { "per_account": { "x": { "a": 500, "b": -500 } }, "global": { "a": 500, "b": -500 } } }
|
||||
},
|
||||
{
|
||||
"name": "tombstoned rows excluded: callers pass only non-deleted rows (§14.5.2); the account's deleted expense and deleted settlement are absent from the input",
|
||||
"input": {
|
||||
"user_ids": ["a", "b"], "account_ids": ["x"],
|
||||
"expenses": [ { "account_id": "x", "amount_ore": 2000, "payer_user_id": "b", "shares": { "a": 1000, "b": 1000 } } ],
|
||||
"settlements": []
|
||||
},
|
||||
"expected": { "ok": { "per_account": { "x": { "a": -1000, "b": 1000 } }, "global": { "a": -1000, "b": 1000 } } }
|
||||
},
|
||||
{
|
||||
"name": "maximum amounts stay exact in 64-bit integers",
|
||||
"input": {
|
||||
"user_ids": ["a", "b"], "account_ids": ["x"],
|
||||
"expenses": [
|
||||
{ "account_id": "x", "amount_ore": 1000000000, "payer_user_id": "a", "shares": { "a": 0, "b": 1000000000 } },
|
||||
{ "account_id": "x", "amount_ore": 1000000000, "payer_user_id": "a", "shares": { "a": 0, "b": 1000000000 } },
|
||||
{ "account_id": "x", "amount_ore": 1000000000, "payer_user_id": "a", "shares": { "a": 0, "b": 1000000000 } }
|
||||
],
|
||||
"settlements": []
|
||||
},
|
||||
"expected": { "ok": { "per_account": { "x": { "a": 3000000000, "b": -3000000000 } }, "global": { "a": 3000000000, "b": -3000000000 } } }
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,216 @@
|
||||
{
|
||||
"format": 1,
|
||||
"function": "compute_split",
|
||||
"cases": [
|
||||
{
|
||||
"name": "§9.1 row 1: 1001, payer a, 5000/5000: payer gets the extra öre",
|
||||
"input": { "amount_ore": 1001, "payer_user_id": "a", "user_ids": ["a", "b"], "spec": { "mode": "ratio", "bp": { "a": 5000, "b": 5000 } } },
|
||||
"expected": { "ok": { "a": 501, "b": 500 } }
|
||||
},
|
||||
{
|
||||
"name": "§9.1 row 2: 1, payer a, 5000/5000: the single öre goes to the payer",
|
||||
"input": { "amount_ore": 1, "payer_user_id": "a", "user_ids": ["a", "b"], "spec": { "mode": "ratio", "bp": { "a": 5000, "b": 5000 } } },
|
||||
"expected": { "ok": { "a": 1, "b": 0 } }
|
||||
},
|
||||
{
|
||||
"name": "§9.1 row 3: 100, payer b, 4000/6000",
|
||||
"input": { "amount_ore": 100, "payer_user_id": "b", "user_ids": ["a", "b"], "spec": { "mode": "ratio", "bp": { "a": 6000, "b": 4000 } } },
|
||||
"expected": { "ok": { "a": 60, "b": 40 } }
|
||||
},
|
||||
{
|
||||
"name": "§9.1 row 4: 999, payer a, 3333/6667",
|
||||
"input": { "amount_ore": 999, "payer_user_id": "a", "user_ids": ["a", "b"], "spec": { "mode": "ratio", "bp": { "a": 3333, "b": 6667 } } },
|
||||
"expected": { "ok": { "a": 333, "b": 666 } }
|
||||
},
|
||||
{
|
||||
"name": "§9.1 row 5: 999, payer b, 6667/3333",
|
||||
"input": { "amount_ore": 999, "payer_user_id": "b", "user_ids": ["a", "b"], "spec": { "mode": "ratio", "bp": { "a": 3333, "b": 6667 } } },
|
||||
"expected": { "ok": { "a": 332, "b": 667 } }
|
||||
},
|
||||
{
|
||||
"name": "§9.1 row 6: 50000, payer a, 0/10000",
|
||||
"input": { "amount_ore": 50000, "payer_user_id": "a", "user_ids": ["a", "b"], "spec": { "mode": "ratio", "bp": { "a": 0, "b": 10000 } } },
|
||||
"expected": { "ok": { "a": 0, "b": 50000 } }
|
||||
},
|
||||
{
|
||||
"name": "§9.1 row 7: 50000, payer a, 10000/0",
|
||||
"input": { "amount_ore": 50000, "payer_user_id": "a", "user_ids": ["a", "b"], "spec": { "mode": "ratio", "bp": { "a": 10000, "b": 0 } } },
|
||||
"expected": { "ok": { "a": 50000, "b": 0 } }
|
||||
},
|
||||
{
|
||||
"name": "user_ids order does not matter: payer b listed second, remainder still to payer",
|
||||
"input": { "amount_ore": 1001, "payer_user_id": "b", "user_ids": ["a", "b"], "spec": { "mode": "ratio", "bp": { "a": 5000, "b": 5000 } } },
|
||||
"expected": { "ok": { "a": 500, "b": 501 } }
|
||||
},
|
||||
{
|
||||
"name": "user_ids given in reverse order",
|
||||
"input": { "amount_ore": 999, "payer_user_id": "a", "user_ids": ["b", "a"], "spec": { "mode": "ratio", "bp": { "a": 3333, "b": 6667 } } },
|
||||
"expected": { "ok": { "a": 333, "b": 666 } }
|
||||
},
|
||||
{
|
||||
"name": "non-payer exact share 0.9 öre: rounded down to 0, payer bears the whole amount",
|
||||
"input": { "amount_ore": 9, "payer_user_id": "a", "user_ids": ["a", "b"], "spec": { "mode": "ratio", "bp": { "a": 9000, "b": 1000 } } },
|
||||
"expected": { "ok": { "a": 9, "b": 0 } }
|
||||
},
|
||||
{
|
||||
"name": "full on the payer",
|
||||
"input": { "amount_ore": 1001, "payer_user_id": "a", "user_ids": ["a", "b"], "spec": { "mode": "full", "bearer_user_id": "a" } },
|
||||
"expected": { "ok": { "a": 1001, "b": 0 } }
|
||||
},
|
||||
{
|
||||
"name": "full on the non-payer",
|
||||
"input": { "amount_ore": 1001, "payer_user_id": "a", "user_ids": ["a", "b"], "spec": { "mode": "full", "bearer_user_id": "b" } },
|
||||
"expected": { "ok": { "a": 0, "b": 1001 } }
|
||||
},
|
||||
{
|
||||
"name": "exact valid: shares sum exactly to the amount",
|
||||
"input": { "amount_ore": 1001, "payer_user_id": "a", "user_ids": ["a", "b"], "spec": { "mode": "exact", "ore": { "a": 300, "b": 701 } } },
|
||||
"expected": { "ok": { "a": 300, "b": 701 } }
|
||||
},
|
||||
{
|
||||
"name": "exact valid with a zero share",
|
||||
"input": { "amount_ore": 41200, "payer_user_id": "b", "user_ids": ["a", "b"], "spec": { "mode": "exact", "ore": { "a": 41200, "b": 0 } } },
|
||||
"expected": { "ok": { "a": 41200, "b": 0 } }
|
||||
},
|
||||
{
|
||||
"name": "exact sum mismatch: one öre short",
|
||||
"input": { "amount_ore": 1001, "payer_user_id": "a", "user_ids": ["a", "b"], "spec": { "mode": "exact", "ore": { "a": 300, "b": 700 } } },
|
||||
"expected": { "error": "exact_sum_mismatch" }
|
||||
},
|
||||
{
|
||||
"name": "exact sum mismatch: one öre over",
|
||||
"input": { "amount_ore": 1001, "payer_user_id": "a", "user_ids": ["a", "b"], "spec": { "mode": "exact", "ore": { "a": 300, "b": 702 } } },
|
||||
"expected": { "error": "exact_sum_mismatch" }
|
||||
},
|
||||
{
|
||||
"name": "exact negative share (sum would match)",
|
||||
"input": { "amount_ore": 1001, "payer_user_id": "a", "user_ids": ["a", "b"], "spec": { "mode": "exact", "ore": { "a": -100, "b": 1101 } } },
|
||||
"expected": { "error": "exact_negative" }
|
||||
},
|
||||
{
|
||||
"name": "amount 0",
|
||||
"input": { "amount_ore": 0, "payer_user_id": "a", "user_ids": ["a", "b"], "spec": { "mode": "ratio", "bp": { "a": 5000, "b": 5000 } } },
|
||||
"expected": { "error": "amount_out_of_range" }
|
||||
},
|
||||
{
|
||||
"name": "amount negative",
|
||||
"input": { "amount_ore": -1, "payer_user_id": "a", "user_ids": ["a", "b"], "spec": { "mode": "ratio", "bp": { "a": 5000, "b": 5000 } } },
|
||||
"expected": { "error": "amount_out_of_range" }
|
||||
},
|
||||
{
|
||||
"name": "amount 1 000 000 001",
|
||||
"input": { "amount_ore": 1000000001, "payer_user_id": "a", "user_ids": ["a", "b"], "spec": { "mode": "ratio", "bp": { "a": 5000, "b": 5000 } } },
|
||||
"expected": { "error": "amount_out_of_range" }
|
||||
},
|
||||
{
|
||||
"name": "amount 1 000 000 000 with 3333 bp: 64-bit intermediate, no overflow",
|
||||
"input": { "amount_ore": 1000000000, "payer_user_id": "a", "user_ids": ["a", "b"], "spec": { "mode": "ratio", "bp": { "a": 6667, "b": 3333 } } },
|
||||
"expected": { "ok": { "a": 666700000, "b": 333300000 } }
|
||||
},
|
||||
{
|
||||
"name": "amount 999 999 999 with 3333 bp on the non-payer: remainder to payer at the top of the range",
|
||||
"input": { "amount_ore": 999999999, "payer_user_id": "a", "user_ids": ["a", "b"], "spec": { "mode": "ratio", "bp": { "a": 6667, "b": 3333 } } },
|
||||
"expected": { "ok": { "a": 666700000, "b": 333299999 } }
|
||||
},
|
||||
{
|
||||
"name": "amount 1 000 000 000 exactly, full on the non-payer",
|
||||
"input": { "amount_ore": 1000000000, "payer_user_id": "a", "user_ids": ["a", "b"], "spec": { "mode": "full", "bearer_user_id": "b" } },
|
||||
"expected": { "ok": { "a": 0, "b": 1000000000 } }
|
||||
},
|
||||
{
|
||||
"name": "bp 10001",
|
||||
"input": { "amount_ore": 1000, "payer_user_id": "a", "user_ids": ["a", "b"], "spec": { "mode": "ratio", "bp": { "a": 10001, "b": 0 } } },
|
||||
"expected": { "error": "bp_out_of_range" }
|
||||
},
|
||||
{
|
||||
"name": "ratio does not sum to 10000",
|
||||
"input": { "amount_ore": 1000, "payer_user_id": "a", "user_ids": ["a", "b"], "spec": { "mode": "ratio", "bp": { "a": 5000, "b": 4000 } } },
|
||||
"expected": { "error": "bp_sum_not_10000" }
|
||||
},
|
||||
{
|
||||
"name": "ratio sums to 10001",
|
||||
"input": { "amount_ore": 1000, "payer_user_id": "a", "user_ids": ["a", "b"], "spec": { "mode": "ratio", "bp": { "a": 5001, "b": 5000 } } },
|
||||
"expected": { "error": "bp_sum_not_10000" }
|
||||
},
|
||||
{
|
||||
"name": "three user ids",
|
||||
"input": { "amount_ore": 1000, "payer_user_id": "a", "user_ids": ["a", "b", "c"], "spec": { "mode": "ratio", "bp": { "a": 5000, "b": 5000 } } },
|
||||
"expected": { "error": "users_invalid" }
|
||||
},
|
||||
{
|
||||
"name": "one user id",
|
||||
"input": { "amount_ore": 1000, "payer_user_id": "a", "user_ids": ["a"], "spec": { "mode": "ratio", "bp": { "a": 10000 } } },
|
||||
"expected": { "error": "users_invalid" }
|
||||
},
|
||||
{
|
||||
"name": "two user ids that are not distinct",
|
||||
"input": { "amount_ore": 1000, "payer_user_id": "a", "user_ids": ["a", "a"], "spec": { "mode": "ratio", "bp": { "a": 10000 } } },
|
||||
"expected": { "error": "users_invalid" }
|
||||
},
|
||||
{
|
||||
"name": "payer not in users",
|
||||
"input": { "amount_ore": 1000, "payer_user_id": "x", "user_ids": ["a", "b"], "spec": { "mode": "ratio", "bp": { "a": 5000, "b": 5000 } } },
|
||||
"expected": { "error": "payer_unknown" }
|
||||
},
|
||||
{
|
||||
"name": "bearer not in users",
|
||||
"input": { "amount_ore": 1000, "payer_user_id": "a", "user_ids": ["a", "b"], "spec": { "mode": "full", "bearer_user_id": "x" } },
|
||||
"expected": { "error": "bearer_unknown" }
|
||||
},
|
||||
{
|
||||
"name": "ratio map keys do not match users",
|
||||
"input": { "amount_ore": 1000, "payer_user_id": "a", "user_ids": ["a", "b"], "spec": { "mode": "ratio", "bp": { "a": 5000, "x": 5000 } } },
|
||||
"expected": { "error": "share_users_mismatch" }
|
||||
},
|
||||
{
|
||||
"name": "ratio map has an extra zero entry",
|
||||
"input": { "amount_ore": 1000, "payer_user_id": "a", "user_ids": ["a", "b"], "spec": { "mode": "ratio", "bp": { "a": 5000, "b": 5000, "x": 0 } } },
|
||||
"expected": { "error": "share_users_mismatch" }
|
||||
},
|
||||
{
|
||||
"name": "ratio map misses one user",
|
||||
"input": { "amount_ore": 1000, "payer_user_id": "a", "user_ids": ["a", "b"], "spec": { "mode": "ratio", "bp": { "a": 10000 } } },
|
||||
"expected": { "error": "share_users_mismatch" }
|
||||
},
|
||||
{
|
||||
"name": "exact map keys do not match users",
|
||||
"input": { "amount_ore": 1000, "payer_user_id": "a", "user_ids": ["a", "b"], "spec": { "mode": "exact", "ore": { "a": 500, "x": 500 } } },
|
||||
"expected": { "error": "share_users_mismatch" }
|
||||
},
|
||||
{
|
||||
"name": "error order: amount checked before users",
|
||||
"input": { "amount_ore": 0, "payer_user_id": "x", "user_ids": ["a", "b", "c"], "spec": { "mode": "ratio", "bp": { "a": 5000, "b": 4000 } } },
|
||||
"expected": { "error": "amount_out_of_range" }
|
||||
},
|
||||
{
|
||||
"name": "error order: users checked before payer",
|
||||
"input": { "amount_ore": 1000, "payer_user_id": "x", "user_ids": ["a", "b", "c"], "spec": { "mode": "ratio", "bp": { "a": 5000, "b": 5000 } } },
|
||||
"expected": { "error": "users_invalid" }
|
||||
},
|
||||
{
|
||||
"name": "error order: payer checked before bp range",
|
||||
"input": { "amount_ore": 1000, "payer_user_id": "x", "user_ids": ["a", "b"], "spec": { "mode": "ratio", "bp": { "a": 10001, "b": 0 } } },
|
||||
"expected": { "error": "payer_unknown" }
|
||||
},
|
||||
{
|
||||
"name": "error order: bp range checked before bp sum",
|
||||
"input": { "amount_ore": 1000, "payer_user_id": "a", "user_ids": ["a", "b"], "spec": { "mode": "ratio", "bp": { "a": 10001, "b": 5000 } } },
|
||||
"expected": { "error": "bp_out_of_range" }
|
||||
},
|
||||
{
|
||||
"name": "error order: bp sum checked before share users",
|
||||
"input": { "amount_ore": 1000, "payer_user_id": "a", "user_ids": ["a", "b"], "spec": { "mode": "ratio", "bp": { "a": 5000, "x": 4000 } } },
|
||||
"expected": { "error": "bp_sum_not_10000" }
|
||||
},
|
||||
{
|
||||
"name": "error order: exact negative checked before exact sum",
|
||||
"input": { "amount_ore": 1000, "payer_user_id": "a", "user_ids": ["a", "b"], "spec": { "mode": "exact", "ore": { "a": -1, "b": 500 } } },
|
||||
"expected": { "error": "exact_negative" }
|
||||
},
|
||||
{
|
||||
"name": "error order: exact sum checked before share users",
|
||||
"input": { "amount_ore": 1000, "payer_user_id": "a", "user_ids": ["a", "b"], "spec": { "mode": "exact", "ore": { "a": 500, "x": 400 } } },
|
||||
"expected": { "error": "exact_sum_mismatch" }
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
{
|
||||
"format": 1,
|
||||
"function": "over_settled_accounts",
|
||||
"cases": [
|
||||
{
|
||||
"name": "none: every allocation within the debt (equal to the debt is not over)",
|
||||
"input": {
|
||||
"allocations": [ { "account_id": "X", "amount_ore": 30000 }, { "account_id": "Y", "amount_ore": 5000 } ],
|
||||
"accounts": [
|
||||
{ "account_id": "X", "created_at": "2025-01-01T00:00:00.000Z", "archived": false, "payer_net_ore": -30000 },
|
||||
{ "account_id": "Y", "created_at": "2025-02-01T00:00:00.000Z", "archived": false, "payer_net_ore": -20000 }
|
||||
]
|
||||
},
|
||||
"expected": { "ok": [] }
|
||||
},
|
||||
{
|
||||
"name": "one: Y gets one öre more than owed",
|
||||
"input": {
|
||||
"allocations": [ { "account_id": "X", "amount_ore": 30000 }, { "account_id": "Y", "amount_ore": 20001 } ],
|
||||
"accounts": [
|
||||
{ "account_id": "X", "created_at": "2025-01-01T00:00:00.000Z", "archived": false, "payer_net_ore": -30000 },
|
||||
{ "account_id": "Y", "created_at": "2025-02-01T00:00:00.000Z", "archived": false, "payer_net_ore": -20000 }
|
||||
]
|
||||
},
|
||||
"expected": { "ok": ["Y"] }
|
||||
},
|
||||
{
|
||||
"name": "an account where the payer is owed: any allocation is over (max(0, -net) = 0)",
|
||||
"input": {
|
||||
"allocations": [ { "account_id": "Z", "amount_ore": 1 } ],
|
||||
"accounts": [ { "account_id": "Z", "created_at": "2024-12-01T00:00:00.000Z", "archived": false, "payer_net_ore": 10000 } ]
|
||||
},
|
||||
"expected": { "ok": ["Z"] }
|
||||
},
|
||||
{
|
||||
"name": "an unknown account counts as net 0",
|
||||
"input": {
|
||||
"allocations": [ { "account_id": "X", "amount_ore": 100 }, { "account_id": "U", "amount_ore": 100 } ],
|
||||
"accounts": [ { "account_id": "X", "created_at": "2025-01-01T00:00:00.000Z", "archived": false, "payer_net_ore": -30000 } ]
|
||||
},
|
||||
"expected": { "ok": ["U"] }
|
||||
},
|
||||
{
|
||||
"name": "several: reported in allocation order, not account order",
|
||||
"input": {
|
||||
"allocations": [ { "account_id": "Y", "amount_ore": 25000 }, { "account_id": "X", "amount_ore": 100 }, { "account_id": "Z", "amount_ore": 1 } ],
|
||||
"accounts": [
|
||||
{ "account_id": "X", "created_at": "2025-01-01T00:00:00.000Z", "archived": false, "payer_net_ore": -30000 },
|
||||
{ "account_id": "Y", "created_at": "2025-02-01T00:00:00.000Z", "archived": false, "payer_net_ore": -20000 },
|
||||
{ "account_id": "Z", "created_at": "2024-12-01T00:00:00.000Z", "archived": true, "payer_net_ore": 0 }
|
||||
]
|
||||
},
|
||||
"expected": { "ok": ["Y", "Z"] }
|
||||
},
|
||||
{
|
||||
"name": "no allocations",
|
||||
"input": { "allocations": [], "accounts": [ { "account_id": "X", "created_at": "2025-01-01T00:00:00.000Z", "archived": false, "payer_net_ore": -30000 } ] },
|
||||
"expected": { "ok": [] }
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
{
|
||||
"format": 1,
|
||||
"function": "propose_allocation",
|
||||
"cases": [
|
||||
{
|
||||
"name": "owing X (-30000, oldest owing) and Y (-20000), owed in Z (+10000): 40000 gives X 30000, Y 10000",
|
||||
"input": {
|
||||
"amount_ore": 40000,
|
||||
"accounts": [
|
||||
{ "account_id": "X", "created_at": "2025-01-01T00:00:00.000Z", "archived": false, "payer_net_ore": -30000 },
|
||||
{ "account_id": "Y", "created_at": "2025-02-01T00:00:00.000Z", "archived": false, "payer_net_ore": -20000 },
|
||||
{ "account_id": "Z", "created_at": "2024-12-01T00:00:00.000Z", "archived": false, "payer_net_ore": 10000 }
|
||||
]
|
||||
},
|
||||
"expected": { "ok": { "allocations": [ { "account_id": "X", "amount_ore": 30000 }, { "account_id": "Y", "amount_ore": 10000 } ], "excess_ore": 0, "over_settled_account_ids": [] } }
|
||||
},
|
||||
{
|
||||
"name": "same accounts, 50000 = exactly the total debt: no excess",
|
||||
"input": {
|
||||
"amount_ore": 50000,
|
||||
"accounts": [
|
||||
{ "account_id": "X", "created_at": "2025-01-01T00:00:00.000Z", "archived": false, "payer_net_ore": -30000 },
|
||||
{ "account_id": "Y", "created_at": "2025-02-01T00:00:00.000Z", "archived": false, "payer_net_ore": -20000 },
|
||||
{ "account_id": "Z", "created_at": "2024-12-01T00:00:00.000Z", "archived": false, "payer_net_ore": 10000 }
|
||||
]
|
||||
},
|
||||
"expected": { "ok": { "allocations": [ { "account_id": "X", "amount_ore": 30000 }, { "account_id": "Y", "amount_ore": 20000 } ], "excess_ore": 0, "over_settled_account_ids": [] } }
|
||||
},
|
||||
{
|
||||
"name": "same accounts, 60000: excess 10000 added to the last owing account Y, over-settled [Y]",
|
||||
"input": {
|
||||
"amount_ore": 60000,
|
||||
"accounts": [
|
||||
{ "account_id": "X", "created_at": "2025-01-01T00:00:00.000Z", "archived": false, "payer_net_ore": -30000 },
|
||||
{ "account_id": "Y", "created_at": "2025-02-01T00:00:00.000Z", "archived": false, "payer_net_ore": -20000 },
|
||||
{ "account_id": "Z", "created_at": "2024-12-01T00:00:00.000Z", "archived": false, "payer_net_ore": 10000 }
|
||||
]
|
||||
},
|
||||
"expected": { "ok": { "allocations": [ { "account_id": "X", "amount_ore": 30000 }, { "account_id": "Y", "amount_ore": 30000 } ], "excess_ore": 10000, "over_settled_account_ids": ["Y"] } }
|
||||
},
|
||||
{
|
||||
"name": "same accounts, 20000 is less than the first debt: only X",
|
||||
"input": {
|
||||
"amount_ore": 20000,
|
||||
"accounts": [
|
||||
{ "account_id": "X", "created_at": "2025-01-01T00:00:00.000Z", "archived": false, "payer_net_ore": -30000 },
|
||||
{ "account_id": "Y", "created_at": "2025-02-01T00:00:00.000Z", "archived": false, "payer_net_ore": -20000 },
|
||||
{ "account_id": "Z", "created_at": "2024-12-01T00:00:00.000Z", "archived": false, "payer_net_ore": 10000 }
|
||||
]
|
||||
},
|
||||
"expected": { "ok": { "allocations": [ { "account_id": "X", "amount_ore": 20000 } ], "excess_ore": 0, "over_settled_account_ids": [] } }
|
||||
},
|
||||
{
|
||||
"name": "input order does not matter: accounts listed newest first",
|
||||
"input": {
|
||||
"amount_ore": 40000,
|
||||
"accounts": [
|
||||
{ "account_id": "Y", "created_at": "2025-02-01T00:00:00.000Z", "archived": false, "payer_net_ore": -20000 },
|
||||
{ "account_id": "Z", "created_at": "2024-12-01T00:00:00.000Z", "archived": false, "payer_net_ore": 10000 },
|
||||
{ "account_id": "X", "created_at": "2025-01-01T00:00:00.000Z", "archived": false, "payer_net_ore": -30000 }
|
||||
]
|
||||
},
|
||||
"expected": { "ok": { "allocations": [ { "account_id": "X", "amount_ore": 30000 }, { "account_id": "Y", "amount_ore": 10000 } ], "excess_ore": 0, "over_settled_account_ids": [] } }
|
||||
},
|
||||
{
|
||||
"name": "no owing accounts: 5000 with only Z (owed) goes to Z, over-settled [Z]",
|
||||
"input": {
|
||||
"amount_ore": 5000,
|
||||
"accounts": [ { "account_id": "Z", "created_at": "2024-12-01T00:00:00.000Z", "archived": false, "payer_net_ore": 10000 } ]
|
||||
},
|
||||
"expected": { "ok": { "allocations": [ { "account_id": "Z", "amount_ore": 5000 } ], "excess_ore": 5000, "over_settled_account_ids": ["Z"] } }
|
||||
},
|
||||
{
|
||||
"name": "no owing accounts: a net of exactly 0 is not owing; target is the oldest non-archived account",
|
||||
"input": {
|
||||
"amount_ore": 700,
|
||||
"accounts": [
|
||||
{ "account_id": "A", "created_at": "2024-01-01T00:00:00.000Z", "archived": true, "payer_net_ore": 0 },
|
||||
{ "account_id": "B", "created_at": "2024-06-01T00:00:00.000Z", "archived": false, "payer_net_ore": 500 },
|
||||
{ "account_id": "C", "created_at": "2024-03-01T00:00:00.000Z", "archived": false, "payer_net_ore": 0 }
|
||||
]
|
||||
},
|
||||
"expected": { "ok": { "allocations": [ { "account_id": "C", "amount_ore": 700 } ], "excess_ore": 700, "over_settled_account_ids": ["C"] } }
|
||||
},
|
||||
{
|
||||
"name": "all accounts archived and none owing: the oldest account",
|
||||
"input": {
|
||||
"amount_ore": 1200,
|
||||
"accounts": [
|
||||
{ "account_id": "P", "created_at": "2025-05-01T00:00:00.000Z", "archived": true, "payer_net_ore": 0 },
|
||||
{ "account_id": "Q", "created_at": "2025-03-01T00:00:00.000Z", "archived": true, "payer_net_ore": 300 },
|
||||
{ "account_id": "R", "created_at": "2025-04-01T00:00:00.000Z", "archived": true, "payer_net_ore": 0 }
|
||||
]
|
||||
},
|
||||
"expected": { "ok": { "allocations": [ { "account_id": "Q", "amount_ore": 1200 } ], "excess_ore": 1200, "over_settled_account_ids": ["Q"] } }
|
||||
},
|
||||
{
|
||||
"name": "archived owing accounts are still settled (archived never affects money)",
|
||||
"input": {
|
||||
"amount_ore": 1500,
|
||||
"accounts": [
|
||||
{ "account_id": "old", "created_at": "2024-01-01T00:00:00.000Z", "archived": true, "payer_net_ore": -1000 },
|
||||
{ "account_id": "new", "created_at": "2025-01-01T00:00:00.000Z", "archived": false, "payer_net_ore": -1000 }
|
||||
]
|
||||
},
|
||||
"expected": { "ok": { "allocations": [ { "account_id": "old", "amount_ore": 1000 }, { "account_id": "new", "amount_ore": 500 } ], "excess_ore": 0, "over_settled_account_ids": [] } }
|
||||
},
|
||||
{
|
||||
"name": "equal created_at among owing accounts: ordered by id ascending",
|
||||
"input": {
|
||||
"amount_ore": 1500,
|
||||
"accounts": [
|
||||
{ "account_id": "q", "created_at": "2025-01-01T00:00:00.000Z", "archived": false, "payer_net_ore": -1000 },
|
||||
{ "account_id": "p", "created_at": "2025-01-01T00:00:00.000Z", "archived": false, "payer_net_ore": -1000 }
|
||||
]
|
||||
},
|
||||
"expected": { "ok": { "allocations": [ { "account_id": "p", "amount_ore": 1000 }, { "account_id": "q", "amount_ore": 500 } ], "excess_ore": 0, "over_settled_account_ids": [] } }
|
||||
},
|
||||
{
|
||||
"name": "equal created_at with no owing account: target chosen by id ascending",
|
||||
"input": {
|
||||
"amount_ore": 100,
|
||||
"accounts": [
|
||||
{ "account_id": "n", "created_at": "2025-01-01T00:00:00.000Z", "archived": false, "payer_net_ore": 0 },
|
||||
{ "account_id": "m", "created_at": "2025-01-01T00:00:00.000Z", "archived": false, "payer_net_ore": 50 }
|
||||
]
|
||||
},
|
||||
"expected": { "ok": { "allocations": [ { "account_id": "m", "amount_ore": 100 } ], "excess_ore": 100, "over_settled_account_ids": ["m"] } }
|
||||
},
|
||||
{
|
||||
"name": "single owing account, amount above its debt: excess to it",
|
||||
"input": {
|
||||
"amount_ore": 1000,
|
||||
"accounts": [
|
||||
{ "account_id": "Z", "created_at": "2024-12-01T00:00:00.000Z", "archived": false, "payer_net_ore": 10000 },
|
||||
{ "account_id": "X", "created_at": "2025-01-01T00:00:00.000Z", "archived": false, "payer_net_ore": -1 }
|
||||
]
|
||||
},
|
||||
"expected": { "ok": { "allocations": [ { "account_id": "X", "amount_ore": 1000 } ], "excess_ore": 999, "over_settled_account_ids": ["X"] } }
|
||||
},
|
||||
{
|
||||
"name": "no accounts",
|
||||
"input": { "amount_ore": 1000, "accounts": [] },
|
||||
"expected": { "error": "no_accounts" }
|
||||
},
|
||||
{
|
||||
"name": "amount 0",
|
||||
"input": {
|
||||
"amount_ore": 0,
|
||||
"accounts": [ { "account_id": "X", "created_at": "2025-01-01T00:00:00.000Z", "archived": false, "payer_net_ore": -30000 } ]
|
||||
},
|
||||
"expected": { "error": "amount_out_of_range" }
|
||||
},
|
||||
{
|
||||
"name": "amount 1 000 000 001",
|
||||
"input": {
|
||||
"amount_ore": 1000000001,
|
||||
"accounts": [ { "account_id": "X", "created_at": "2025-01-01T00:00:00.000Z", "archived": false, "payer_net_ore": -30000 } ]
|
||||
},
|
||||
"expected": { "error": "amount_out_of_range" }
|
||||
},
|
||||
{
|
||||
"name": "error order: amount checked before no accounts",
|
||||
"input": { "amount_ore": 0, "accounts": [] },
|
||||
"expected": { "error": "amount_out_of_range" }
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,214 @@
|
||||
{
|
||||
"format": 1,
|
||||
"function": "resolve_range",
|
||||
"cases": [
|
||||
{
|
||||
"name": "this_month",
|
||||
"input": {
|
||||
"preset": "this_month",
|
||||
"today": "2026-10-17",
|
||||
"earliest_expense": "2024-03-05",
|
||||
"custom_start": null,
|
||||
"custom_end": null
|
||||
},
|
||||
"expected": {
|
||||
"ok": {
|
||||
"start": "2026-10-01",
|
||||
"end_inclusive": "2026-10-17"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "last_3_months: the current month and the two before",
|
||||
"input": {
|
||||
"preset": "last_3_months",
|
||||
"today": "2026-10-17",
|
||||
"earliest_expense": "2024-03-05",
|
||||
"custom_start": null,
|
||||
"custom_end": null
|
||||
},
|
||||
"expected": {
|
||||
"ok": {
|
||||
"start": "2026-08-01",
|
||||
"end_inclusive": "2026-10-17"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "last_6_months",
|
||||
"input": {
|
||||
"preset": "last_6_months",
|
||||
"today": "2026-10-17",
|
||||
"earliest_expense": "2024-03-05",
|
||||
"custom_start": null,
|
||||
"custom_end": null
|
||||
},
|
||||
"expected": {
|
||||
"ok": {
|
||||
"start": "2026-05-01",
|
||||
"end_inclusive": "2026-10-17"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "last_12_months: crosses into the previous year",
|
||||
"input": {
|
||||
"preset": "last_12_months",
|
||||
"today": "2026-10-17",
|
||||
"earliest_expense": "2024-03-05",
|
||||
"custom_start": null,
|
||||
"custom_end": null
|
||||
},
|
||||
"expected": {
|
||||
"ok": {
|
||||
"start": "2025-11-01",
|
||||
"end_inclusive": "2026-10-17"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "this_year",
|
||||
"input": {
|
||||
"preset": "this_year",
|
||||
"today": "2026-10-17",
|
||||
"earliest_expense": "2024-03-05",
|
||||
"custom_start": null,
|
||||
"custom_end": null
|
||||
},
|
||||
"expected": {
|
||||
"ok": {
|
||||
"start": "2026-01-01",
|
||||
"end_inclusive": "2026-10-17"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "all_time with an earliest expense",
|
||||
"input": {
|
||||
"preset": "all_time",
|
||||
"today": "2026-10-17",
|
||||
"earliest_expense": "2024-03-05",
|
||||
"custom_start": null,
|
||||
"custom_end": null
|
||||
},
|
||||
"expected": {
|
||||
"ok": {
|
||||
"start": "2024-03-05",
|
||||
"end_inclusive": "2026-10-17"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "all_time without expenses: today to today",
|
||||
"input": {
|
||||
"preset": "all_time",
|
||||
"today": "2026-10-17",
|
||||
"earliest_expense": null,
|
||||
"custom_start": null,
|
||||
"custom_end": null
|
||||
},
|
||||
"expected": {
|
||||
"ok": {
|
||||
"start": "2026-10-17",
|
||||
"end_inclusive": "2026-10-17"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "custom in order, future dates allowed",
|
||||
"input": {
|
||||
"preset": "custom",
|
||||
"today": "2026-10-17",
|
||||
"earliest_expense": null,
|
||||
"custom_start": "2026-11-01",
|
||||
"custom_end": "2026-12-31"
|
||||
},
|
||||
"expected": {
|
||||
"ok": {
|
||||
"start": "2026-11-01",
|
||||
"end_inclusive": "2026-12-31"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "custom swapped when start is after end",
|
||||
"input": {
|
||||
"preset": "custom",
|
||||
"today": "2026-10-17",
|
||||
"earliest_expense": null,
|
||||
"custom_start": "2026-12-31",
|
||||
"custom_end": "2026-11-01"
|
||||
},
|
||||
"expected": {
|
||||
"ok": {
|
||||
"start": "2026-11-01",
|
||||
"end_inclusive": "2026-12-31"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "custom single day",
|
||||
"input": {
|
||||
"preset": "custom",
|
||||
"today": "2026-10-17",
|
||||
"earliest_expense": null,
|
||||
"custom_start": "2026-02-28",
|
||||
"custom_end": "2026-02-28"
|
||||
},
|
||||
"expected": {
|
||||
"ok": {
|
||||
"start": "2026-02-28",
|
||||
"end_inclusive": "2026-02-28"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "last_3_months in January crosses the year",
|
||||
"input": {
|
||||
"preset": "last_3_months",
|
||||
"today": "2027-01-01",
|
||||
"earliest_expense": null,
|
||||
"custom_start": null,
|
||||
"custom_end": null
|
||||
},
|
||||
"expected": {
|
||||
"ok": {
|
||||
"start": "2026-11-01",
|
||||
"end_inclusive": "2027-01-01"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "last_12_months on 31 March: first of April the year before",
|
||||
"input": {
|
||||
"preset": "last_12_months",
|
||||
"today": "2027-03-31",
|
||||
"earliest_expense": null,
|
||||
"custom_start": null,
|
||||
"custom_end": null
|
||||
},
|
||||
"expected": {
|
||||
"ok": {
|
||||
"start": "2026-04-01",
|
||||
"end_inclusive": "2027-03-31"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "this_month on the first day of the month",
|
||||
"input": {
|
||||
"preset": "this_month",
|
||||
"today": "2026-11-01",
|
||||
"earliest_expense": null,
|
||||
"custom_start": null,
|
||||
"custom_end": null
|
||||
},
|
||||
"expected": {
|
||||
"ok": {
|
||||
"start": "2026-11-01",
|
||||
"end_inclusive": "2026-11-01"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user