mirror of
https://github.com/bookorbit/bookorbit.git
synced 2026-11-03 08:21:39 -06:00
feat(books): allow progress updates for manual want_to_read books
Allow the system to automatically update the reading status of a book from 'want_to_read' to 'reading' or 'read' when reading progress is recorded, even if the status was originally set manually. Ensure that manually cataloged books are updated when the user begins reading them, while still protecting other manually set statuses from being overridden by auto-updates. Closes #566
This commit is contained in:
@@ -17,7 +17,7 @@ export const userBookStatus = pgTable(
|
||||
.references(() => books.id, { onDelete: 'cascade' }),
|
||||
// 'unread' | 'want_to_read' | 'reading' | 'on_hold' | 'rereading' | 'read' | 'skimmed' | 'abandoned'
|
||||
status: varchar('status', { length: 20 }).$type<ReadStatus>().notNull().default('unread'),
|
||||
// 'auto' (derived from progress) | 'manual' (user-set; never auto-overridden)
|
||||
// 'auto' (derived from progress) | 'manual' (user-set/imported; protected from progress updates except want_to_read)
|
||||
source: varchar('source', { length: 10 }).$type<ReadStatusSource>().notNull().default('auto'),
|
||||
startedAt: timestamp('started_at', { withTimezone: true }),
|
||||
finishedAt: timestamp('finished_at', { withTimezone: true }),
|
||||
|
||||
@@ -374,6 +374,34 @@ describe('autoUpdate normalization and guard behavior', () => {
|
||||
expect(mockRepo.upsertState).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ percentage: 50, expectedStatus: 'reading' },
|
||||
{ percentage: 98, expectedStatus: 'read' },
|
||||
] as const)('updates manual want_to_read to $expectedStatus when progress crosses thresholds', async ({ percentage, expectedStatus }) => {
|
||||
const existing = makeRow({ status: 'want_to_read', source: 'manual' });
|
||||
mockRepo.findOne.mockResolvedValue(existing);
|
||||
|
||||
await service.autoUpdate(1, 10, percentage);
|
||||
|
||||
expect(mockRepo.upsert).toHaveBeenCalledOnce();
|
||||
expect(mockRepo.upsert).toHaveBeenCalledWith(1, 10, expectedStatus, 'auto', expect.any(Date), existing);
|
||||
expect(mockRepo.upsertState).not.toHaveBeenCalled();
|
||||
expect(mockAchievementEvents.emit).toHaveBeenCalledWith(
|
||||
expect.any(String),
|
||||
expect.objectContaining({ userId: 1, bookId: 10, previousStatus: 'want_to_read', newStatus: expectedStatus }),
|
||||
);
|
||||
});
|
||||
|
||||
it('does not change manual want_to_read to unread below the reading threshold', async () => {
|
||||
mockRepo.findOne.mockResolvedValue(makeRow({ status: 'want_to_read', source: 'manual' }));
|
||||
|
||||
await service.autoUpdate(1, 10, 0.1);
|
||||
|
||||
expect(mockRepo.upsert).not.toHaveBeenCalled();
|
||||
expect(mockRepo.upsertState).not.toHaveBeenCalled();
|
||||
expect(mockAchievementEvents.emit).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('skips updates when derived status is unchanged', async () => {
|
||||
mockRepo.findOne.mockResolvedValue(makeRow({ status: 'reading', source: 'auto' }));
|
||||
|
||||
|
||||
@@ -124,13 +124,13 @@ export class UserBookStatusService {
|
||||
): Promise<void> {
|
||||
const existing = await this.repo.findOne(userId, bookId);
|
||||
|
||||
if (existing?.source === 'manual') return;
|
||||
|
||||
const normalizedPercentage = this.normalizePercentage(percentage);
|
||||
const { readThreshold, finishThreshold: normalizedFinishThreshold } = this.normalizeThresholds(readingThreshold, finishThreshold);
|
||||
const derived: ReadStatus =
|
||||
normalizedPercentage >= normalizedFinishThreshold ? 'read' : normalizedPercentage >= readThreshold ? 'reading' : 'unread';
|
||||
|
||||
if (existing?.source === 'manual' && (existing.status !== 'want_to_read' || derived === 'unread')) return;
|
||||
|
||||
if (!existing && derived === 'unread') return;
|
||||
if (existing?.status === derived) return;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user