feat(tip): add selectable coffee gift tiers
This commit is contained in:
@@ -32,28 +32,8 @@ describe("external entry navigation", () => {
|
||||
expect(resolveExternalEntryTarget({ target: "sidebar" })).toBe(ROUTES.chat);
|
||||
});
|
||||
|
||||
it("routes tip entries to the requested coffee type", () => {
|
||||
expect(
|
||||
resolveExternalEntryDestination({
|
||||
target: "tip",
|
||||
coffeeType: "medium",
|
||||
}),
|
||||
).toBe("/tip?coffee_type=medium");
|
||||
expect(
|
||||
resolveExternalEntryDestination({
|
||||
target: "tip",
|
||||
coffeeType: "large",
|
||||
}),
|
||||
).toBe("/tip?coffee_type=large");
|
||||
});
|
||||
|
||||
it("defaults invalid tip coffee types to small", () => {
|
||||
expect(
|
||||
resolveExternalEntryDestination({
|
||||
target: "tip",
|
||||
coffeeType: "unknown",
|
||||
}),
|
||||
).toBe("/tip?coffee_type=small");
|
||||
it("routes every tip entry to the tier selection page", () => {
|
||||
expect(resolveExternalEntryDestination({ target: "tip" })).toBe("/tip");
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -5,11 +5,6 @@ import { UserStorage } from "@/data/storage/user/user_storage";
|
||||
import type { PendingChatPromotionType } from "@/data/storage/navigation";
|
||||
import type { LoginStatus } from "@/data/dto/auth";
|
||||
import { ROUTES } from "@/router/routes";
|
||||
import {
|
||||
buildTipCoffeePath,
|
||||
DEFAULT_TIP_COFFEE_TYPE,
|
||||
resolveTipCoffeeType,
|
||||
} from "@/lib/tip/tip_coffee";
|
||||
|
||||
export type ExternalEntryTarget =
|
||||
| typeof ROUTES.chat
|
||||
@@ -27,10 +22,6 @@ export interface ExternalEntryTargetInput {
|
||||
target?: string | null;
|
||||
}
|
||||
|
||||
export interface ExternalEntryDestinationInput extends ExternalEntryTargetInput {
|
||||
coffeeType?: string | null;
|
||||
}
|
||||
|
||||
export interface ExternalEntryPromotionInput {
|
||||
mode?: string | null;
|
||||
promotionType?: string | null;
|
||||
@@ -88,14 +79,8 @@ export function resolveExternalEntryTarget({
|
||||
|
||||
export function resolveExternalEntryDestination({
|
||||
target,
|
||||
coffeeType,
|
||||
}: ExternalEntryDestinationInput): string {
|
||||
const resolvedTarget = resolveExternalEntryTarget({ target });
|
||||
if (resolvedTarget !== ROUTES.tip) return resolvedTarget;
|
||||
|
||||
return buildTipCoffeePath(
|
||||
resolveTipCoffeeType(coffeeType) ?? DEFAULT_TIP_COFFEE_TYPE,
|
||||
);
|
||||
}: ExternalEntryTargetInput): string {
|
||||
return resolveExternalEntryTarget({ target });
|
||||
}
|
||||
|
||||
export async function persistExternalEntryPayload({
|
||||
|
||||
@@ -23,12 +23,12 @@ describe("pending payment order helpers", () => {
|
||||
).toBe("/tip?payChannel=ezpay&paymentReturn=1&coffee_type=large");
|
||||
});
|
||||
|
||||
it("defaults legacy tip payment returns to small coffee", () => {
|
||||
it("defaults legacy tip payment returns to medium coffee", () => {
|
||||
expect(
|
||||
buildPendingPaymentSubscriptionUrl({
|
||||
payChannel: "ezpay",
|
||||
subscriptionType: "tip",
|
||||
}),
|
||||
).toBe("/tip?payChannel=ezpay&paymentReturn=1&coffee_type=small");
|
||||
).toBe("/tip?payChannel=ezpay&paymentReturn=1&coffee_type=medium");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,8 +2,10 @@ import { describe, expect, it } from "vitest";
|
||||
|
||||
import {
|
||||
buildTipCoffeePath,
|
||||
DEFAULT_TIP_COFFEE_TYPE,
|
||||
getTipCoffeeOption,
|
||||
resolveTipCoffeeType,
|
||||
TIP_COFFEE_OPTIONS,
|
||||
} from "../tip_coffee";
|
||||
|
||||
describe("tip coffee configuration", () => {
|
||||
@@ -21,18 +23,33 @@ describe("tip coffee configuration", () => {
|
||||
it("provides the configured plan and fallback price for each type", () => {
|
||||
expect(getTipCoffeeOption("small")).toMatchObject({
|
||||
amountCents: 499,
|
||||
displayName: "Velvet Espresso",
|
||||
image: { src: "/images/tip/small.jpg", width: 736, height: 736 },
|
||||
planId: "tip_coffee_usd_4_99",
|
||||
});
|
||||
expect(getTipCoffeeOption("medium")).toMatchObject({
|
||||
amountCents: 999,
|
||||
displayName: "Golden Reserve",
|
||||
image: { src: "/images/tip/medium.png", width: 1024, height: 1024 },
|
||||
planId: "tip_coffee_usd_9_99",
|
||||
});
|
||||
expect(getTipCoffeeOption("large")).toMatchObject({
|
||||
amountCents: 1999,
|
||||
displayName: "Imperial Grand Cru",
|
||||
image: { src: "/images/tip/large.png", width: 1024, height: 1024 },
|
||||
planId: "tip_coffee_usd_19_99",
|
||||
});
|
||||
});
|
||||
|
||||
it("lists all tiers in display order and defaults to medium", () => {
|
||||
expect(TIP_COFFEE_OPTIONS.map(({ type }) => type)).toEqual([
|
||||
"small",
|
||||
"medium",
|
||||
"large",
|
||||
]);
|
||||
expect(DEFAULT_TIP_COFFEE_TYPE).toBe("medium");
|
||||
});
|
||||
|
||||
it("builds canonical tip paths", () => {
|
||||
expect(buildTipCoffeePath("small")).toBe("/tip?coffee_type=small");
|
||||
expect(buildTipCoffeePath("medium")).toBe("/tip?coffee_type=medium");
|
||||
|
||||
+40
-10
@@ -1,38 +1,68 @@
|
||||
import { ROUTES } from "@/router/routes";
|
||||
|
||||
export const TIP_COFFEE_TYPE_PARAM = "coffee_type";
|
||||
export const DEFAULT_TIP_COFFEE_TYPE = "small";
|
||||
export const DEFAULT_TIP_COFFEE_TYPE = "medium";
|
||||
|
||||
export type TipCoffeeType = "small" | "medium" | "large";
|
||||
|
||||
export interface TipCoffeeOption {
|
||||
type: TipCoffeeType;
|
||||
amountCents: number;
|
||||
fallbackName: string;
|
||||
planId: string;
|
||||
readonly type: TipCoffeeType;
|
||||
readonly tierLabel: string;
|
||||
readonly amountCents: number;
|
||||
readonly displayName: string;
|
||||
readonly image: {
|
||||
readonly src: string;
|
||||
readonly width: number;
|
||||
readonly height: number;
|
||||
};
|
||||
readonly planId: string;
|
||||
}
|
||||
|
||||
const TIP_COFFEE_OPTIONS: Record<TipCoffeeType, TipCoffeeOption> = {
|
||||
const TIP_COFFEE_OPTION_BY_TYPE: Record<TipCoffeeType, TipCoffeeOption> = {
|
||||
small: {
|
||||
type: "small",
|
||||
tierLabel: "Small",
|
||||
amountCents: 499,
|
||||
fallbackName: "Small Coffee",
|
||||
displayName: "Velvet Espresso",
|
||||
image: {
|
||||
src: "/images/tip/small.jpg",
|
||||
width: 736,
|
||||
height: 736,
|
||||
},
|
||||
planId: "tip_coffee_usd_4_99",
|
||||
},
|
||||
medium: {
|
||||
type: "medium",
|
||||
tierLabel: "Medium",
|
||||
amountCents: 999,
|
||||
fallbackName: "Medium Coffee",
|
||||
displayName: "Golden Reserve",
|
||||
image: {
|
||||
src: "/images/tip/medium.png",
|
||||
width: 1024,
|
||||
height: 1024,
|
||||
},
|
||||
planId: "tip_coffee_usd_9_99",
|
||||
},
|
||||
large: {
|
||||
type: "large",
|
||||
tierLabel: "Large",
|
||||
amountCents: 1999,
|
||||
fallbackName: "Large Coffee",
|
||||
displayName: "Imperial Grand Cru",
|
||||
image: {
|
||||
src: "/images/tip/large.png",
|
||||
width: 1024,
|
||||
height: 1024,
|
||||
},
|
||||
planId: "tip_coffee_usd_19_99",
|
||||
},
|
||||
};
|
||||
|
||||
export const TIP_COFFEE_OPTIONS: readonly TipCoffeeOption[] = [
|
||||
TIP_COFFEE_OPTION_BY_TYPE.small,
|
||||
TIP_COFFEE_OPTION_BY_TYPE.medium,
|
||||
TIP_COFFEE_OPTION_BY_TYPE.large,
|
||||
];
|
||||
|
||||
export function resolveTipCoffeeType(
|
||||
value: string | null | undefined,
|
||||
): TipCoffeeType | null {
|
||||
@@ -48,7 +78,7 @@ export function resolveTipCoffeeType(
|
||||
}
|
||||
|
||||
export function getTipCoffeeOption(type: TipCoffeeType): TipCoffeeOption {
|
||||
return TIP_COFFEE_OPTIONS[type];
|
||||
return TIP_COFFEE_OPTION_BY_TYPE[type];
|
||||
}
|
||||
|
||||
export function buildTipCoffeePath(type: TipCoffeeType): string {
|
||||
|
||||
Reference in New Issue
Block a user