feat(analytics): add behavior and payment funnel tracking
This commit is contained in:
@@ -7,6 +7,7 @@ import {
|
||||
PaymentOrderStatusResponse,
|
||||
PaymentPlansResponse,
|
||||
} from "@/data/dto/payment";
|
||||
import { behaviorAnalytics } from "@/lib/analytics";
|
||||
import { paymentMachine } from "@/stores/payment/payment-machine";
|
||||
import {
|
||||
MAX_ORDER_POLLING_MS,
|
||||
@@ -94,6 +95,7 @@ const tipPlan = {
|
||||
function createTestPaymentMachine(
|
||||
overrides: Partial<{
|
||||
createOrderSpy: CreateOrderSpy;
|
||||
createOrderError: Error;
|
||||
orderStatus: "pending" | "paid" | "failed";
|
||||
orderStatuses: Array<"pending" | "paid" | "failed">;
|
||||
}> = {},
|
||||
@@ -124,6 +126,7 @@ function createTestPaymentMachine(
|
||||
CreateOrderInput
|
||||
>(async ({ input }) => {
|
||||
createOrderSpy(input);
|
||||
if (overrides.createOrderError) throw overrides.createOrderError;
|
||||
return CreatePaymentOrderResponse.from({
|
||||
orderId: "pay_test_001",
|
||||
payParams: {
|
||||
@@ -152,6 +155,7 @@ function createTestPaymentMachine(
|
||||
describe("paymentMachine", () => {
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("loads plans and selects the default monthly VIP plan", async () => {
|
||||
@@ -460,6 +464,18 @@ describe("paymentMachine", () => {
|
||||
});
|
||||
|
||||
it("creates an order, polls it, and reaches paid state", async () => {
|
||||
const createOrderStart = vi
|
||||
.spyOn(behaviorAnalytics, "createOrderStart")
|
||||
.mockImplementation(() => undefined);
|
||||
const createOrderSuccess = vi
|
||||
.spyOn(behaviorAnalytics, "createOrderSuccess")
|
||||
.mockImplementation(() => undefined);
|
||||
const statusPollStart = vi
|
||||
.spyOn(behaviorAnalytics, "statusPollStart")
|
||||
.mockImplementation(() => undefined);
|
||||
const statusPollUpdate = vi
|
||||
.spyOn(behaviorAnalytics, "statusPollUpdate")
|
||||
.mockImplementation(() => undefined);
|
||||
const createOrderSpy = vi.fn<CreateOrderSpy>();
|
||||
const actor = createActor(
|
||||
createTestPaymentMachine({ createOrderSpy }),
|
||||
@@ -481,6 +497,50 @@ describe("paymentMachine", () => {
|
||||
expect(context.orderStatus).toBe("paid");
|
||||
expect(context.orderPollingStartedAt).toBeNull();
|
||||
expect(context.launchNonce).toBe(1);
|
||||
expect(createOrderStart).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ planId: "vip_monthly" }),
|
||||
"stripe",
|
||||
);
|
||||
expect(createOrderSuccess).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ planId: "vip_monthly" }),
|
||||
"pay_test_001",
|
||||
"stripe",
|
||||
);
|
||||
expect(statusPollStart).toHaveBeenCalledWith(
|
||||
"pay_test_001",
|
||||
expect.objectContaining({ planId: "vip_monthly" }),
|
||||
);
|
||||
expect(statusPollUpdate).toHaveBeenCalledWith(
|
||||
"pay_test_001",
|
||||
"paid",
|
||||
expect.objectContaining({ planId: "vip_monthly" }),
|
||||
);
|
||||
|
||||
actor.stop();
|
||||
});
|
||||
|
||||
it("tracks order creation failures without interrupting failure handling", async () => {
|
||||
const createOrderFailed = vi
|
||||
.spyOn(behaviorAnalytics, "createOrderFailed")
|
||||
.mockImplementation(() => undefined);
|
||||
const actor = createActor(
|
||||
createTestPaymentMachine({
|
||||
createOrderError: new Error("backend unavailable"),
|
||||
}),
|
||||
).start();
|
||||
|
||||
actor.send({ type: "PaymentInit" });
|
||||
await waitFor(actor, (snapshot) => snapshot.matches("ready"));
|
||||
actor.send({ type: "PaymentCreateOrderSubmitted" });
|
||||
await waitFor(actor, (snapshot) => snapshot.matches("failed"));
|
||||
|
||||
expect(createOrderFailed).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ planId: "vip_monthly" }),
|
||||
"stripe",
|
||||
);
|
||||
expect(actor.getSnapshot().context.errorMessage).toBe(
|
||||
"backend unavailable",
|
||||
);
|
||||
|
||||
actor.stop();
|
||||
});
|
||||
@@ -533,6 +593,9 @@ describe("paymentMachine", () => {
|
||||
});
|
||||
|
||||
it("moves to failed state when a pending order exceeds polling timeout", async () => {
|
||||
const statusPollTimeout = vi
|
||||
.spyOn(behaviorAnalytics, "statusPollTimeout")
|
||||
.mockImplementation(() => undefined);
|
||||
const now = new Date("2026-06-30T00:00:00.000Z");
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(now);
|
||||
@@ -555,6 +618,10 @@ describe("paymentMachine", () => {
|
||||
expect(context.orderStatus).toBe("failed");
|
||||
expect(context.orderPollingStartedAt).toBeNull();
|
||||
expect(context.errorMessage).toBe(PAYMENT_TIMEOUT_ERROR_MESSAGE);
|
||||
expect(statusPollTimeout).toHaveBeenCalledWith(
|
||||
"pay_test_001",
|
||||
expect.objectContaining({ planId: "vip_monthly" }),
|
||||
);
|
||||
|
||||
actor.stop();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user