feat(payment): expand Stripe methods and checkout handoff
Docker Image / Build and Push Docker Image (push) Successful in 2m10s
Docker Image / Build and Push Docker Image (push) Successful in 2m10s
This commit is contained in:
@@ -1,6 +1,10 @@
|
||||
import { ExceptionHandler } from "@/core/errors";
|
||||
import type { IAuthRepository } from "@/data/repositories/interfaces";
|
||||
import {
|
||||
CheckoutHandoffConsumeRequestSchema,
|
||||
CheckoutHandoffCreateRequestSchema,
|
||||
type CheckoutHandoffCreateResponse,
|
||||
type CheckoutIntent,
|
||||
FacebookIdentityRequestSchema,
|
||||
FacebookLoginRequestSchema,
|
||||
FbIdLoginRequestSchema,
|
||||
@@ -276,6 +280,30 @@ export class AuthRepository implements IAuthRepository {
|
||||
});
|
||||
}
|
||||
|
||||
/** 创建订单前生成一次性外部浏览器支付交接链接。 */
|
||||
async createCheckoutHandoff(
|
||||
checkoutIntent: CheckoutIntent,
|
||||
): Promise<Result<CheckoutHandoffCreateResponse>> {
|
||||
return Result.wrap(() =>
|
||||
this.api.createCheckoutHandoff(
|
||||
CheckoutHandoffCreateRequestSchema.parse(checkoutIntent),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/** 消费支付交接凭证,保存后端签发的正式登录态并返回购买意图。 */
|
||||
async consumeCheckoutHandoff(
|
||||
handoffToken: string,
|
||||
): Promise<Result<CheckoutIntent>> {
|
||||
return Result.wrap(async () => {
|
||||
const response = await this.api.consumeCheckoutHandoff(
|
||||
CheckoutHandoffConsumeRequestSchema.parse({ handoffToken }),
|
||||
);
|
||||
await this._saveLoginData(response, response.loginStatus);
|
||||
return response.checkoutIntent;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新 token:先读本地的 refresh token,空则直接返回错误;
|
||||
* 调用 API 成功后写回新的 login token + refresh token。
|
||||
|
||||
@@ -13,6 +13,8 @@
|
||||
|
||||
import type { Result } from "@/utils/result";
|
||||
import type {
|
||||
CheckoutHandoffCreateResponse,
|
||||
CheckoutIntent,
|
||||
GuestLoginResponse,
|
||||
LoginStatus,
|
||||
LoginResponse,
|
||||
@@ -74,6 +76,16 @@ export interface IAuthRepository {
|
||||
/** 消费一次性充值登录凭证并建立正式会话。 */
|
||||
consumeTopUpHandoff(handoffToken: string): Promise<Result<LoginStatus>>;
|
||||
|
||||
/** 在创建订单前生成10分钟有效的外部浏览器支付链接。 */
|
||||
createCheckoutHandoff(
|
||||
checkoutIntent: CheckoutIntent,
|
||||
): Promise<Result<CheckoutHandoffCreateResponse>>;
|
||||
|
||||
/** 消费单次支付交接凭证并保存正式登录态。 */
|
||||
consumeCheckoutHandoff(
|
||||
handoffToken: string,
|
||||
): Promise<Result<CheckoutIntent>>;
|
||||
|
||||
/** 刷新 token:先读本地的 refresh token,空则返回错误。 */
|
||||
refreshToken(): Promise<Result<RefreshTokenResponse>>;
|
||||
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import {
|
||||
CheckoutHandoffConsumeResponseSchema,
|
||||
CheckoutHandoffCreateRequestSchema,
|
||||
} from "../checkout_handoff";
|
||||
|
||||
describe("checkout handoff schemas", () => {
|
||||
it("keeps only the minimal order-free purchase intent", () => {
|
||||
expect(
|
||||
CheckoutHandoffCreateRequestSchema.parse({
|
||||
planId: "vip_monthly",
|
||||
autoRenew: true,
|
||||
recipientCharacterId: "elio",
|
||||
price: 9.99,
|
||||
}),
|
||||
).toEqual({
|
||||
planId: "vip_monthly",
|
||||
autoRenew: true,
|
||||
recipientCharacterId: "elio",
|
||||
});
|
||||
});
|
||||
|
||||
it("parses the formal session and checkout intent returned after consumption", () => {
|
||||
const parsed = CheckoutHandoffConsumeResponseSchema.parse({
|
||||
user: {
|
||||
id: "00000000-0000-0000-0000-000000000123",
|
||||
username: "checkout-user",
|
||||
email: "checkout@example.com",
|
||||
platform: "web",
|
||||
},
|
||||
token: "access-token",
|
||||
refreshToken: "refresh-token",
|
||||
loginStatus: "email",
|
||||
checkoutIntent: {
|
||||
planId: "vip_monthly",
|
||||
autoRenew: true,
|
||||
},
|
||||
});
|
||||
|
||||
expect(parsed.checkoutIntent.planId).toBe("vip_monthly");
|
||||
expect(parsed.loginStatus).toBe("email");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,58 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { stringOrEmpty } from "../nullable-defaults";
|
||||
import { UserSchema } from "../user/user";
|
||||
|
||||
export const CheckoutIntentSchema = z
|
||||
.object({
|
||||
planId: z.string().min(1).max(120),
|
||||
autoRenew: z.boolean(),
|
||||
recipientCharacterId: z.string().min(1).max(80).optional(),
|
||||
commercialOfferId: z.string().min(1).max(80).optional(),
|
||||
chatActionId: z.uuid().optional(),
|
||||
})
|
||||
.readonly();
|
||||
|
||||
export const CheckoutHandoffCreateRequestSchema = CheckoutIntentSchema;
|
||||
|
||||
export const CheckoutHandoffCreateResponseSchema = z
|
||||
.object({
|
||||
externalUrl: z.url(),
|
||||
expiresAt: z.string().min(1),
|
||||
})
|
||||
.readonly();
|
||||
|
||||
export const CheckoutHandoffConsumeRequestSchema = z
|
||||
.object({
|
||||
handoffToken: z.string().min(32).max(512),
|
||||
})
|
||||
.readonly();
|
||||
|
||||
export const CheckoutHandoffConsumeResponseSchema = z
|
||||
.object({
|
||||
user: UserSchema,
|
||||
token: z.string().min(1),
|
||||
refreshToken: stringOrEmpty,
|
||||
loginStatus: z.enum([
|
||||
"email",
|
||||
"google",
|
||||
"facebook",
|
||||
"facebookMessenger",
|
||||
]),
|
||||
checkoutIntent: CheckoutIntentSchema,
|
||||
})
|
||||
.readonly();
|
||||
|
||||
export type CheckoutIntent = z.output<typeof CheckoutIntentSchema>;
|
||||
export type CheckoutHandoffCreateRequest = z.output<
|
||||
typeof CheckoutHandoffCreateRequestSchema
|
||||
>;
|
||||
export type CheckoutHandoffCreateResponse = z.output<
|
||||
typeof CheckoutHandoffCreateResponseSchema
|
||||
>;
|
||||
export type CheckoutHandoffConsumeRequest = z.output<
|
||||
typeof CheckoutHandoffConsumeRequestSchema
|
||||
>;
|
||||
export type CheckoutHandoffConsumeResponse = z.output<
|
||||
typeof CheckoutHandoffConsumeResponseSchema
|
||||
>;
|
||||
@@ -3,6 +3,7 @@
|
||||
*/
|
||||
|
||||
export * from "./facebook_user_data";
|
||||
export * from "./checkout_handoff";
|
||||
export * from "./login_status";
|
||||
export * from "./request/facebook_identity_request";
|
||||
export * from "./request/facebook_login_request";
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
"facebookLogin": { "method": "post", "path": "/api/auth/login/facebook" },
|
||||
"facebookIdLogin": { "method": "post", "path": "/api/auth/login/facebook/by-id" },
|
||||
"topUpHandoffConsume": { "method": "post", "path": "/api/auth/handoff/topup/consume" },
|
||||
"checkoutHandoffCreate": { "method": "post", "path": "/api/auth/handoff/checkout" },
|
||||
"checkoutHandoffConsume": { "method": "post", "path": "/api/auth/handoff/checkout/consume" },
|
||||
"refresh": { "method": "post", "path": "/api/auth/refresh" },
|
||||
"logout": { "method": "post", "path": "/api/auth/logout" },
|
||||
"getCurrentUser": { "method": "get", "path": "/api/auth/me" },
|
||||
|
||||
@@ -30,6 +30,12 @@ export class ApiPath {
|
||||
/** 消费一次性充值登录凭证 */
|
||||
static readonly topUpHandoffConsume = apiContract.topUpHandoffConsume.path;
|
||||
|
||||
/** 创建一次性外部浏览器支付交接链接(创建订单前)。 */
|
||||
static readonly checkoutHandoffCreate = apiContract.checkoutHandoffCreate.path;
|
||||
|
||||
/** 消费一次性外部浏览器支付交接凭证。 */
|
||||
static readonly checkoutHandoffConsume = apiContract.checkoutHandoffConsume.path;
|
||||
|
||||
/** 刷新 Token */
|
||||
static readonly refresh = apiContract.refresh.path;
|
||||
|
||||
|
||||
@@ -7,6 +7,12 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import {
|
||||
CheckoutHandoffConsumeRequest,
|
||||
CheckoutHandoffConsumeResponse,
|
||||
CheckoutHandoffConsumeResponseSchema,
|
||||
CheckoutHandoffCreateRequest,
|
||||
CheckoutHandoffCreateResponse,
|
||||
CheckoutHandoffCreateResponseSchema,
|
||||
FacebookIdentityRequest,
|
||||
FacebookIdentityResponse,
|
||||
FacebookIdentityResponseSchema,
|
||||
@@ -104,6 +110,32 @@ export class AuthApi {
|
||||
);
|
||||
}
|
||||
|
||||
/** 在创建支付订单前生成一次性外部浏览器交接链接。 */
|
||||
async createCheckoutHandoff(
|
||||
body: CheckoutHandoffCreateRequest,
|
||||
): Promise<CheckoutHandoffCreateResponse> {
|
||||
const env = await httpClient<ApiEnvelope<unknown>>(
|
||||
ApiPath.checkoutHandoffCreate,
|
||||
{ method: "POST", body },
|
||||
);
|
||||
return CheckoutHandoffCreateResponseSchema.parse(
|
||||
unwrap(env) as Record<string, unknown>,
|
||||
);
|
||||
}
|
||||
|
||||
/** 消费一次性支付交接凭证并恢复正式登录态与购买意图。 */
|
||||
async consumeCheckoutHandoff(
|
||||
body: CheckoutHandoffConsumeRequest,
|
||||
): Promise<CheckoutHandoffConsumeResponse> {
|
||||
const env = await httpClient<ApiEnvelope<unknown>>(
|
||||
ApiPath.checkoutHandoffConsume,
|
||||
{ method: "POST", body },
|
||||
);
|
||||
return CheckoutHandoffConsumeResponseSchema.parse(
|
||||
unwrap(env) as Record<string, unknown>,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 绑定 Facebook ASID / PSID 到当前用户
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user