feat: neues Tier folgt dem Kurator-Tier zurück

Bisher folgte nur "PawFeed Team" dem neuen Tier, der Feed des neuen
Nutzers startete leer. welcomeNewPet legt jetzt auch den Follow in
Gegenrichtung an und befüllt den Redis-Feed per backfillOnFollow mit den
letzten Team-Posts. Gleicher Admin-Schalter (autoFollowEnabled),
Beschriftung im Kurator-Panel angepasst.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
adminandClaude Opus 5.5 committed 2026-10-03 19:49:46 +02:00
1 parent 80e27a7f0b
commit f112d36f4a
3 files changed
+42 -3

No files matched your search

+26
View File
@@ -13,6 +13,9 @@ vi.mock("@/lib/team-pet", () => ({
ensureTeamPet: vi.fn().mockResolvedValue({ id: "pet-team" }),
sendTeamMessage: (...args: unknown[]) => mockSendTeamMessage(...args),
}));
vi.mock("@/lib/redis", () => ({ redis: {} }));
const mockBackfillOnFollow = vi.fn().mockResolvedValue(undefined);
vi.mock("@/lib/feed-helpers", () => ({ backfillOnFollow: (...args: unknown[]) => mockBackfillOnFollow(...args) }));
const mockAssertAdmin = vi.fn();
vi.mock("@/trpc/routers/admin", () => ({ assertAdmin: (...args: unknown[]) => mockAssertAdmin(...args) }));
@@ -67,6 +70,28 @@ describe("welcomeNewPet", () => {
expect(mockSendTeamMessage).toHaveBeenCalledWith(expect.anything(), "pet-luna", expect.stringContaining("Hallo Luna"));
});
it("makes the new pet follow the team back and seeds its feed with the team's posts", async () => {
const prisma = createMockPrisma();
prisma.follow.upsert.mockResolvedValue({});
await welcomeNewPet(asDb(prisma), { id: "pet-luna", name: "Luna" }, "de");
expect(prisma.follow.upsert).toHaveBeenCalledWith(
expect.objectContaining({ create: { followerPetId: "pet-luna", followeePetId: "pet-team" } })
);
expect(mockBackfillOnFollow).toHaveBeenCalledWith(expect.anything(), "pet-luna", "pet-team", expect.anything());
});
it("still sends the welcome when following the team back fails", async () => {
const prisma = createMockPrisma();
prisma.follow.upsert.mockResolvedValueOnce({}).mockRejectedValueOnce(new Error("db down"));
await welcomeNewPet(asDb(prisma), { id: "pet-luna", name: "Luna" }, "de");
expect(mockBackfillOnFollow).not.toHaveBeenCalled();
expect(mockSendTeamMessage).toHaveBeenCalled();
});
it("uses the English text for English users", async () => {
const prisma = createMockPrisma();
prisma.follow.upsert.mockResolvedValue({});
@@ -86,6 +111,7 @@ describe("welcomeNewPet", () => {
await welcomeNewPet(asDb(prisma), { id: "pet-kiki", name: "Kiki" }, "de");
expect(prisma.follow.upsert).not.toHaveBeenCalled();
expect(mockBackfillOnFollow).not.toHaveBeenCalled();
expect(mockSendTeamMessage).toHaveBeenCalledWith(expect.anything(), "pet-kiki", "Servus Kiki!");
});
+2 -2
View File
@@ -80,8 +80,8 @@ function CuratorForm({ initial }: { initial: Form }) {
return (
<div className="space-y-4">
<ToggleRow
title="Team folgt jedem neuen Tier"
hint="Das neue Tier bekommt „PawFeed Team“ als ersten Follower und eine Benachrichtigung."
title="Team und neues Tier folgen sich gegenseitig"
hint="Das neue Tier bekommt „PawFeed Team“ als ersten Follower (mit Benachrichtigung) und folgt dem Team selbst, damit sein Feed nicht leer startet."
checked={form.autoFollowEnabled}
onChange={(v) => set("autoFollowEnabled", v)}
/>
+14 -1
View File
@@ -2,12 +2,15 @@ import "server-only";
import type { PrismaClient } from "@prisma/client";
import { ensureTeamPet, sendTeamMessage } from "@/lib/team-pet";
import { notify } from "@/lib/notify";
import { redis } from "@/lib/redis";
import { backfillOnFollow } from "@/lib/feed-helpers";
/**
* Was beim Anlegen eines neuen Tiers automatisch passiert (aufgerufen aus
* pets.create, nie blockierend):
* 1. Gründungsmitglied, wenn es eines der ersten FOUNDING_MEMBER_LIMIT Tiere ist.
* 2. Das Kurator-Tier „PawFeed Team“ folgt dem neuen Tier.
* 2. Das Kurator-Tier „PawFeed Team“ und das neue Tier folgen sich gegenseitig
* (der Feed des neuen Tiers wird mit den Team-Posts befüllt).
* 3. Das Kurator-Tier schickt eine Begrüßungs-Direktnachricht.
* 2 und 3 sind im Admin-Panel abschaltbar, der Text ist dort bearbeitbar
* (CuratorSettings). {name} im Text wird durch den Tiernamen ersetzt.
@@ -84,6 +87,16 @@ export async function welcomeNewPet(
})
.then(() => notify(prisma, { recipientPetId: pet.id, type: "FOLLOW", actorPetId: teamPet.id }))
.catch((err) => console.error("[welcome] curator follow failed", err));
// Gegenrichtung: das neue Tier folgt dem Team, damit sein Feed nicht leer startet.
await prisma.follow
.upsert({
where: { followerPetId_followeePetId: { followerPetId: pet.id, followeePetId: teamPet.id } },
create: { followerPetId: pet.id, followeePetId: teamPet.id },
update: {},
})
.then(() => backfillOnFollow(redis, pet.id, teamPet.id, prisma))
.catch((err) => console.error("[welcome] follow-back of curator failed", err));
}
if (settings.welcomeEnabled) {