mirror of
https://github.com/Patiun/com.gregkilmer.www.git
synced 2026-11-08 04:08:24 +00:00
Phase 6: sync jobs, inquiry and order forms
- Sync scheduler in the content service with a per-job cache (sync_cache), sync_runs reporting and Healthchecks pings. Jobs: Archidekt, BoardGameGeek, cover art (Open Library, TMDB, AniList), Suwayomi and Kavita reading progress, Bambuddy filament and Homebox material stock. A note's own value always wins over a synced one. - PersonalReader: updates only Library notes with a matching anilistId, changes to Greg's notes are held for review; Reading series with no note show as suggestions on the admin Overview. - /api/inquiries and /api/orders: Turnstile, rate limits, a saved copy in the content service (intake token) plus email over SMTP. Orders are re-checked against the live notes: accepted only when the category's ordersOpen is true and the line has capacity. - Admin Inbox for saved submissions. Options' inStock is optional so synced stock can fill it. All settings from process.env; .env.example per service. - Sources could not be reached from the build environment; parsers are tested against fixtures labelled as not live. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012aSbo2D1qP8cMFQCykf7kx
This commit is contained in:
@@ -0,0 +1,86 @@
|
||||
/**
|
||||
* PersonalReader progress on Library rows and synced stock on Store options.
|
||||
* Synced values are the shapes the content service's jobs store (see
|
||||
* services/content/src/sync/jobs/); a note's own values always win, and a
|
||||
* note with nothing synced renders as before.
|
||||
*/
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { withSynced, type Note } from "../src/lib/content.ts";
|
||||
import { itemDetails, readerProgress, type Item } from "../src/lib/library.ts";
|
||||
import { optionGroups, type Line } from "../src/lib/store.ts";
|
||||
|
||||
const note = <T extends Note>(type: string, data: Record<string, unknown>, synced?: Record<string, unknown>) =>
|
||||
withSynced({ path: `${type}/x.md`, type, name: "x", slug: "x", title: "X", body: "", acceptedAt: "", data: { type, ...data }, synced } as unknown as T);
|
||||
|
||||
describe("Library progress from PersonalReader", () => {
|
||||
const base = { medium: "manhwa", state: "now", creator: "Someone", year: 2020 };
|
||||
|
||||
it("adds the chapter line to the details", () => {
|
||||
const i = note<Item>("library-item", base, { chapterProgress: { title: "X", status: "now", chapter: 142, readAt: "2026-10-03T05:00:00.000Z" } });
|
||||
expect(readerProgress(i)).toBe("Chapter 142, read Oct 3, 2026");
|
||||
expect(itemDetails(i)).toEqual(["Someone", 2020, "Chapter 142, read Oct 3, 2026"]);
|
||||
const book = note<Item>("library-item", { ...base, medium: "light novel" }, { bookProgress: { title: "X", percent: 62 } });
|
||||
expect(readerProgress(book)).toBe("62% read");
|
||||
});
|
||||
|
||||
it("lets the note's own progress win, and shows the same line when nothing is synced", () => {
|
||||
const own = note<Item>("library-item", { ...base, progress: "16/18 volumes" }, { chapterProgress: { chapter: 3 } });
|
||||
expect(itemDetails(own)).toEqual(["Someone", 2020, "16/18 volumes"]);
|
||||
const bare = note<Item>("library-item", base);
|
||||
expect(itemDetails(bare)).toEqual(["Someone", 2020]);
|
||||
expect(readerProgress(note<Item>("library-item", base, { bookProgress: { percent: 0 } }))).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Store options with synced stock", () => {
|
||||
const lineData = (options: unknown[]) => ({ category: "Tabletop", mode: "made to order", channel: "here", summary: "", priceTiers: [], options });
|
||||
const stock = {
|
||||
filamentStock: [
|
||||
{
|
||||
group: "Filament",
|
||||
options: [
|
||||
{ name: "Jade White", hex: "#f5f5dc", inStock: true },
|
||||
{ name: "Bambu Green", hex: "#00ae42", inStock: false },
|
||||
{ name: "Charcoal", hex: "#2b2b2b", inStock: true },
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
it("is exactly the note's options when nothing is synced", () => {
|
||||
const opts = [
|
||||
{ group: "Filament", name: "Jade White", hex: "#ffffff", inStock: true },
|
||||
{ group: "Finish", name: "Matte", inStock: false },
|
||||
];
|
||||
expect(optionGroups(note<Line>("product-line", lineData(opts)))).toEqual([
|
||||
["Filament", [opts[0]]],
|
||||
["Finish", [opts[1]]],
|
||||
]);
|
||||
// Left out with nothing synced: in stock, as before the field was optional.
|
||||
expect(optionGroups(note<Line>("product-line", lineData([{ group: "Finish", name: "Gloss" }])))[0]![1][0]!.inStock).toBe(true);
|
||||
});
|
||||
|
||||
it("sits under the note: note values win, gaps take the synced value, unlisted in-stock colors follow", () => {
|
||||
const l = note<Line>(
|
||||
"product-line",
|
||||
lineData([
|
||||
{ group: "Filament", name: "Jade White" },
|
||||
{ group: "Filament", name: "Bambu Green", inStock: true },
|
||||
{ group: "Filament", name: "Galaxy Black" },
|
||||
{ group: "Finish", name: "Matte", inStock: true },
|
||||
]),
|
||||
stock,
|
||||
);
|
||||
const [[g, filament], [, finish]] = optionGroups(l) as [string, ReturnType<typeof optionGroups>[number][1]][];
|
||||
expect(g).toBe("Filament");
|
||||
expect(filament).toEqual([
|
||||
{ group: "Filament", name: "Jade White", hex: "#f5f5dc", inStock: true },
|
||||
// The note says in stock: the note wins over the source.
|
||||
{ group: "Filament", name: "Bambu Green", hex: "#00ae42", inStock: true },
|
||||
// Not at the source: out of stock.
|
||||
{ group: "Filament", name: "Galaxy Black", inStock: false },
|
||||
{ group: "Filament", name: "Charcoal", hex: "#2b2b2b", inStock: true },
|
||||
]);
|
||||
expect(finish).toEqual([{ group: "Finish", name: "Matte", inStock: true }]);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user