mirror of
https://github.com/stack-auth/stack.git
synced 2026-07-12 21:00:52 +08:00
Some checks failed
all-good: Did all the other checks pass? / all-good (push) Has been cancelled
Ensure Prisma migrations are in sync with the schema / check_prisma_migrations (22.x) (push) Has been cancelled
DB migration compat / Check if migrations changed (push) Has been cancelled
Docker Server Build and Push / Docker Build and Push Server (push) Has been cancelled
Docker Server Build and Run / docker (push) Has been cancelled
Runs E2E API Tests (Local Emulator) / E2E Tests (Local Emulator, Node ${{ matrix.node-version }}) (22.x) (push) Has been cancelled
Runs E2E API Tests / E2E Tests (Node ${{ matrix.node-version }}, Freestyle ${{ matrix.freestyle-mode }}) (mock, 22.x) (push) Has been cancelled
Runs E2E API Tests / E2E Tests (Node ${{ matrix.node-version }}, Freestyle ${{ matrix.freestyle-mode }}) (prod, 22.x) (push) Has been cancelled
Runs E2E API Tests with custom port prefix / build (22.x) (push) Has been cancelled
Runs E2E Fallback Tests / E2E Fallback Tests (Node ${{ matrix.node-version }}) (22.x) (push) Has been cancelled
Lint & build / lint_and_build (24) (push) Has been cancelled
TOC Generator / TOC Generator (push) Has been cancelled
DB migration compat / Back-compat — Current branch migrations with ${{ needs.check-migrations-changed.outputs.base_branch }} branch code (push) Has been cancelled
DB migration compat / Forward-compat — Current branch code with ${{ needs.check-migrations-changed.outputs.base_branch }} branch migrations (push) Has been cancelled
DB migration compat / No migration changes (skipped) (push) Has been cancelled
129 lines
4.5 KiB
TypeScript
129 lines
4.5 KiB
TypeScript
import { getEnvVariable } from "@hexclave/shared/dist/utils/env";
|
|
import { randomUUID } from "node:crypto";
|
|
import { expect } from "vitest";
|
|
import { Auth, Payments, Project, niceBackendFetch } from "../backend-helpers";
|
|
|
|
export function createDefaultPaymentsConfig(testMode: boolean | undefined) {
|
|
return {
|
|
payments: {
|
|
testMode: testMode ?? true,
|
|
products: {
|
|
"otp-product": {
|
|
displayName: "One-Time Product",
|
|
customerType: "user",
|
|
serverOnly: false,
|
|
stackable: false,
|
|
prices: {
|
|
single: { USD: "50.00" },
|
|
},
|
|
includedItems: {},
|
|
},
|
|
},
|
|
items: {},
|
|
},
|
|
};
|
|
}
|
|
|
|
export async function setupProjectWithPaymentsConfig(options: { testMode?: boolean } = {}) {
|
|
await Project.createAndSwitch();
|
|
await Payments.setup();
|
|
const config = createDefaultPaymentsConfig(options.testMode);
|
|
await Project.updateConfig(config);
|
|
return config;
|
|
}
|
|
|
|
export async function createPurchaseCode(options: { userId: string, productId: string }) {
|
|
const res = await niceBackendFetch("/api/latest/payments/purchases/create-purchase-url", {
|
|
method: "POST",
|
|
accessType: "client",
|
|
body: {
|
|
customer_type: "user",
|
|
customer_id: options.userId,
|
|
product_id: options.productId,
|
|
},
|
|
});
|
|
expect(res.status).toBe(200);
|
|
const codeMatch = (res.body.url as string).match(/\/purchase\/([a-z0-9-_]+)/);
|
|
const code = codeMatch ? codeMatch[1] : undefined;
|
|
expect(code).toBeDefined();
|
|
return code as string;
|
|
}
|
|
|
|
export async function createTestModeTransaction(productId: string, priceId: string) {
|
|
const { userId } = await Auth.fastSignUp();
|
|
const code = await createPurchaseCode({ userId, productId });
|
|
const response = await niceBackendFetch("/api/latest/internal/payments/test-mode-purchase-session", {
|
|
accessType: "admin",
|
|
method: "POST",
|
|
body: { full_code: code, price_id: priceId, quantity: 1 },
|
|
});
|
|
expect(response.status).toBe(200);
|
|
const transactions = await niceBackendFetch("/api/latest/internal/payments/transactions", {
|
|
accessType: "admin",
|
|
});
|
|
expect(transactions.status).toBe(200);
|
|
expect(transactions.body.transactions.length).toBeGreaterThan(0);
|
|
const transaction = transactions.body.transactions[0];
|
|
return { transactionId: transaction.id, userId };
|
|
}
|
|
|
|
export async function createLiveModeOneTimePurchaseTransaction(options: { quantity?: number } = {}) {
|
|
const config = await setupProjectWithPaymentsConfig({ testMode: false });
|
|
const { userId } = await Auth.fastSignUp();
|
|
const quantity = options.quantity ?? 1;
|
|
|
|
const accountInfo = await niceBackendFetch("/api/latest/internal/payments/stripe/account-info", {
|
|
accessType: "admin",
|
|
});
|
|
expect(accountInfo.status).toBe(200);
|
|
const accountId: string = accountInfo.body.account_id;
|
|
|
|
const code = await createPurchaseCode({ userId, productId: "otp-product" });
|
|
const stackTestTenancyId = code.split("_")[0];
|
|
const product = config.payments.products["otp-product"];
|
|
|
|
const idSuffix = randomUUID().replace(/-/g, "");
|
|
const eventId = `evt_otp_purchase_${idSuffix}`;
|
|
const paymentIntentId = `pi_otp_purchase_${idSuffix}`;
|
|
const paymentIntentPayload = {
|
|
id: eventId,
|
|
type: "payment_intent.succeeded",
|
|
account: accountId,
|
|
data: {
|
|
object: {
|
|
id: paymentIntentId,
|
|
customer: userId,
|
|
stack_stripe_mock_data: {
|
|
"accounts.retrieve": { metadata: { tenancyId: stackTestTenancyId } },
|
|
"customers.retrieve": { metadata: { customerId: userId, customerType: "USER" } },
|
|
"subscriptions.list": { data: [] },
|
|
},
|
|
metadata: {
|
|
productId: "otp-product",
|
|
product: JSON.stringify(product),
|
|
customerId: userId,
|
|
customerType: "user",
|
|
purchaseQuantity: String(quantity),
|
|
purchaseKind: "ONE_TIME",
|
|
priceId: "single",
|
|
},
|
|
},
|
|
},
|
|
};
|
|
|
|
const webhookSecret = getEnvVariable("STACK_STRIPE_WEBHOOK_SECRET", "mock_stripe_webhook_secret");
|
|
const webhookRes = await Payments.sendStripeWebhook(paymentIntentPayload, { secret: webhookSecret });
|
|
expect(webhookRes.status).toBe(200);
|
|
expect(webhookRes.body).toEqual({ received: true });
|
|
|
|
const transactionsRes = await niceBackendFetch("/api/latest/internal/payments/transactions", {
|
|
accessType: "admin",
|
|
});
|
|
expect(transactionsRes.status).toBe(200);
|
|
|
|
const purchaseTransaction = transactionsRes.body.transactions.find((tx: any) => tx.type === "purchase");
|
|
expect(purchaseTransaction).toBeDefined();
|
|
|
|
return { userId, transactionsRes, purchaseTransaction };
|
|
}
|