feat(private-room): migrate to album APIs
This commit is contained in:
@@ -0,0 +1,103 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import {
|
||||
PrivateAlbumsResponse,
|
||||
PrivateAlbumUnlockResponse,
|
||||
UnlockPrivateAlbumRequest,
|
||||
} from "@/data/dto/private-room";
|
||||
import { ApiPath } from "@/data/services/api";
|
||||
|
||||
const ALBUM_ID = "a1b2c3d4-0000-0000-0000-000000000000";
|
||||
const COVER_URL =
|
||||
"https://dbapi.banlv-ai.com/storage/v1/object/public/elio-schedules/01.jpg";
|
||||
|
||||
const lockedAlbum = {
|
||||
albumId: ALBUM_ID,
|
||||
momentId: `album:${ALBUM_ID}`,
|
||||
characterId: "elio",
|
||||
collectionKey: "manila_202607",
|
||||
title: "Private Manila set",
|
||||
content: null,
|
||||
previewText: "Only for you.",
|
||||
imageCount: 8,
|
||||
mediaCount: 8,
|
||||
images: [{ url: COVER_URL, type: "image", locked: true, index: 0 }],
|
||||
locked: true,
|
||||
unlocked: false,
|
||||
unlockCost: 320,
|
||||
requiredCredits: 320,
|
||||
creditCostPerImage: 40,
|
||||
currency: "credits",
|
||||
canUnlockWithCredits: false,
|
||||
publishedAt: "2026-07-13T00:00:00+00:00",
|
||||
lockDetail: {
|
||||
locked: true,
|
||||
showContent: false,
|
||||
showUpgrade: true,
|
||||
reason: "private_album",
|
||||
},
|
||||
};
|
||||
|
||||
describe("Private Album DTOs", () => {
|
||||
it("keeps a locked album cover URL while preserving the lock state", () => {
|
||||
const response = PrivateAlbumsResponse.fromJson({
|
||||
items: [lockedAlbum],
|
||||
creditBalance: 100,
|
||||
nextCursor: null,
|
||||
hasMore: false,
|
||||
});
|
||||
|
||||
expect(response.items[0]?.locked).toBe(true);
|
||||
expect(response.items[0]?.unlocked).toBe(false);
|
||||
expect(response.items[0]?.images[0]?.url).toBe(COVER_URL);
|
||||
expect(response.items[0]?.lockDetail).toEqual({ locked: true });
|
||||
});
|
||||
|
||||
it("strips response fields that are not used by the frontend", () => {
|
||||
const response = PrivateAlbumsResponse.fromJson({
|
||||
items: [lockedAlbum],
|
||||
creditBalance: 100,
|
||||
packageOptions: [{ imageCount: 8, creditCost: 320 }],
|
||||
});
|
||||
const albumJson = response.items[0]?.toJson();
|
||||
|
||||
expect(albumJson).not.toHaveProperty("momentId");
|
||||
expect(albumJson).not.toHaveProperty("characterId");
|
||||
expect(albumJson).not.toHaveProperty("collectionKey");
|
||||
expect(response.toJson()).not.toHaveProperty("packageOptions");
|
||||
});
|
||||
|
||||
it("parses an unlock response with all album images", () => {
|
||||
const response = PrivateAlbumUnlockResponse.fromJson({
|
||||
albumId: ALBUM_ID,
|
||||
locked: false,
|
||||
unlocked: true,
|
||||
reason: "ok",
|
||||
unlockCost: 320,
|
||||
creditBalance: 180,
|
||||
images: Array.from({ length: 8 }, (_, index) => ({
|
||||
url: `${COVER_URL}?image=${index}`,
|
||||
locked: false,
|
||||
index,
|
||||
})),
|
||||
creditsCharged: 320,
|
||||
});
|
||||
|
||||
expect(response.unlocked).toBe(true);
|
||||
expect(response.images).toHaveLength(8);
|
||||
expect(response.creditBalance).toBe(180);
|
||||
expect(response.toJson()).not.toHaveProperty("creditsCharged");
|
||||
});
|
||||
|
||||
it("serializes the expected unlock cost", () => {
|
||||
expect(
|
||||
UnlockPrivateAlbumRequest.from({ expectedCost: 320 }).toJson(),
|
||||
).toEqual({ expectedCost: 320 });
|
||||
});
|
||||
|
||||
it("encodes the album id in the unlock path", () => {
|
||||
expect(ApiPath.privateRoomAlbumUnlock("album:91")).toBe(
|
||||
"/api/private-room/albums/album%3A91/unlock",
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,133 +0,0 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import {
|
||||
PrivateRoomMomentsResponse,
|
||||
PrivateRoomUnlockResponse,
|
||||
} from "@/data/dto/private-room";
|
||||
import { ApiPath } from "@/data/services/api";
|
||||
|
||||
const lockedMoment = {
|
||||
momentId: "schedule:91",
|
||||
source: "elio_schedules",
|
||||
sourceId: "91",
|
||||
characterId: "elio",
|
||||
author: {
|
||||
id: "elio",
|
||||
name: "Elio Silvestri",
|
||||
avatarUrl: null,
|
||||
},
|
||||
createdAt: "2026-07-01T08:11:35+00:00",
|
||||
publishedAt: "2026-07-01T08:11:35+00:00",
|
||||
timeText: "7 days ago",
|
||||
title: "Paris morning",
|
||||
content: null,
|
||||
text: null,
|
||||
textPreview: "Unlock to view private room photos",
|
||||
mediaCount: 1,
|
||||
images: [
|
||||
{
|
||||
url: null,
|
||||
type: "image",
|
||||
locked: true,
|
||||
index: 0,
|
||||
},
|
||||
],
|
||||
locked: true,
|
||||
unlocked: false,
|
||||
unlockCost: 40,
|
||||
unlockCostPerImage: 40,
|
||||
requiredCredits: 40,
|
||||
currency: "credits",
|
||||
lockDetail: {
|
||||
locked: true,
|
||||
showContent: false,
|
||||
showUpgrade: true,
|
||||
reason: "private_room_moment",
|
||||
hint: "40 credits · Unlock photo",
|
||||
actionLabel: "Unlock",
|
||||
type: "private_room_moment",
|
||||
requiredCredits: 40,
|
||||
currentCredits: 100,
|
||||
shortfallCredits: 0,
|
||||
mediaCount: 1,
|
||||
unlockCostPerImage: 40,
|
||||
},
|
||||
};
|
||||
|
||||
describe("PrivateRoom DTOs", () => {
|
||||
it("parses locked moments without image URLs", () => {
|
||||
const response = PrivateRoomMomentsResponse.from({
|
||||
profile: {
|
||||
characterId: "elio",
|
||||
displayName: "Elio Silvestri",
|
||||
authorName: "Elio Silvestri",
|
||||
avatarUrl: null,
|
||||
coverUrl: null,
|
||||
title: "Elio Private room",
|
||||
subtitle: "Join me, unlock my private room",
|
||||
},
|
||||
items: [lockedMoment],
|
||||
nextCursor: null,
|
||||
hasMore: false,
|
||||
creditBalance: 100,
|
||||
unlockCostDefault: 40,
|
||||
unlockCostPerImage: 40,
|
||||
currency: "credits",
|
||||
source: "elio_schedules",
|
||||
});
|
||||
|
||||
expect(response.items[0]?.locked).toBe(true);
|
||||
expect(response.items[0]?.images[0]?.url).toBeNull();
|
||||
expect(response.items[0]?.unlockCost).toBe(40);
|
||||
expect(response.profile.displayName).toBe("Elio Silvestri");
|
||||
});
|
||||
|
||||
it("parses unlock success with real image URLs", () => {
|
||||
const response = PrivateRoomUnlockResponse.from({
|
||||
...lockedMoment,
|
||||
locked: false,
|
||||
unlocked: true,
|
||||
content: "A quiet morning in Paris.",
|
||||
text: "A quiet morning in Paris.",
|
||||
images: [
|
||||
{
|
||||
url: "https://lehwkihwnlqkavhcspel.supabase.co/storage/v1/object/public/elio-schedules/schedules/91/photo.jpg",
|
||||
type: "image",
|
||||
locked: false,
|
||||
index: 0,
|
||||
},
|
||||
],
|
||||
reason: "ok",
|
||||
creditsCharged: 40,
|
||||
creditBalance: 60,
|
||||
previousCreditBalance: 100,
|
||||
persisted: true,
|
||||
});
|
||||
|
||||
expect(response.unlocked).toBe(true);
|
||||
expect(response.reason).toBe("ok");
|
||||
expect(response.images[0]?.url).toContain("https://");
|
||||
expect(response.creditBalance).toBe(60);
|
||||
});
|
||||
|
||||
it("parses insufficient credits as business data", () => {
|
||||
const response = PrivateRoomUnlockResponse.from({
|
||||
...lockedMoment,
|
||||
reason: "insufficient_credits",
|
||||
currentCredits: 10,
|
||||
shortfallCredits: 30,
|
||||
creditBalance: 10,
|
||||
});
|
||||
|
||||
expect(response.unlocked).toBe(false);
|
||||
expect(response.locked).toBe(true);
|
||||
expect(response.reason).toBe("insufficient_credits");
|
||||
expect(response.images[0]?.url).toBeNull();
|
||||
});
|
||||
|
||||
it("encodes moment id in the unlock path", () => {
|
||||
expect(ApiPath.privateRoomMomentUnlock("schedule:91")).toBe(
|
||||
"/api/private-room/moments/schedule%3A91/unlock",
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,3 +1,3 @@
|
||||
export * from "./private_room_moment";
|
||||
export * from "./private_album";
|
||||
export * from "./request";
|
||||
export * from "./response";
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import {
|
||||
PrivateAlbumSchema,
|
||||
type PrivateAlbumData,
|
||||
type PrivateAlbumInput,
|
||||
} from "@/data/schemas/private-room";
|
||||
|
||||
export class PrivateAlbum {
|
||||
declare readonly albumId: string;
|
||||
declare readonly title: string;
|
||||
declare readonly content: string | null;
|
||||
declare readonly previewText: string;
|
||||
declare readonly imageCount: number;
|
||||
declare readonly images: PrivateAlbumData["images"];
|
||||
declare readonly locked: boolean;
|
||||
declare readonly unlocked: boolean;
|
||||
declare readonly unlockCost: number;
|
||||
declare readonly publishedAt: string | null;
|
||||
declare readonly lockDetail: PrivateAlbumData["lockDetail"];
|
||||
|
||||
private constructor(input: PrivateAlbumInput) {
|
||||
const data = PrivateAlbumSchema.parse(input);
|
||||
Object.assign(this, data);
|
||||
Object.freeze(this);
|
||||
}
|
||||
|
||||
static from(input: PrivateAlbumInput): PrivateAlbum {
|
||||
return new PrivateAlbum(input);
|
||||
}
|
||||
|
||||
static fromJson(json: unknown): PrivateAlbum {
|
||||
return PrivateAlbum.from(json as PrivateAlbumInput);
|
||||
}
|
||||
|
||||
toJson(): PrivateAlbumData {
|
||||
return PrivateAlbumSchema.parse(this);
|
||||
}
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
import {
|
||||
PrivateRoomMomentSchema,
|
||||
type PrivateRoomMomentData,
|
||||
type PrivateRoomMomentInput,
|
||||
} from "@/data/schemas/private-room";
|
||||
|
||||
export class PrivateRoomMoment {
|
||||
declare readonly momentId: string;
|
||||
declare readonly author: PrivateRoomMomentData["author"];
|
||||
declare readonly publishedAt: string | null;
|
||||
declare readonly timeText: string;
|
||||
declare readonly title: string;
|
||||
declare readonly content: string | null;
|
||||
declare readonly text: string | null;
|
||||
declare readonly textPreview: string;
|
||||
declare readonly mediaCount: number;
|
||||
declare readonly images: PrivateRoomMomentData["images"];
|
||||
declare readonly locked: boolean;
|
||||
declare readonly unlockCost: number;
|
||||
|
||||
protected constructor(input: PrivateRoomMomentInput, freeze = true) {
|
||||
const data = PrivateRoomMomentSchema.parse(input);
|
||||
Object.assign(this, data);
|
||||
if (freeze) Object.freeze(this);
|
||||
}
|
||||
|
||||
static from(input: PrivateRoomMomentInput): PrivateRoomMoment {
|
||||
return new PrivateRoomMoment(input);
|
||||
}
|
||||
|
||||
static fromJson(json: unknown): PrivateRoomMoment {
|
||||
return PrivateRoomMoment.from(json as PrivateRoomMomentInput);
|
||||
}
|
||||
|
||||
toJson(): PrivateRoomMomentData {
|
||||
return PrivateRoomMomentSchema.parse(this);
|
||||
}
|
||||
}
|
||||
|
||||
export type PrivateRoomProfile = import("@/data/schemas/private-room").PrivateRoomProfileData;
|
||||
@@ -1 +1 @@
|
||||
export * from "./unlock_private_room_moment_request";
|
||||
export * from "./unlock_private_album_request";
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import {
|
||||
UnlockPrivateAlbumRequestSchema,
|
||||
type UnlockPrivateAlbumRequestData,
|
||||
type UnlockPrivateAlbumRequestInput,
|
||||
} from "@/data/schemas/private-room";
|
||||
|
||||
export class UnlockPrivateAlbumRequest {
|
||||
private constructor(input: UnlockPrivateAlbumRequestInput) {
|
||||
const data = UnlockPrivateAlbumRequestSchema.parse(input);
|
||||
Object.assign(this, data);
|
||||
Object.freeze(this);
|
||||
}
|
||||
|
||||
static from(input: UnlockPrivateAlbumRequestInput): UnlockPrivateAlbumRequest {
|
||||
return new UnlockPrivateAlbumRequest(input);
|
||||
}
|
||||
|
||||
toJson(): UnlockPrivateAlbumRequestData {
|
||||
return UnlockPrivateAlbumRequestSchema.parse(this);
|
||||
}
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
import {
|
||||
UnlockPrivateRoomMomentRequestSchema,
|
||||
type UnlockPrivateRoomMomentRequestData,
|
||||
type UnlockPrivateRoomMomentRequestInput,
|
||||
} from "@/data/schemas/private-room";
|
||||
|
||||
export class UnlockPrivateRoomMomentRequest {
|
||||
declare readonly expectedCost: number;
|
||||
|
||||
private constructor(input: UnlockPrivateRoomMomentRequestInput) {
|
||||
const data = UnlockPrivateRoomMomentRequestSchema.parse(input);
|
||||
Object.assign(this, data);
|
||||
Object.freeze(this);
|
||||
}
|
||||
|
||||
static from(
|
||||
input: UnlockPrivateRoomMomentRequestInput,
|
||||
): UnlockPrivateRoomMomentRequest {
|
||||
return new UnlockPrivateRoomMomentRequest(input);
|
||||
}
|
||||
|
||||
static fromJson(json: unknown): UnlockPrivateRoomMomentRequest {
|
||||
return UnlockPrivateRoomMomentRequest.from(
|
||||
json as UnlockPrivateRoomMomentRequestInput,
|
||||
);
|
||||
}
|
||||
|
||||
toJson(): UnlockPrivateRoomMomentRequestData {
|
||||
return UnlockPrivateRoomMomentRequestSchema.parse(this);
|
||||
}
|
||||
}
|
||||
@@ -1,2 +1,2 @@
|
||||
export * from "./private_room_moments_response";
|
||||
export * from "./private_room_unlock_response";
|
||||
export * from "./private_albums_response";
|
||||
export * from "./private_album_unlock_response";
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import {
|
||||
PrivateAlbumUnlockResponseSchema,
|
||||
type PrivateAlbumUnlockReason,
|
||||
type PrivateAlbumUnlockResponseData,
|
||||
type PrivateAlbumUnlockResponseInput,
|
||||
} from "@/data/schemas/private-room";
|
||||
|
||||
export class PrivateAlbumUnlockResponse {
|
||||
declare readonly albumId: string;
|
||||
declare readonly locked: boolean;
|
||||
declare readonly unlocked: boolean;
|
||||
declare readonly reason: PrivateAlbumUnlockReason | string;
|
||||
declare readonly unlockCost: number;
|
||||
declare readonly requiredCredits: number;
|
||||
declare readonly creditBalance: number;
|
||||
declare readonly shortfallCredits: number;
|
||||
declare readonly images: PrivateAlbumUnlockResponseData["images"];
|
||||
|
||||
private constructor(input: PrivateAlbumUnlockResponseInput) {
|
||||
const data = PrivateAlbumUnlockResponseSchema.parse(input);
|
||||
Object.assign(this, data);
|
||||
Object.freeze(this);
|
||||
}
|
||||
|
||||
static from(input: PrivateAlbumUnlockResponseInput): PrivateAlbumUnlockResponse {
|
||||
return new PrivateAlbumUnlockResponse(input);
|
||||
}
|
||||
|
||||
static fromJson(json: unknown): PrivateAlbumUnlockResponse {
|
||||
return PrivateAlbumUnlockResponse.from(
|
||||
json as PrivateAlbumUnlockResponseInput,
|
||||
);
|
||||
}
|
||||
|
||||
toJson(): PrivateAlbumUnlockResponseData {
|
||||
return PrivateAlbumUnlockResponseSchema.parse(this);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import {
|
||||
PrivateAlbumsResponseSchema,
|
||||
type PrivateAlbumsResponseData,
|
||||
type PrivateAlbumsResponseInput,
|
||||
} from "@/data/schemas/private-room";
|
||||
|
||||
import { PrivateAlbum } from "../private_album";
|
||||
|
||||
export class PrivateAlbumsResponse {
|
||||
declare readonly items: PrivateAlbum[];
|
||||
declare readonly creditBalance: number;
|
||||
|
||||
private constructor(input: PrivateAlbumsResponseInput) {
|
||||
const data = PrivateAlbumsResponseSchema.parse(input);
|
||||
Object.assign(this, {
|
||||
items: data.items.map((item) => PrivateAlbum.from(item)),
|
||||
creditBalance: data.creditBalance,
|
||||
});
|
||||
Object.freeze(this);
|
||||
}
|
||||
|
||||
static from(input: PrivateAlbumsResponseInput): PrivateAlbumsResponse {
|
||||
return new PrivateAlbumsResponse(input);
|
||||
}
|
||||
|
||||
static fromJson(json: unknown): PrivateAlbumsResponse {
|
||||
return PrivateAlbumsResponse.from(json as PrivateAlbumsResponseInput);
|
||||
}
|
||||
|
||||
toJson(): PrivateAlbumsResponseData {
|
||||
return {
|
||||
items: this.items.map((item) => item.toJson()),
|
||||
creditBalance: this.creditBalance,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
import {
|
||||
PrivateRoomMomentsResponseSchema,
|
||||
type PrivateRoomMomentsResponseData,
|
||||
type PrivateRoomMomentsResponseInput,
|
||||
} from "@/data/schemas/private-room";
|
||||
|
||||
import { PrivateRoomMoment } from "../private_room_moment";
|
||||
|
||||
export class PrivateRoomMomentsResponse {
|
||||
declare readonly profile: PrivateRoomMomentsResponseData["profile"];
|
||||
declare readonly items: PrivateRoomMoment[];
|
||||
declare readonly nextCursor: string | null;
|
||||
declare readonly hasMore: boolean;
|
||||
declare readonly creditBalance: number;
|
||||
declare readonly unlockCostDefault: number;
|
||||
declare readonly unlockCostPerImage: number;
|
||||
declare readonly currency: string;
|
||||
declare readonly source: string;
|
||||
|
||||
private constructor(input: PrivateRoomMomentsResponseInput) {
|
||||
const data = PrivateRoomMomentsResponseSchema.parse(input);
|
||||
Object.assign(this, {
|
||||
...data,
|
||||
items: data.items.map((item) => PrivateRoomMoment.from(item)),
|
||||
});
|
||||
Object.freeze(this);
|
||||
}
|
||||
|
||||
static from(
|
||||
input: PrivateRoomMomentsResponseInput,
|
||||
): PrivateRoomMomentsResponse {
|
||||
return new PrivateRoomMomentsResponse(input);
|
||||
}
|
||||
|
||||
static fromJson(json: unknown): PrivateRoomMomentsResponse {
|
||||
return PrivateRoomMomentsResponse.from(
|
||||
json as PrivateRoomMomentsResponseInput,
|
||||
);
|
||||
}
|
||||
|
||||
toJson(): PrivateRoomMomentsResponseData {
|
||||
return {
|
||||
profile: this.profile,
|
||||
items: this.items.map((item) => item.toJson()),
|
||||
nextCursor: this.nextCursor,
|
||||
hasMore: this.hasMore,
|
||||
creditBalance: this.creditBalance,
|
||||
unlockCostDefault: this.unlockCostDefault,
|
||||
unlockCostPerImage: this.unlockCostPerImage,
|
||||
currency: this.currency,
|
||||
source: this.source,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
import {
|
||||
PrivateRoomUnlockResponseSchema,
|
||||
type PrivateRoomUnlockReason,
|
||||
type PrivateRoomUnlockResponseData,
|
||||
type PrivateRoomUnlockResponseInput,
|
||||
} from "@/data/schemas/private-room";
|
||||
|
||||
import { PrivateRoomMoment } from "../private_room_moment";
|
||||
|
||||
export class PrivateRoomUnlockResponse extends PrivateRoomMoment {
|
||||
declare readonly unlocked: boolean;
|
||||
declare readonly requiredCredits: number;
|
||||
declare readonly reason: PrivateRoomUnlockReason | string;
|
||||
declare readonly creditsCharged: number;
|
||||
declare readonly creditBalance: number;
|
||||
declare readonly previousCreditBalance: number;
|
||||
declare readonly currentCredits: number;
|
||||
declare readonly shortfallCredits: number;
|
||||
declare readonly persisted: boolean;
|
||||
|
||||
private constructor(input: PrivateRoomUnlockResponseInput) {
|
||||
const data = PrivateRoomUnlockResponseSchema.parse(input);
|
||||
super(data, false);
|
||||
Object.assign(this, data);
|
||||
Object.freeze(this);
|
||||
}
|
||||
|
||||
static override from(
|
||||
input: PrivateRoomUnlockResponseInput,
|
||||
): PrivateRoomUnlockResponse {
|
||||
return new PrivateRoomUnlockResponse(input);
|
||||
}
|
||||
|
||||
static override fromJson(json: unknown): PrivateRoomUnlockResponse {
|
||||
return PrivateRoomUnlockResponse.from(
|
||||
json as PrivateRoomUnlockResponseInput,
|
||||
);
|
||||
}
|
||||
|
||||
override toJson(): PrivateRoomUnlockResponseData {
|
||||
return PrivateRoomUnlockResponseSchema.parse(this);
|
||||
}
|
||||
}
|
||||
@@ -1,22 +1,21 @@
|
||||
import type {
|
||||
PrivateRoomMomentsResponse,
|
||||
PrivateRoomUnlockResponse,
|
||||
PrivateAlbumsResponse,
|
||||
PrivateAlbumUnlockResponse,
|
||||
} from "@/data/dto/private-room";
|
||||
import type { Result } from "@/utils/result";
|
||||
|
||||
export interface GetPrivateRoomMomentsInput {
|
||||
export interface GetPrivateAlbumsInput {
|
||||
character?: string;
|
||||
limit?: number;
|
||||
cursor?: string | null;
|
||||
}
|
||||
|
||||
export interface IPrivateRoomRepository {
|
||||
getMoments(
|
||||
input?: GetPrivateRoomMomentsInput,
|
||||
): Promise<Result<PrivateRoomMomentsResponse>>;
|
||||
getAlbums(
|
||||
input?: GetPrivateAlbumsInput,
|
||||
): Promise<Result<PrivateAlbumsResponse>>;
|
||||
|
||||
unlockMoment(
|
||||
momentId: string,
|
||||
unlockAlbum(
|
||||
albumId: string,
|
||||
expectedCost: number,
|
||||
): Promise<Result<PrivateRoomUnlockResponse>>;
|
||||
): Promise<Result<PrivateAlbumUnlockResponse>>;
|
||||
}
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
UnlockPrivateRoomMomentRequest,
|
||||
type PrivateRoomMomentsResponse,
|
||||
type PrivateRoomUnlockResponse,
|
||||
UnlockPrivateAlbumRequest,
|
||||
type PrivateAlbumsResponse,
|
||||
type PrivateAlbumUnlockResponse,
|
||||
} from "@/data/dto/private-room";
|
||||
import {
|
||||
PrivateRoomApi,
|
||||
privateRoomApi,
|
||||
} from "@/data/services/api/private_room_api";
|
||||
import type {
|
||||
GetPrivateRoomMomentsInput,
|
||||
GetPrivateAlbumsInput,
|
||||
IPrivateRoomRepository,
|
||||
} from "@/data/repositories/interfaces";
|
||||
import { Result } from "@/utils/result";
|
||||
@@ -20,20 +20,20 @@ import { createLazySingleton } from "./lazy_singleton";
|
||||
export class PrivateRoomRepository implements IPrivateRoomRepository {
|
||||
constructor(private readonly api: PrivateRoomApi) {}
|
||||
|
||||
getMoments(
|
||||
input: GetPrivateRoomMomentsInput = {},
|
||||
): Promise<Result<PrivateRoomMomentsResponse>> {
|
||||
return Result.wrap(() => this.api.getMoments(input));
|
||||
getAlbums(
|
||||
input: GetPrivateAlbumsInput = {},
|
||||
): Promise<Result<PrivateAlbumsResponse>> {
|
||||
return Result.wrap(() => this.api.getAlbums(input));
|
||||
}
|
||||
|
||||
unlockMoment(
|
||||
momentId: string,
|
||||
unlockAlbum(
|
||||
albumId: string,
|
||||
expectedCost: number,
|
||||
): Promise<Result<PrivateRoomUnlockResponse>> {
|
||||
): Promise<Result<PrivateAlbumUnlockResponse>> {
|
||||
return Result.wrap(() =>
|
||||
this.api.unlockMoment(
|
||||
momentId,
|
||||
UnlockPrivateRoomMomentRequest.from({ expectedCost }),
|
||||
this.api.unlockAlbum(
|
||||
albumId,
|
||||
UnlockPrivateAlbumRequest.from({ expectedCost }),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export * from "./private_room";
|
||||
export * from "./private_room_moments_response";
|
||||
export * from "./private_room_unlock_response";
|
||||
export * from "./unlock_private_room_moment_request";
|
||||
export * from "./private_album";
|
||||
export * from "./private_albums_response";
|
||||
export * from "./private_album_unlock_response";
|
||||
export * from "./unlock_private_album_request";
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import {
|
||||
arrayOrEmpty,
|
||||
booleanOrFalse,
|
||||
numberOrZero,
|
||||
schemaOr,
|
||||
stringOrEmpty,
|
||||
stringOrNull,
|
||||
} from "../nullable-defaults";
|
||||
|
||||
export const PrivateAlbumImageSchema = z.object({
|
||||
url: stringOrEmpty,
|
||||
locked: booleanOrFalse,
|
||||
index: numberOrZero,
|
||||
});
|
||||
|
||||
export const PrivateAlbumLockDetailSchema = z.object({
|
||||
locked: booleanOrFalse,
|
||||
});
|
||||
|
||||
export const PrivateAlbumSchema = z.object({
|
||||
albumId: stringOrEmpty,
|
||||
title: stringOrEmpty,
|
||||
content: stringOrNull,
|
||||
previewText: stringOrEmpty,
|
||||
imageCount: numberOrZero,
|
||||
images: arrayOrEmpty(PrivateAlbumImageSchema),
|
||||
locked: booleanOrFalse,
|
||||
unlocked: booleanOrFalse,
|
||||
unlockCost: numberOrZero,
|
||||
publishedAt: stringOrNull,
|
||||
lockDetail: schemaOr(PrivateAlbumLockDetailSchema, { locked: false }),
|
||||
});
|
||||
|
||||
export type PrivateAlbumImageData = z.output<typeof PrivateAlbumImageSchema>;
|
||||
export type PrivateAlbumInput = z.input<typeof PrivateAlbumSchema>;
|
||||
export type PrivateAlbumData = z.output<typeof PrivateAlbumSchema>;
|
||||
@@ -0,0 +1,42 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import {
|
||||
arrayOrEmpty,
|
||||
booleanOrFalse,
|
||||
numberOrZero,
|
||||
stringOrEmpty,
|
||||
} from "../nullable-defaults";
|
||||
import { PrivateAlbumImageSchema } from "./private_album";
|
||||
|
||||
export const PrivateAlbumUnlockReasonSchema = z.enum([
|
||||
"ok",
|
||||
"already_unlocked",
|
||||
"insufficient_credits",
|
||||
"cost_changed",
|
||||
"unlock_in_progress",
|
||||
"deduct_failed",
|
||||
"persist_failed_refunded",
|
||||
"not_found",
|
||||
]);
|
||||
|
||||
export const PrivateAlbumUnlockResponseSchema = z.object({
|
||||
albumId: stringOrEmpty,
|
||||
locked: booleanOrFalse,
|
||||
unlocked: booleanOrFalse,
|
||||
reason: PrivateAlbumUnlockReasonSchema.or(stringOrEmpty),
|
||||
unlockCost: numberOrZero,
|
||||
requiredCredits: numberOrZero,
|
||||
creditBalance: numberOrZero,
|
||||
shortfallCredits: numberOrZero,
|
||||
images: arrayOrEmpty(PrivateAlbumImageSchema),
|
||||
});
|
||||
|
||||
export type PrivateAlbumUnlockReason = z.output<
|
||||
typeof PrivateAlbumUnlockReasonSchema
|
||||
>;
|
||||
export type PrivateAlbumUnlockResponseInput = z.input<
|
||||
typeof PrivateAlbumUnlockResponseSchema
|
||||
>;
|
||||
export type PrivateAlbumUnlockResponseData = z.output<
|
||||
typeof PrivateAlbumUnlockResponseSchema
|
||||
>;
|
||||
@@ -0,0 +1,16 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { arrayOrEmpty, numberOrZero } from "../nullable-defaults";
|
||||
import { PrivateAlbumSchema } from "./private_album";
|
||||
|
||||
export const PrivateAlbumsResponseSchema = z.object({
|
||||
items: arrayOrEmpty(PrivateAlbumSchema),
|
||||
creditBalance: numberOrZero,
|
||||
});
|
||||
|
||||
export type PrivateAlbumsResponseInput = z.input<
|
||||
typeof PrivateAlbumsResponseSchema
|
||||
>;
|
||||
export type PrivateAlbumsResponseData = z.output<
|
||||
typeof PrivateAlbumsResponseSchema
|
||||
>;
|
||||
@@ -1,102 +0,0 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import {
|
||||
arrayOrEmpty,
|
||||
booleanOrFalse,
|
||||
numberOrZero,
|
||||
schemaOr,
|
||||
stringOrEmpty,
|
||||
stringOrNull,
|
||||
} from "../nullable-defaults";
|
||||
|
||||
export const PrivateRoomProfileSchema = z.object({
|
||||
characterId: stringOrEmpty,
|
||||
displayName: stringOrEmpty,
|
||||
authorName: stringOrEmpty,
|
||||
avatarUrl: stringOrNull,
|
||||
coverUrl: stringOrNull,
|
||||
title: stringOrEmpty,
|
||||
subtitle: stringOrEmpty,
|
||||
});
|
||||
|
||||
export const PrivateRoomImageSchema = z.object({
|
||||
url: stringOrNull,
|
||||
type: stringOrEmpty,
|
||||
locked: booleanOrFalse,
|
||||
index: numberOrZero,
|
||||
});
|
||||
|
||||
export const PrivateRoomLockDetailSchema = z.object({
|
||||
locked: booleanOrFalse,
|
||||
showContent: booleanOrFalse,
|
||||
showUpgrade: booleanOrFalse,
|
||||
reason: stringOrNull,
|
||||
hint: stringOrNull,
|
||||
actionLabel: stringOrNull,
|
||||
type: stringOrNull,
|
||||
requiredCredits: numberOrZero,
|
||||
currentCredits: numberOrZero,
|
||||
shortfallCredits: numberOrZero,
|
||||
mediaCount: numberOrZero,
|
||||
unlockCostPerImage: numberOrZero,
|
||||
detail: z.unknown().nullable().default(null),
|
||||
});
|
||||
|
||||
export const PrivateRoomAuthorSchema = z.object({
|
||||
id: stringOrEmpty,
|
||||
name: stringOrEmpty,
|
||||
avatarUrl: stringOrNull,
|
||||
});
|
||||
|
||||
export const PrivateRoomMomentSchema = z.object({
|
||||
momentId: stringOrEmpty,
|
||||
source: stringOrEmpty,
|
||||
sourceId: stringOrEmpty,
|
||||
characterId: stringOrEmpty,
|
||||
author: schemaOr(PrivateRoomAuthorSchema, {
|
||||
id: "",
|
||||
name: "",
|
||||
avatarUrl: null,
|
||||
}),
|
||||
createdAt: stringOrNull,
|
||||
publishedAt: stringOrNull,
|
||||
timeText: stringOrEmpty,
|
||||
title: stringOrEmpty,
|
||||
content: stringOrNull,
|
||||
text: stringOrNull,
|
||||
textPreview: stringOrEmpty,
|
||||
mediaCount: numberOrZero,
|
||||
images: arrayOrEmpty(PrivateRoomImageSchema),
|
||||
locked: booleanOrFalse,
|
||||
unlocked: booleanOrFalse,
|
||||
unlockCost: numberOrZero,
|
||||
unlockCostPerImage: numberOrZero,
|
||||
requiredCredits: numberOrZero,
|
||||
currency: stringOrEmpty,
|
||||
lockDetail: schemaOr(PrivateRoomLockDetailSchema, {
|
||||
locked: false,
|
||||
showContent: false,
|
||||
showUpgrade: false,
|
||||
reason: null,
|
||||
hint: null,
|
||||
actionLabel: null,
|
||||
type: null,
|
||||
requiredCredits: 0,
|
||||
currentCredits: 0,
|
||||
shortfallCredits: 0,
|
||||
mediaCount: 0,
|
||||
unlockCostPerImage: 0,
|
||||
detail: null,
|
||||
}),
|
||||
});
|
||||
|
||||
export type PrivateRoomProfileData = z.output<
|
||||
typeof PrivateRoomProfileSchema
|
||||
>;
|
||||
export type PrivateRoomImageData = z.output<typeof PrivateRoomImageSchema>;
|
||||
export type PrivateRoomLockDetailData = z.output<
|
||||
typeof PrivateRoomLockDetailSchema
|
||||
>;
|
||||
export type PrivateRoomAuthorData = z.output<typeof PrivateRoomAuthorSchema>;
|
||||
export type PrivateRoomMomentInput = z.input<typeof PrivateRoomMomentSchema>;
|
||||
export type PrivateRoomMomentData = z.output<typeof PrivateRoomMomentSchema>;
|
||||
@@ -1,41 +0,0 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import {
|
||||
arrayOrEmpty,
|
||||
booleanOrFalse,
|
||||
numberOrZero,
|
||||
schemaOr,
|
||||
stringOrEmpty,
|
||||
stringOrNull,
|
||||
} from "../nullable-defaults";
|
||||
import {
|
||||
PrivateRoomMomentSchema,
|
||||
PrivateRoomProfileSchema,
|
||||
} from "./private_room";
|
||||
|
||||
export const PrivateRoomMomentsResponseSchema = z.object({
|
||||
profile: schemaOr(PrivateRoomProfileSchema, {
|
||||
characterId: "elio",
|
||||
displayName: "Elio Silvestri",
|
||||
authorName: "Elio Silvestri",
|
||||
avatarUrl: null,
|
||||
coverUrl: null,
|
||||
title: "Elio Private room",
|
||||
subtitle: "Join me, unlock my private room",
|
||||
}),
|
||||
items: arrayOrEmpty(PrivateRoomMomentSchema),
|
||||
nextCursor: stringOrNull,
|
||||
hasMore: booleanOrFalse,
|
||||
creditBalance: numberOrZero,
|
||||
unlockCostDefault: numberOrZero,
|
||||
unlockCostPerImage: numberOrZero,
|
||||
currency: stringOrEmpty,
|
||||
source: stringOrEmpty,
|
||||
});
|
||||
|
||||
export type PrivateRoomMomentsResponseInput = z.input<
|
||||
typeof PrivateRoomMomentsResponseSchema
|
||||
>;
|
||||
export type PrivateRoomMomentsResponseData = z.output<
|
||||
typeof PrivateRoomMomentsResponseSchema
|
||||
>;
|
||||
@@ -1,36 +0,0 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { booleanOrFalse, numberOrZero, stringOrEmpty } from "../nullable-defaults";
|
||||
import { PrivateRoomMomentSchema } from "./private_room";
|
||||
|
||||
export const PrivateRoomUnlockReasonSchema = z.enum([
|
||||
"ok",
|
||||
"already_unlocked",
|
||||
"insufficient_credits",
|
||||
"cost_changed",
|
||||
"not_found",
|
||||
"no_media",
|
||||
"deduct_failed",
|
||||
"ok_persist_failed",
|
||||
]);
|
||||
|
||||
export const PrivateRoomUnlockResponseSchema =
|
||||
PrivateRoomMomentSchema.extend({
|
||||
reason: PrivateRoomUnlockReasonSchema.or(stringOrEmpty),
|
||||
creditsCharged: numberOrZero,
|
||||
creditBalance: numberOrZero,
|
||||
previousCreditBalance: numberOrZero,
|
||||
currentCredits: numberOrZero,
|
||||
shortfallCredits: numberOrZero,
|
||||
persisted: booleanOrFalse,
|
||||
});
|
||||
|
||||
export type PrivateRoomUnlockReason = z.output<
|
||||
typeof PrivateRoomUnlockReasonSchema
|
||||
>;
|
||||
export type PrivateRoomUnlockResponseInput = z.input<
|
||||
typeof PrivateRoomUnlockResponseSchema
|
||||
>;
|
||||
export type PrivateRoomUnlockResponseData = z.output<
|
||||
typeof PrivateRoomUnlockResponseSchema
|
||||
>;
|
||||
@@ -0,0 +1,14 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { numberOrZero } from "../nullable-defaults";
|
||||
|
||||
export const UnlockPrivateAlbumRequestSchema = z.object({
|
||||
expectedCost: numberOrZero,
|
||||
});
|
||||
|
||||
export type UnlockPrivateAlbumRequestInput = z.input<
|
||||
typeof UnlockPrivateAlbumRequestSchema
|
||||
>;
|
||||
export type UnlockPrivateAlbumRequestData = z.output<
|
||||
typeof UnlockPrivateAlbumRequestSchema
|
||||
>;
|
||||
@@ -1,14 +0,0 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { numberOrZero } from "../nullable-defaults";
|
||||
|
||||
export const UnlockPrivateRoomMomentRequestSchema = z.object({
|
||||
expectedCost: numberOrZero,
|
||||
});
|
||||
|
||||
export type UnlockPrivateRoomMomentRequestInput = z.input<
|
||||
typeof UnlockPrivateRoomMomentRequestSchema
|
||||
>;
|
||||
export type UnlockPrivateRoomMomentRequestData = z.output<
|
||||
typeof UnlockPrivateRoomMomentRequestSchema
|
||||
>;
|
||||
@@ -84,12 +84,12 @@ export class ApiPath {
|
||||
static readonly chatUnlockHistory = `${ApiPath._chat}/unlock-history`;
|
||||
|
||||
// ============ 私密空间相关 ============
|
||||
/** 获取私密空间动态列表 */
|
||||
static readonly privateRoomMoments = `${ApiPath._privateRoom}/moments`;
|
||||
/** 获取私密图片包列表 */
|
||||
static readonly privateRoomAlbums = `${ApiPath._privateRoom}/albums`;
|
||||
|
||||
/** 解锁私密空间动态 */
|
||||
static privateRoomMomentUnlock(momentId: string): string {
|
||||
return `${ApiPath.privateRoomMoments}/${encodeURIComponent(momentId)}/unlock`;
|
||||
/** 解锁私密图片包 */
|
||||
static privateRoomAlbumUnlock(albumId: string): string {
|
||||
return `${ApiPath.privateRoomAlbums}/${encodeURIComponent(albumId)}/unlock`;
|
||||
}
|
||||
|
||||
// ============ 数据看板相关 ============
|
||||
|
||||
@@ -1,48 +1,46 @@
|
||||
import {
|
||||
PrivateRoomMomentsResponse,
|
||||
PrivateRoomUnlockResponse,
|
||||
UnlockPrivateRoomMomentRequest,
|
||||
PrivateAlbumsResponse,
|
||||
PrivateAlbumUnlockResponse,
|
||||
UnlockPrivateAlbumRequest,
|
||||
} from "@/data/dto/private-room";
|
||||
|
||||
import { ApiPath } from "./api_path";
|
||||
import { httpClient } from "./http_client";
|
||||
import { type ApiEnvelope, unwrap } from "./response_helper";
|
||||
|
||||
export interface GetPrivateRoomMomentsInput {
|
||||
export interface GetPrivateAlbumsInput {
|
||||
character?: string;
|
||||
limit?: number;
|
||||
cursor?: string | null;
|
||||
}
|
||||
|
||||
export class PrivateRoomApi {
|
||||
async getMoments(
|
||||
input: GetPrivateRoomMomentsInput = {},
|
||||
): Promise<PrivateRoomMomentsResponse> {
|
||||
async getAlbums(
|
||||
input: GetPrivateAlbumsInput = {},
|
||||
): Promise<PrivateAlbumsResponse> {
|
||||
const env = await httpClient<ApiEnvelope<unknown>>(
|
||||
ApiPath.privateRoomMoments,
|
||||
ApiPath.privateRoomAlbums,
|
||||
{
|
||||
query: {
|
||||
character: input.character ?? "elio",
|
||||
limit: input.limit ?? 20,
|
||||
...(input.cursor ? { cursor: input.cursor } : {}),
|
||||
},
|
||||
},
|
||||
);
|
||||
return PrivateRoomMomentsResponse.fromJson(unwrap(env));
|
||||
return PrivateAlbumsResponse.fromJson(unwrap(env));
|
||||
}
|
||||
|
||||
async unlockMoment(
|
||||
momentId: string,
|
||||
body: UnlockPrivateRoomMomentRequest,
|
||||
): Promise<PrivateRoomUnlockResponse> {
|
||||
async unlockAlbum(
|
||||
albumId: string,
|
||||
body: UnlockPrivateAlbumRequest,
|
||||
): Promise<PrivateAlbumUnlockResponse> {
|
||||
const env = await httpClient<ApiEnvelope<unknown>>(
|
||||
ApiPath.privateRoomMomentUnlock(momentId),
|
||||
ApiPath.privateRoomAlbumUnlock(albumId),
|
||||
{
|
||||
method: "POST",
|
||||
body: body.toJson(),
|
||||
},
|
||||
);
|
||||
return PrivateRoomUnlockResponse.fromJson(unwrap(env));
|
||||
return PrivateAlbumUnlockResponse.fromJson(unwrap(env));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user