Rework entry types: merge hotel into stay, transport with modes, auto-transport

- Type set is now activity/stay/transport/flight/rental/note; hotel, travel
  and immigration are removed with idempotent startup data migrations
  (hotel->stay, travel->transport, immigration->activity with flag prefix)
- Transport entries carry an optional mode (train/bus/ferry/taxi/drive/other)
  that drives the chip/map icon; route stops expose transport_mode
- Creating a stay auto-creates a bridging transport to its neighbouring
  stays unless a transport/flight already covers the gap (one-shot)
- Summary: transports count replaces hotels/travelLegs
This commit is contained in:
2026-07-19 17:47:21 +07:00
parent b066e7b885
commit d393e16ae1
17 changed files with 469 additions and 62 deletions
+8 -8
View File
@@ -82,12 +82,12 @@ test('costs: equal split with payer produces net balances and a settlement', asy
const ben = await createAccount();
const trip = await costTrip(anna, [ben]);
// anna pays 950 (hotel), ben pays 500 (travel); both split equally between the two.
// anna pays 950 (stay), ben pays 500 (transport); both split equally between the two.
await anna.agent.post(`/api/trips/${trip.id}/entries`).send({
date: '2026-08-01', type: 'hotel', title: 'Hotel', price: 950, paid_by: anna.user.id,
date: '2026-08-01', type: 'stay', title: 'Hotel', price: 950, paid_by: anna.user.id,
});
await anna.agent.post(`/api/trips/${trip.id}/entries`).send({
date: '2026-08-02', type: 'travel', title: 'Van', price: 500, paid_by: ben.user.id,
date: '2026-08-02', type: 'transport', title: 'Van', price: 500, paid_by: ben.user.id,
});
const res = await anna.agent.get(`/api/trips/${trip.id}/costs`);
@@ -95,7 +95,7 @@ test('costs: equal split with payer produces net balances and a settlement', asy
const c = res.body;
assert.equal(c.currency, 'USD');
assert.equal(c.totalCost, 1450);
assert.deepEqual(c.byType, { hotel: 950, travel: 500 });
assert.deepEqual(c.byType, { stay: 950, transport: 500 });
assert.equal(c.unassigned, 0);
const a = findUser(c, anna.user.id);
@@ -159,7 +159,7 @@ test('costs: participants subset only splits among the chosen members', async ()
// 90 split equally between anna & ben only (carol excluded), anna pays.
await anna.agent.post(`/api/trips/${trip.id}/entries`).send({
date: '2026-08-01', type: 'travel', title: 'Taxi', price: 90, paid_by: anna.user.id,
date: '2026-08-01', type: 'transport', title: 'Taxi', price: 90, paid_by: anna.user.id,
participants: [anna.user.id, ben.user.id],
});
@@ -179,13 +179,13 @@ test('costs: equal with no payer accumulates into unassigned', async () => {
const trip = await costTrip(anna, [ben]);
await anna.agent.post(`/api/trips/${trip.id}/entries`).send({
date: '2026-08-01', type: 'hotel', title: 'Hotel', price: 200, // paid_by omitted (null)
date: '2026-08-01', type: 'stay', title: 'Hotel', price: 200, // paid_by omitted (null)
});
const c = (await anna.agent.get(`/api/trips/${trip.id}/costs`)).body;
assert.equal(c.totalCost, 200);
assert.equal(c.unassigned, 200);
assert.deepEqual(c.byType, { hotel: 200 });
assert.deepEqual(c.byType, { stay: 200 });
for (const u of c.perUser) {
assert.deepEqual([u.share, u.paid, u.net], [100, 0, -100]);
}
@@ -207,7 +207,7 @@ test('computeCosts: greedy settlement matches largest debtor with largest credit
const c = computeCosts({
currency: 'USD',
members,
entries: [{ type: 'hotel', price: 300, paid_by: 1, split_mode: 'equal', participants: [] }],
entries: [{ type: 'stay', price: 300, paid_by: 1, split_mode: 'equal', participants: [] }],
});
assert.equal(c.totalCost, 300);
assert.equal(findUser(c, 1).displayName, 'brave-otter');