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:
2026-10-17 20:57:59 +02:00
parent cab3e935d6
commit c6df2ab8f5
90 changed files with 8931 additions and 0 deletions
@@ -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"
}
+42
View File
@@ -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
}
+5
View File
@@ -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
}
+5
View File
@@ -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
}
+5
View File
@@ -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"
}
}
}
+17
View File
@@ -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": []
}
+323
View File
@@ -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
}
}
]
}
+59
View File
@@ -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
+230
View File
@@ -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 } } }
}
]
}
+216
View File
@@ -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" }
}
]
}
+214
View File
@@ -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
+1159
View File
File diff suppressed because it is too large Load Diff