refactor(scanner): extract session and route API libs (page Brief 1)
Move scanner session persistence, queue merge helpers, and destination routing fetch calls into lib/scanner-session.js and lib/scanner-route-api.js. Load collections/decks on mount (were defined but never invoked). Remove unused mana-symbol imports and dead select-all helpers. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
cf770b618c
commit
6b70fd9bd1
5 changed files with 351 additions and 222 deletions
113
lib/scanner-route-api.js
Normal file
113
lib/scanner-route-api.js
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
import { VOCAB } from './collection-vocabulary.js';
|
||||
|
||||
function authHeaders(json = true) {
|
||||
const headers = {
|
||||
Authorization: `Bearer ${localStorage.getItem('auth_token')}`,
|
||||
};
|
||||
if (json) {
|
||||
headers['Content-Type'] = 'application/json';
|
||||
}
|
||||
return headers;
|
||||
}
|
||||
|
||||
export function buildScannerCardPayload(cardData) {
|
||||
const payload = {
|
||||
cardId: cardData.databaseId,
|
||||
quantity: cardData.quantity || 1,
|
||||
condition: cardData.condition || 'NM',
|
||||
is_foil: Boolean(cardData.isFoil),
|
||||
};
|
||||
|
||||
if (cardData.scanImageUrl) {
|
||||
payload.scan_image_url = cardData.scanImageUrl;
|
||||
}
|
||||
|
||||
return payload;
|
||||
}
|
||||
|
||||
export async function fetchScannerCollections() {
|
||||
const response = await fetch('/api/collections', { headers: authHeaders(false) });
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to load lists');
|
||||
}
|
||||
const data = await response.json();
|
||||
return data.filter((collection) => !collection.is_system_collection);
|
||||
}
|
||||
|
||||
export async function fetchScannerDecks() {
|
||||
const response = await fetch('/api/decks', { headers: authHeaders(false) });
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to load decks');
|
||||
}
|
||||
return response.json();
|
||||
}
|
||||
|
||||
export async function addScannedCardToOwned(cardData) {
|
||||
const response = await fetch('/api/user-cards', {
|
||||
method: 'POST',
|
||||
headers: authHeaders(),
|
||||
body: JSON.stringify(buildScannerCardPayload(cardData)),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to add to ${VOCAB.MY_COLLECTION}`);
|
||||
}
|
||||
}
|
||||
|
||||
export async function addScannedCardToCollection(cardData, collectionId) {
|
||||
const response = await fetch(`/api/collections/${collectionId}/cards`, {
|
||||
method: 'POST',
|
||||
headers: authHeaders(),
|
||||
body: JSON.stringify(buildScannerCardPayload(cardData)),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to add to ${VOCAB.LIST}`);
|
||||
}
|
||||
}
|
||||
|
||||
export async function addScannedCardToDeck(cardData, deckId) {
|
||||
const response = await fetch(`/api/decks/${deckId}/cards`, {
|
||||
method: 'POST',
|
||||
headers: authHeaders(),
|
||||
body: JSON.stringify(buildScannerCardPayload(cardData)),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to add to deck');
|
||||
}
|
||||
}
|
||||
|
||||
export async function createScannerCollection(name) {
|
||||
const response = await fetch('/api/collections', {
|
||||
method: 'POST',
|
||||
headers: authHeaders(),
|
||||
body: JSON.stringify({
|
||||
name,
|
||||
description: 'Created from card scanner',
|
||||
is_public: false,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to create list');
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
export async function routeScannedCardToDestination(cardData, destination) {
|
||||
if (!destination || !cardData.databaseId) return false;
|
||||
|
||||
if (destination.type === 'owned') {
|
||||
await addScannedCardToOwned(cardData);
|
||||
} else if (destination.type === 'collection') {
|
||||
await addScannedCardToCollection(cardData, destination.id);
|
||||
} else if (destination.type === 'deck') {
|
||||
await addScannedCardToDeck(cardData, destination.id);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
87
lib/scanner-session.js
Normal file
87
lib/scanner-session.js
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
import { VOCAB } from './collection-vocabulary.js';
|
||||
|
||||
export const SCANNER_SESSION_STORAGE_KEY = 'deckhearth:scanner-session';
|
||||
|
||||
export const DEFAULT_SCANNER_DESTINATION = {
|
||||
type: 'owned',
|
||||
id: null,
|
||||
label: VOCAB.MY_COLLECTION,
|
||||
};
|
||||
|
||||
export const DEFAULT_SCAN_DEFAULTS = { condition: 'NM', isFoil: false };
|
||||
|
||||
export function loadSavedScannerSession() {
|
||||
if (typeof window === 'undefined') {
|
||||
return {
|
||||
destination: DEFAULT_SCANNER_DESTINATION,
|
||||
gameFilter: 'All',
|
||||
scanDefaults: DEFAULT_SCAN_DEFAULTS,
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const saved = JSON.parse(localStorage.getItem(SCANNER_SESSION_STORAGE_KEY));
|
||||
return {
|
||||
destination: saved?.destination || DEFAULT_SCANNER_DESTINATION,
|
||||
gameFilter: saved?.gameFilter || 'All',
|
||||
scanDefaults: saved?.scanDefaults || DEFAULT_SCAN_DEFAULTS,
|
||||
};
|
||||
} catch {
|
||||
return {
|
||||
destination: DEFAULT_SCANNER_DESTINATION,
|
||||
gameFilter: 'All',
|
||||
scanDefaults: DEFAULT_SCAN_DEFAULTS,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export function saveScannerSession({ destination, gameFilter, scanDefaults }) {
|
||||
if (typeof window === 'undefined') return;
|
||||
localStorage.setItem(
|
||||
SCANNER_SESSION_STORAGE_KEY,
|
||||
JSON.stringify({ destination, gameFilter, scanDefaults })
|
||||
);
|
||||
}
|
||||
|
||||
export function createScanCardId() {
|
||||
return Date.now() + Math.random();
|
||||
}
|
||||
|
||||
export function destinationActionKey(destination) {
|
||||
if (!destination) return 'pending';
|
||||
return destination.type === 'owned' ? 'owned' : destination.type;
|
||||
}
|
||||
|
||||
export function mergeScannedCardEntry(existingCards, cardData, scanDefaults) {
|
||||
const existingCardIndex = existingCards.findIndex(
|
||||
(existing) =>
|
||||
existing.name === cardData.name && existing.set === cardData.set && !existing.processed
|
||||
);
|
||||
|
||||
if (existingCardIndex !== -1) {
|
||||
const existing = existingCards[existingCardIndex];
|
||||
const cardEntry = {
|
||||
...existing,
|
||||
quantity: (existing.quantity || 1) + 1,
|
||||
scanImageUrl: cardData.scanImageUrl || existing.scanImageUrl,
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
const updated = [...existingCards];
|
||||
updated[existingCardIndex] = cardEntry;
|
||||
return { cardEntry, scannedCards: updated, merged: true };
|
||||
}
|
||||
|
||||
const cardEntry = {
|
||||
...cardData,
|
||||
id: createScanCardId(),
|
||||
name: cardData.name,
|
||||
set: cardData.set,
|
||||
quantity: 1,
|
||||
condition: scanDefaults.condition,
|
||||
isFoil: scanDefaults.isFoil,
|
||||
timestamp: new Date().toISOString(),
|
||||
processed: false,
|
||||
};
|
||||
|
||||
return { cardEntry, scannedCards: [cardEntry, ...existingCards], merged: false };
|
||||
}
|
||||
275
pages/scanner.js
275
pages/scanner.js
|
|
@ -5,41 +5,25 @@ import CameraScanner from '../components/CameraScanner';
|
|||
import ScannerDestinationPicker from '../components/ScannerDestinationPicker';
|
||||
import ScannedCardItem, { CONDITION_OPTIONS } from '../components/ScannedCardItem';
|
||||
import OCRSettings from '../components/OCRSettings';
|
||||
import { ManaCost, ColorIdentity } from '../components/ManaSymbols';
|
||||
import ManaSymbolSettings from '../components/ManaSymbolSettings';
|
||||
import { useAuth } from '../lib/use-auth';
|
||||
import { useFocusTrap } from '../lib/use-focus-trap.js';
|
||||
import { VOCAB } from '../lib/collection-vocabulary.js';
|
||||
|
||||
const SESSION_STORAGE_KEY = 'deckhearth:scanner-session';
|
||||
|
||||
const DEFAULT_DESTINATION = { type: 'owned', id: null, label: VOCAB.MY_COLLECTION };
|
||||
const DEFAULT_SCAN_DEFAULTS = { condition: 'NM', isFoil: false };
|
||||
|
||||
function loadSavedScannerSession() {
|
||||
if (typeof window === 'undefined') {
|
||||
return { destination: DEFAULT_DESTINATION, gameFilter: 'All', scanDefaults: DEFAULT_SCAN_DEFAULTS };
|
||||
}
|
||||
try {
|
||||
const saved = JSON.parse(localStorage.getItem(SESSION_STORAGE_KEY));
|
||||
return {
|
||||
destination: saved?.destination || DEFAULT_DESTINATION,
|
||||
gameFilter: saved?.gameFilter || 'All',
|
||||
scanDefaults: saved?.scanDefaults || DEFAULT_SCAN_DEFAULTS,
|
||||
};
|
||||
} catch {
|
||||
return { destination: DEFAULT_DESTINATION, gameFilter: 'All', scanDefaults: DEFAULT_SCAN_DEFAULTS };
|
||||
}
|
||||
}
|
||||
|
||||
function createScanCardId() {
|
||||
return Date.now() + Math.random();
|
||||
}
|
||||
|
||||
function destinationActionKey(destination) {
|
||||
if (!destination) return 'pending';
|
||||
return destination.type === 'owned' ? 'owned' : destination.type;
|
||||
}
|
||||
import {
|
||||
addScannedCardToCollection,
|
||||
addScannedCardToDeck,
|
||||
addScannedCardToOwned,
|
||||
createScannerCollection,
|
||||
fetchScannerCollections,
|
||||
fetchScannerDecks,
|
||||
routeScannedCardToDestination,
|
||||
} from '../lib/scanner-route-api.js';
|
||||
import {
|
||||
DEFAULT_SCANNER_DESTINATION,
|
||||
destinationActionKey,
|
||||
loadSavedScannerSession,
|
||||
mergeScannedCardEntry,
|
||||
saveScannerSession,
|
||||
} from '../lib/scanner-session.js';
|
||||
|
||||
export default function Scanner() {
|
||||
const { user, loading: authLoading } = useAuth();
|
||||
|
|
@ -66,9 +50,6 @@ export default function Scanner() {
|
|||
const [scanDefaults, setScanDefaults] = useState(() => loadSavedScannerSession().scanDefaults);
|
||||
const [autoRouteError, setAutoRouteError] = useState(null);
|
||||
|
||||
// Mana symbol settings
|
||||
const [manaSymbolSettings, setManaSymbolSettings] = useState({ useSVG: false });
|
||||
|
||||
// Redirect to login if not authenticated (wait for verify to finish)
|
||||
useEffect(() => {
|
||||
if (!authLoading && !user) {
|
||||
|
|
@ -77,109 +58,50 @@ export default function Scanner() {
|
|||
}, [authLoading, user, router]);
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window === 'undefined') return;
|
||||
localStorage.setItem(
|
||||
SESSION_STORAGE_KEY,
|
||||
JSON.stringify({ destination: sessionDestination, gameFilter, scanDefaults })
|
||||
);
|
||||
saveScannerSession({ destination: sessionDestination, gameFilter, scanDefaults });
|
||||
}, [sessionDestination, gameFilter, scanDefaults]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!user) return;
|
||||
|
||||
let cancelled = false;
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
const [loadedCollections, loadedDecks] = await Promise.all([
|
||||
fetchScannerCollections(),
|
||||
fetchScannerDecks(),
|
||||
]);
|
||||
if (cancelled) return;
|
||||
setCollections(loadedCollections);
|
||||
setDecks(loadedDecks);
|
||||
} catch (error) {
|
||||
console.error('Error loading scanner destinations:', error);
|
||||
}
|
||||
})();
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [user]);
|
||||
|
||||
const handleGameFilterChange = (nextFilter) => {
|
||||
setGameFilter(nextFilter);
|
||||
setSessionDestination((current) => {
|
||||
if (!current || current.type === 'owned') return current;
|
||||
return DEFAULT_DESTINATION;
|
||||
return DEFAULT_SCANNER_DESTINATION;
|
||||
});
|
||||
};
|
||||
|
||||
const routeCardToDestination = async (card, destination) => {
|
||||
if (!destination || !card.databaseId) return false;
|
||||
|
||||
if (destination.type === 'owned') {
|
||||
await addToOwnedCards(card);
|
||||
} else if (destination.type === 'collection') {
|
||||
await addToCollection(card, destination.id);
|
||||
} else if (destination.type === 'deck') {
|
||||
await addToDeck(card, destination.id);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
const loadCollections = async () => {
|
||||
try {
|
||||
const response = await fetch('/api/collections', {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${localStorage.getItem('auth_token')}`
|
||||
}
|
||||
});
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
// Filter out auto-sync system lists (not user-curated)
|
||||
const userCollections = data.filter(collection => !collection.is_system_collection);
|
||||
setCollections(userCollections);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading collections:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const loadDecks = async () => {
|
||||
try {
|
||||
const response = await fetch('/api/decks', {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${localStorage.getItem('auth_token')}`
|
||||
}
|
||||
});
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
setDecks(data);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading decks:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCardScanned = async (cardData) => {
|
||||
setAutoRouteError(null);
|
||||
|
||||
const existingCardIndex = scannedCards.findIndex((existing) =>
|
||||
existing.name === cardData.name &&
|
||||
existing.set === cardData.set &&
|
||||
!existing.processed
|
||||
const { cardEntry, scannedCards: nextQueue } = mergeScannedCardEntry(
|
||||
scannedCards,
|
||||
cardData,
|
||||
scanDefaults
|
||||
);
|
||||
|
||||
let cardEntry;
|
||||
|
||||
if (existingCardIndex !== -1) {
|
||||
const existing = scannedCards[existingCardIndex];
|
||||
cardEntry = {
|
||||
...existing,
|
||||
quantity: (existing.quantity || 1) + 1,
|
||||
scanImageUrl: cardData.scanImageUrl || existing.scanImageUrl,
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
setScannedCards((prev) => {
|
||||
const updated = [...prev];
|
||||
updated[existingCardIndex] = cardEntry;
|
||||
return updated;
|
||||
});
|
||||
} else {
|
||||
cardEntry = {
|
||||
...cardData,
|
||||
id: createScanCardId(),
|
||||
name: cardData.name,
|
||||
set: cardData.set,
|
||||
quantity: 1,
|
||||
condition: scanDefaults.condition,
|
||||
isFoil: scanDefaults.isFoil,
|
||||
timestamp: new Date().toISOString(),
|
||||
processed: false,
|
||||
};
|
||||
setScannedCards((prev) => [cardEntry, ...prev]);
|
||||
}
|
||||
setScannedCards(nextQueue);
|
||||
|
||||
if (!sessionDestination) return;
|
||||
|
||||
|
|
@ -190,7 +112,7 @@ export default function Scanner() {
|
|||
|
||||
try {
|
||||
if (!tryBeginAdding(cardEntry.id)) return;
|
||||
await routeCardToDestination(cardEntry, sessionDestination);
|
||||
await routeScannedCardToDestination(cardEntry, sessionDestination);
|
||||
markCardAsProcessed(cardEntry.id, destinationActionKey(sessionDestination));
|
||||
} catch (error) {
|
||||
console.error('Auto-route failed:', error);
|
||||
|
|
@ -242,7 +164,7 @@ export default function Scanner() {
|
|||
const addSingleCardToOwned = async (card) => {
|
||||
if (!tryBeginAdding(card.id)) return;
|
||||
try {
|
||||
await addToOwnedCards(card);
|
||||
await addScannedCardToOwned(card);
|
||||
markCardAsProcessed(card.id, 'owned');
|
||||
} catch (error) {
|
||||
console.error('Error adding card to owned:', error);
|
||||
|
|
@ -254,7 +176,7 @@ export default function Scanner() {
|
|||
const addSingleCardToCollection = async (card, collectionId) => {
|
||||
if (!tryBeginAdding(card.id)) return;
|
||||
try {
|
||||
await addToCollection(card, collectionId);
|
||||
await addScannedCardToCollection(card, collectionId);
|
||||
markCardAsProcessed(card.id, 'collection');
|
||||
} catch (error) {
|
||||
console.error('Error adding card to collection:', error);
|
||||
|
|
@ -266,7 +188,7 @@ export default function Scanner() {
|
|||
const addSingleCardToDeck = async (card, deckId) => {
|
||||
if (!tryBeginAdding(card.id)) return;
|
||||
try {
|
||||
await addToDeck(card, deckId);
|
||||
await addScannedCardToDeck(card, deckId);
|
||||
markCardAsProcessed(card.id, 'deck');
|
||||
} catch (error) {
|
||||
console.error('Error adding card to deck:', error);
|
||||
|
|
@ -289,21 +211,21 @@ export default function Scanner() {
|
|||
if (action === 'owned') {
|
||||
if (!tryBeginAdding(card.id)) continue;
|
||||
try {
|
||||
await addToOwnedCards(card);
|
||||
await addScannedCardToOwned(card);
|
||||
} finally {
|
||||
endAdding(card.id);
|
||||
}
|
||||
} else if (action === 'collection' && target) {
|
||||
if (!tryBeginAdding(card.id)) continue;
|
||||
try {
|
||||
await addToCollection(card, target);
|
||||
await addScannedCardToCollection(card, target);
|
||||
} finally {
|
||||
endAdding(card.id);
|
||||
}
|
||||
} else if (action === 'deck' && target) {
|
||||
if (!tryBeginAdding(card.id)) continue;
|
||||
try {
|
||||
await addToDeck(card, target);
|
||||
await addScannedCardToDeck(card, target);
|
||||
} finally {
|
||||
endAdding(card.id);
|
||||
}
|
||||
|
|
@ -337,91 +259,15 @@ export default function Scanner() {
|
|||
);
|
||||
};
|
||||
|
||||
const buildCardPayload = (cardData) => {
|
||||
const payload = {
|
||||
cardId: cardData.databaseId,
|
||||
quantity: cardData.quantity || 1,
|
||||
condition: cardData.condition || 'NM',
|
||||
is_foil: Boolean(cardData.isFoil),
|
||||
};
|
||||
|
||||
if (cardData.scanImageUrl) {
|
||||
payload.scan_image_url = cardData.scanImageUrl;
|
||||
}
|
||||
|
||||
return payload;
|
||||
};
|
||||
|
||||
// Helper functions for API calls
|
||||
const addToOwnedCards = async (cardData) => {
|
||||
const response = await fetch('/api/user-cards', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${localStorage.getItem('auth_token')}`,
|
||||
},
|
||||
body: JSON.stringify(buildCardPayload(cardData)),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to add to ${VOCAB.MY_COLLECTION}`);
|
||||
}
|
||||
};
|
||||
|
||||
const addToCollection = async (cardData, collectionId) => {
|
||||
const response = await fetch(`/api/collections/${collectionId}/cards`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${localStorage.getItem('auth_token')}`
|
||||
},
|
||||
body: JSON.stringify(buildCardPayload(cardData)),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to add to ${VOCAB.LIST}`);
|
||||
}
|
||||
};
|
||||
|
||||
const addToDeck = async (cardData, deckId) => {
|
||||
const response = await fetch(`/api/decks/${deckId}/cards`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${localStorage.getItem('auth_token')}`,
|
||||
},
|
||||
body: JSON.stringify(buildCardPayload(cardData)),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to add to deck');
|
||||
}
|
||||
};
|
||||
|
||||
const createCollection = async () => {
|
||||
if (!newCollectionName.trim()) return;
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/collections', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${localStorage.getItem('auth_token')}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
name: newCollectionName,
|
||||
description: 'Created from card scanner',
|
||||
is_public: false
|
||||
})
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const newCollection = await response.json();
|
||||
setCollections(prev => [newCollection, ...prev]);
|
||||
const newCollection = await createScannerCollection(newCollectionName.trim());
|
||||
setCollections((prev) => [newCollection, ...prev]);
|
||||
setBulkTarget(newCollection.id.toString());
|
||||
setNewCollectionName('');
|
||||
setShowCreateCollection(false);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error creating collection:', error);
|
||||
}
|
||||
|
|
@ -453,15 +299,6 @@ export default function Scanner() {
|
|||
});
|
||||
};
|
||||
|
||||
const selectAllCards = () => {
|
||||
const unprocessedCards = scannedCards.filter(card => !card.processed);
|
||||
setSelectedCards(new Set(unprocessedCards.map(card => card.id)));
|
||||
};
|
||||
|
||||
const deselectAllCards = () => {
|
||||
setSelectedCards(new Set());
|
||||
};
|
||||
|
||||
if (authLoading) {
|
||||
return (
|
||||
<Layout user={null}>
|
||||
|
|
|
|||
31
test/lib/scanner-route-api.test.js
Normal file
31
test/lib/scanner-route-api.test.js
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
import { describe, expect, it } from 'vitest';
|
||||
import { buildScannerCardPayload } from '../../lib/scanner-route-api.js';
|
||||
|
||||
describe('buildScannerCardPayload', () => {
|
||||
it('maps scanner queue fields to API body shape', () => {
|
||||
expect(
|
||||
buildScannerCardPayload({
|
||||
databaseId: 42,
|
||||
quantity: 2,
|
||||
condition: 'LP',
|
||||
isFoil: true,
|
||||
scanImageUrl: 'https://blob.example/scan.jpg',
|
||||
})
|
||||
).toEqual({
|
||||
cardId: 42,
|
||||
quantity: 2,
|
||||
condition: 'LP',
|
||||
is_foil: true,
|
||||
scan_image_url: 'https://blob.example/scan.jpg',
|
||||
});
|
||||
});
|
||||
|
||||
it('omits scan_image_url when capture was not uploaded', () => {
|
||||
expect(buildScannerCardPayload({ databaseId: 1 })).toEqual({
|
||||
cardId: 1,
|
||||
quantity: 1,
|
||||
condition: 'NM',
|
||||
is_foil: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
61
test/lib/scanner-session.test.js
Normal file
61
test/lib/scanner-session.test.js
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
import { describe, expect, it } from 'vitest';
|
||||
import {
|
||||
DEFAULT_SCANNER_DESTINATION,
|
||||
destinationActionKey,
|
||||
mergeScannedCardEntry,
|
||||
} from '../../lib/scanner-session.js';
|
||||
|
||||
describe('destinationActionKey', () => {
|
||||
it('maps owned destination to owned action key', () => {
|
||||
expect(destinationActionKey(DEFAULT_SCANNER_DESTINATION)).toBe('owned');
|
||||
});
|
||||
|
||||
it('returns pending when destination is missing', () => {
|
||||
expect(destinationActionKey(null)).toBe('pending');
|
||||
});
|
||||
});
|
||||
|
||||
describe('mergeScannedCardEntry', () => {
|
||||
const scanDefaults = { condition: 'NM', isFoil: false };
|
||||
|
||||
it('creates a new queue entry with scan defaults', () => {
|
||||
const { cardEntry, scannedCards, merged } = mergeScannedCardEntry([], {
|
||||
name: 'Pikachu',
|
||||
set: 'Base',
|
||||
databaseId: 1,
|
||||
}, scanDefaults);
|
||||
|
||||
expect(merged).toBe(false);
|
||||
expect(scannedCards).toHaveLength(1);
|
||||
expect(cardEntry).toMatchObject({
|
||||
name: 'Pikachu',
|
||||
set: 'Base',
|
||||
quantity: 1,
|
||||
condition: 'NM',
|
||||
isFoil: false,
|
||||
processed: false,
|
||||
});
|
||||
expect(cardEntry.id).toBeTruthy();
|
||||
});
|
||||
|
||||
it('increments quantity for an unprocessed duplicate name+set', () => {
|
||||
const existing = {
|
||||
id: 99,
|
||||
name: 'Bolt',
|
||||
set: 'Alpha',
|
||||
quantity: 2,
|
||||
processed: false,
|
||||
};
|
||||
|
||||
const { cardEntry, scannedCards, merged } = mergeScannedCardEntry(
|
||||
[existing],
|
||||
{ name: 'Bolt', set: 'Alpha', databaseId: 1, scanImageUrl: 'https://blob/new.jpg' },
|
||||
scanDefaults
|
||||
);
|
||||
|
||||
expect(merged).toBe(true);
|
||||
expect(scannedCards).toHaveLength(1);
|
||||
expect(cardEntry.quantity).toBe(3);
|
||||
expect(cardEntry.scanImageUrl).toBe('https://blob/new.jpg');
|
||||
});
|
||||
});
|
||||
Loading…
Reference in a new issue