Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 26 additions & 1 deletion apps/server/src/db/integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { DEFAULT_AVATAR_APPEARANCE, ROOM_CREATION_COST } from "@tilezo/protocol"
import { sql } from "drizzle-orm";
import { DrizzleAuthStore, UsernameTakenError } from "../auth/auth";
import { DrizzleEconomyStore } from "../economy/economy";
import { DrizzlePlaytimeRewardStore, PLAYTIME_ACTIVE_WINDOW_MS } from "../economy/playtimeRewards";
import { DrizzleFriendStore } from "../friends/friends";
import { DrizzleDirectMessageStore } from "../messaging/messaging";
import { createDatabase } from "./db";
Expand All @@ -27,13 +28,14 @@ describe("database integration", () => {

const authStore = new DrizzleAuthStore(database);
const economyStore = new DrizzleEconomyStore(database);
const playtimeRewardStore = new DrizzlePlaytimeRewardStore(database);
const friendStore = new DrizzleFriendStore(database);
const directMessageStore = new DrizzleDirectMessageStore(database);
const persistence = new DrizzlePersistenceStore(database);

beforeEach(async () => {
await database.execute(
sql`TRUNCATE TABLE users, rooms, friendships, user_room_sessions, room_items, direct_messages, user_inventory RESTART IDENTITY CASCADE`,
sql`TRUNCATE TABLE users, rooms, friendships, user_room_sessions, room_items, direct_messages, user_inventory, user_playtime_rewards RESTART IDENTITY CASCADE`,
);
});

Expand Down Expand Up @@ -200,6 +202,29 @@ describe("database integration", () => {
});
});

test("credits hourly active play rewards and persists remainder progress", async () => {
const owner = await seedUser("Dan");
const startBalance = owner.dollars;
const startedAt = new Date("2026-06-15T00:00:00.000Z");
let result: Awaited<ReturnType<typeof playtimeRewardStore.apply>>;

for (let index = 0; index <= 12; index += 1) {
result = await playtimeRewardStore.apply(
owner.id,
"activity",
new Date(startedAt.getTime() + index * PLAYTIME_ACTIVE_WINDOW_MS),
);
}

expect(result).toMatchObject({
accruedActiveMs: 0,
awardedDollars: 500,
awardedIntervals: 1,
balance: startBalance + 500,
});
expect(await economyStore.getBalance(owner.id)).toBe(startBalance + 500);
});

test("rejects spending and purchases with insufficient funds", async () => {
const owner = await seedUser("Dan");
await economyStore.spend(owner.id, owner.dollars);
Expand Down
11 changes: 11 additions & 0 deletions apps/server/src/db/migrations/0015_equal_roulette.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
CREATE TABLE "user_playtime_rewards" (
"user_id" text PRIMARY KEY NOT NULL,
"accrued_active_ms" integer DEFAULT 0 NOT NULL,
"last_activity_at" timestamp with time zone,
"last_accrued_at" timestamp with time zone,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
CONSTRAINT "user_playtime_rewards_accrued_active_ms_check" CHECK ("user_playtime_rewards"."accrued_active_ms" >= 0)
);
--> statement-breakpoint
ALTER TABLE "user_playtime_rewards" ADD CONSTRAINT "user_playtime_rewards_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;
Loading
Loading