feat(payment): implement first recharge offer functionality and UI updates

This commit is contained in:
2026-07-02 14:39:09 +08:00
parent 7b35fd18c5
commit d4de1370e8
20 changed files with 607 additions and 50 deletions
@@ -31,6 +31,9 @@ describe("PaymentPlan", () => {
originalAmountCents: 2499,
dailyPriceCents: 66,
currency: "JPY",
isFirstRechargeOffer: false,
firstRechargeDiscountPercent: null,
promotionType: null,
});
});
@@ -73,4 +76,42 @@ describe("PaymentPlansResponse", () => {
expect(response.plans).toHaveLength(1);
expect(response.plans[0]?.dolAmount).toBe(1000);
});
it("parses first recharge offer metadata", () => {
const response = PaymentPlansResponse.from({
isFirstRecharge: true,
firstRechargeOffer: {
enabled: true,
type: "first_recharge_half_price",
discountPercent: 50,
},
plans: [
{
planId: "vip_monthly",
planName: "VIP Monthly",
orderType: "vip_monthly",
vipDays: 30,
dolAmount: null,
creditBalance: 3000,
amountCents: 995,
originalAmountCents: 1990,
dailyPriceCents: null,
currency: "USD",
isFirstRechargeOffer: true,
firstRechargeDiscountPercent: 50,
promotionType: "first_recharge_half_price",
},
],
});
expect(response.isFirstRecharge).toBe(true);
expect(response.firstRechargeOffer?.discountPercent).toBe(50);
expect(response.plans[0]).toMatchObject({
amountCents: 995,
originalAmountCents: 1990,
isFirstRechargeOffer: true,
firstRechargeDiscountPercent: 50,
promotionType: "first_recharge_half_price",
});
});
});
+3
View File
@@ -18,6 +18,9 @@ export class PaymentPlan {
declare readonly originalAmountCents: number | null;
declare readonly dailyPriceCents: number | null;
declare readonly currency: string;
declare readonly isFirstRechargeOffer: boolean;
declare readonly firstRechargeDiscountPercent: number | null;
declare readonly promotionType: string | null;
private constructor(input: PaymentPlanInput) {
const data = PaymentPlanSchema.parse(input);
@@ -3,6 +3,7 @@
*/
import {
PaymentPlansResponseSchema,
type FirstRechargeOfferData,
type PaymentPlansResponseData,
type PaymentPlansResponseInput,
} from "@/data/schemas/payment/payment_plans_response";
@@ -10,11 +11,15 @@ import {
import { PaymentPlan } from "./payment_plan";
export class PaymentPlansResponse {
declare readonly isFirstRecharge: boolean;
declare readonly firstRechargeOffer: FirstRechargeOfferData | null;
declare readonly plans: PaymentPlan[];
private constructor(input: PaymentPlansResponseInput) {
const data = PaymentPlansResponseSchema.parse(input);
Object.assign(this, {
isFirstRecharge: data.isFirstRecharge,
firstRechargeOffer: data.firstRechargeOffer,
plans: data.plans.map((plan) => PaymentPlan.from(plan)),
});
Object.freeze(this);
@@ -30,7 +35,11 @@ export class PaymentPlansResponse {
toJson(): PaymentPlansResponseData {
return {
isFirstRecharge: this.isFirstRecharge,
firstRechargeOffer: this.firstRechargeOffer,
plans: this.plans.map((plan) => plan.toJson()),
};
}
}
export type FirstRechargeOffer = FirstRechargeOfferData;
@@ -0,0 +1,135 @@
{
"code": 200,
"message": "success",
"success": true,
"data": {
"isFirstRecharge": true,
"firstRechargeOffer": {
"enabled": true,
"type": "first_recharge_half_price",
"discountPercent": 50
},
"plans": [
{
"planId": "vip_monthly",
"planName": "VIP Monthly",
"orderType": "vip_monthly",
"vipDays": 30,
"dolAmount": null,
"creditBalance": 3000,
"amountCents": 995,
"originalAmountCents": 1990,
"dailyPriceCents": 33,
"currency": "USD",
"isFirstRechargeOffer": true,
"firstRechargeDiscountPercent": 50,
"promotionType": "first_recharge_half_price"
},
{
"planId": "vip_quarterly",
"planName": "VIP Quarterly",
"orderType": "vip_quarterly",
"vipDays": 90,
"dolAmount": null,
"creditBalance": 9000,
"amountCents": 2495,
"originalAmountCents": 4990,
"dailyPriceCents": 28,
"currency": "USD",
"isFirstRechargeOffer": true,
"firstRechargeDiscountPercent": 50,
"promotionType": "first_recharge_half_price"
},
{
"planId": "vip_annual",
"planName": "VIP Annual",
"orderType": "vip_annual",
"vipDays": 365,
"dolAmount": null,
"creditBalance": 36000,
"amountCents": 8995,
"originalAmountCents": 17990,
"dailyPriceCents": 25,
"currency": "USD",
"isFirstRechargeOffer": true,
"firstRechargeDiscountPercent": 50,
"promotionType": "first_recharge_half_price"
},
{
"planId": "dol_1000",
"planName": "1000 Credits",
"orderType": "dol",
"vipDays": null,
"dolAmount": 1000,
"creditBalance": 1000,
"amountCents": 495,
"originalAmountCents": 990,
"dailyPriceCents": null,
"currency": "USD",
"isFirstRechargeOffer": true,
"firstRechargeDiscountPercent": 50,
"promotionType": "first_recharge_half_price"
},
{
"planId": "dol_2000",
"planName": "2000 Credits",
"orderType": "dol",
"vipDays": null,
"dolAmount": 2000,
"creditBalance": 2000,
"amountCents": 845,
"originalAmountCents": 1690,
"dailyPriceCents": null,
"currency": "USD",
"isFirstRechargeOffer": true,
"firstRechargeDiscountPercent": 50,
"promotionType": "first_recharge_half_price"
},
{
"planId": "dol_3000",
"planName": "3000 Credits",
"orderType": "dol",
"vipDays": null,
"dolAmount": 3000,
"creditBalance": 3000,
"amountCents": 1145,
"originalAmountCents": 2290,
"dailyPriceCents": null,
"currency": "USD",
"isFirstRechargeOffer": true,
"firstRechargeDiscountPercent": 50,
"promotionType": "first_recharge_half_price"
},
{
"planId": "dol_5000",
"planName": "5000 Credits",
"orderType": "dol",
"vipDays": null,
"dolAmount": 5000,
"creditBalance": 5000,
"amountCents": 1745,
"originalAmountCents": 3490,
"dailyPriceCents": null,
"currency": "USD",
"isFirstRechargeOffer": true,
"firstRechargeDiscountPercent": 50,
"promotionType": "first_recharge_half_price"
},
{
"planId": "dol_10000",
"planName": "10000 Credits",
"orderType": "dol",
"vipDays": null,
"dolAmount": 10000,
"creditBalance": 10000,
"amountCents": 3245,
"originalAmountCents": 6490,
"dailyPriceCents": null,
"currency": "USD",
"isFirstRechargeOffer": true,
"firstRechargeDiscountPercent": 50,
"promotionType": "first_recharge_half_price"
}
]
}
}
+3
View File
@@ -14,6 +14,9 @@ export const PaymentPlanSchema = z.object({
originalAmountCents: z.number().nullable().default(null),
dailyPriceCents: z.number().nullable().default(null),
currency: z.string(),
isFirstRechargeOffer: z.boolean().default(false),
firstRechargeDiscountPercent: z.number().nullable().default(null),
promotionType: z.string().nullable().default(null),
});
export type PaymentPlanInput = z.input<typeof PaymentPlanSchema>;
@@ -5,7 +5,15 @@ import { z } from "zod";
import { PaymentPlanSchema } from "./payment_plan";
export const FirstRechargeOfferSchema = z.object({
enabled: z.boolean(),
type: z.string(),
discountPercent: z.number(),
});
export const PaymentPlansResponseSchema = z.object({
isFirstRecharge: z.boolean().default(false),
firstRechargeOffer: FirstRechargeOfferSchema.nullable().default(null),
plans: z.array(PaymentPlanSchema),
});
@@ -15,3 +23,4 @@ export type PaymentPlansResponseInput = z.input<
export type PaymentPlansResponseData = z.output<
typeof PaymentPlansResponseSchema
>;
export type FirstRechargeOfferData = z.output<typeof FirstRechargeOfferSchema>;