This commit is contained in:
2026-04-12 01:06:31 +07:00
commit 10d660cbcb
1066 changed files with 228596 additions and 0 deletions

View File

@@ -0,0 +1,20 @@
# SePay Configuration
SEPAY_MERCHANT_ID=SP-TEST-XXXXXXX
SEPAY_SECRET_KEY=spsk_test_xxxxxxxxxxxxx
SEPAY_ENV=sandbox # or 'production'
# SePay Webhook Configuration
SEPAY_WEBHOOK_AUTH_TYPE=api_key # or 'oauth2' or 'none'
SEPAY_WEBHOOK_API_KEY=your_webhook_api_key
# Polar Configuration
POLAR_ACCESS_TOKEN=polar_xxxxxxxxxxxxxxxx
POLAR_SERVER=sandbox # or 'production'
POLAR_ORG_ID=org_xxxxxxxxxxxxx
# Polar Webhook Configuration
POLAR_WEBHOOK_SECRET=base64_encoded_secret
# Optional: Database or other configuration
# DATABASE_URL=postgresql://user:password@localhost:5432/dbname
# REDIS_URL=redis://localhost:6379

View File

@@ -0,0 +1,244 @@
#!/usr/bin/env node
/**
* Checkout Helper Script
*
* Generate checkout sessions for both SePay and Polar platforms.
*
* Usage:
* node checkout-helper.js <platform> <config-json>
*
* Platforms: sepay, polar
*
* Environment Variables:
* SEPAY_MERCHANT_ID, SEPAY_SECRET_KEY, SEPAY_ENV
* POLAR_ACCESS_TOKEN, POLAR_SERVER
*/
const crypto = require('crypto');
class CheckoutHelper {
/**
* Generate SePay checkout form fields
*/
static generateSePayCheckout(config) {
const {
merchantId,
secretKey,
orderInvoiceNumber,
orderAmount,
currency = 'VND',
successUrl,
errorUrl,
cancelUrl,
orderDescription,
operation = 'PURCHASE'
} = config;
// Validate required fields
const required = ['merchantId', 'secretKey', 'orderInvoiceNumber', 'orderAmount', 'successUrl', 'errorUrl', 'cancelUrl'];
for (const field of required) {
if (!config[field]) {
throw new Error(`Missing required field: ${field}`);
}
}
// Build fields
const fields = {
merchant_id: merchantId,
operation: operation,
order_invoice_number: orderInvoiceNumber,
order_amount: orderAmount,
currency: currency,
success_url: successUrl,
error_url: errorUrl,
cancel_url: cancelUrl,
order_description: orderDescription || `Order ${orderInvoiceNumber}`,
timestamp: new Date().toISOString()
};
// Generate HMAC SHA256 signature
const signatureData = Object.keys(fields)
.sort()
.map(key => `${key}=${fields[key]}`)
.join('&');
const signature = crypto
.createHmac('sha256', secretKey)
.update(signatureData)
.digest('hex');
fields.signature = signature;
return {
fields,
formUrl: config.env === 'production'
? 'https://pay.sepay.vn/v1/init'
: 'https://sandbox.pay.sepay.vn/v1/init',
htmlForm: this.generateHTMLForm(fields, config.env === 'production'
? 'https://pay.sepay.vn/v1/init'
: 'https://sandbox.pay.sepay.vn/v1/init')
};
}
/**
* Generate Polar checkout configuration
*/
static generatePolarCheckout(config) {
const {
productPriceId,
successUrl,
externalCustomerId,
customerEmail,
customerName,
discountId,
metadata,
embedOrigin
} = config;
// Validate required fields
if (!productPriceId) {
throw new Error('Missing required field: productPriceId');
}
if (!successUrl) {
throw new Error('Missing required field: successUrl');
}
// Must be absolute URL
if (!successUrl.startsWith('http://') && !successUrl.startsWith('https://')) {
throw new Error('successUrl must be an absolute URL');
}
const checkoutConfig = {
product_price_id: productPriceId,
success_url: successUrl
};
// Add optional fields
if (externalCustomerId) checkoutConfig.external_customer_id = externalCustomerId;
if (customerEmail) checkoutConfig.customer_email = customerEmail;
if (customerName) checkoutConfig.customer_name = customerName;
if (discountId) checkoutConfig.discount_id = discountId;
if (metadata) checkoutConfig.metadata = metadata;
if (embedOrigin) checkoutConfig.embed_origin = embedOrigin;
return {
config: checkoutConfig,
apiEndpoint: config.server === 'sandbox'
? 'https://sandbox-api.polar.sh/v1/checkouts'
: 'https://api.polar.sh/v1/checkouts',
curlCommand: this.generatePolarCurl(checkoutConfig, config.accessToken, config.server)
};
}
/**
* Generate HTML form for SePay
*/
static generateHTMLForm(fields, actionUrl) {
const inputs = Object.keys(fields)
.map(key => ` <input type="hidden" name="${key}" value="${fields[key]}" />`)
.join('\n');
return `
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>SePay Payment</title>
</head>
<body>
<form id="payment-form" action="${actionUrl}" method="POST">
${inputs}
<button type="submit">Pay Now</button>
</form>
<script>
// Auto-submit form
// document.getElementById('payment-form').submit();
</script>
</body>
</html>
`.trim();
}
/**
* Generate cURL command for Polar
*/
static generatePolarCurl(config, accessToken, server = 'production') {
const endpoint = server === 'sandbox'
? 'https://sandbox-api.polar.sh/v1/checkouts'
: 'https://api.polar.sh/v1/checkouts';
return `curl -X POST ${endpoint} \\
-H "Authorization: Bearer ${accessToken}" \\
-H "Content-Type: application/json" \\
-d '${JSON.stringify(config, null, 2)}'`;
}
}
// CLI Usage
if (require.main === module) {
const args = process.argv.slice(2);
if (args.length < 2) {
console.log('Usage: node checkout-helper.js <platform> <config-json>');
console.log('\nPlatforms:');
console.log(' sepay - SePay checkout form generation');
console.log(' polar - Polar checkout session configuration');
console.log('\nExamples:');
console.log('\nSePay:');
console.log(' node checkout-helper.js sepay \'{"orderInvoiceNumber":"ORD001","orderAmount":100000,"successUrl":"https://example.com/success","errorUrl":"https://example.com/error","cancelUrl":"https://example.com/cancel"}\'');
console.log('\nPolar:');
console.log(' node checkout-helper.js polar \'{"productPriceId":"price_xxx","successUrl":"https://example.com/success","externalCustomerId":"user_123"}\'');
process.exit(1);
}
try {
const platform = args[0].toLowerCase();
const config = JSON.parse(args[1]);
if (platform === 'sepay') {
// Get from environment or config
config.merchantId = config.merchantId || process.env.SEPAY_MERCHANT_ID;
config.secretKey = config.secretKey || process.env.SEPAY_SECRET_KEY;
config.env = config.env || process.env.SEPAY_ENV || 'sandbox';
const result = CheckoutHelper.generateSePayCheckout(config);
console.log('✓ SePay Checkout Generated\n');
console.log('Form URL:', result.formUrl);
console.log('\nForm Fields:');
console.log(JSON.stringify(result.fields, null, 2));
console.log('\nHTML Form:');
console.log(result.htmlForm);
} else if (platform === 'polar') {
// Get from environment or config
config.accessToken = config.accessToken || process.env.POLAR_ACCESS_TOKEN;
config.server = config.server || process.env.POLAR_SERVER || 'production';
if (!config.accessToken) {
console.error('✗ Error: POLAR_ACCESS_TOKEN is required');
console.error('Set it via environment variable or in config JSON');
process.exit(1);
}
const result = CheckoutHelper.generatePolarCheckout(config);
console.log('✓ Polar Checkout Configuration Generated\n');
console.log('API Endpoint:', result.apiEndpoint);
console.log('\nCheckout Configuration:');
console.log(JSON.stringify(result.config, null, 2));
console.log('\ncURL Command:');
console.log(result.curlCommand);
} else {
console.error(`✗ Error: Unknown platform '${platform}'`);
console.error('Supported platforms: sepay, polar');
process.exit(1);
}
} catch (error) {
console.error('✗ Error:', error.message);
process.exit(1);
}
}
module.exports = CheckoutHelper;

View File

@@ -0,0 +1,17 @@
{
"name": "payment-integration-scripts",
"version": "1.0.0",
"description": "Helper scripts for SePay and Polar payment integration",
"scripts": {
"test": "node test-scripts.js"
},
"keywords": [
"payment",
"sepay",
"polar",
"webhook",
"checkout"
],
"author": "",
"license": "MIT"
}

View File

@@ -0,0 +1,202 @@
#!/usr/bin/env node
/**
* Polar Webhook Verification Script
*
* Verifies Polar webhook signatures following Standard Webhooks specification.
*
* Usage:
* node polar-webhook-verify.js <webhook-payload-json> <webhook-secret>
*
* Environment Variables:
* POLAR_WEBHOOK_SECRET - Webhook secret (base64 encoded)
*/
const crypto = require('crypto');
class PolarWebhookVerifier {
constructor(secret) {
if (!secret) {
throw new Error('Webhook secret is required');
}
// Decode base64 secret
this.secret = Buffer.from(secret, 'base64');
}
/**
* Verify webhook signature
*/
verifySignature(payload, headers) {
const webhookId = headers['webhook-id'];
const webhookTimestamp = headers['webhook-timestamp'];
const webhookSignature = headers['webhook-signature'];
if (!webhookId || !webhookTimestamp || !webhookSignature) {
throw new Error('Missing required webhook headers');
}
// Check timestamp (reject if > 5 minutes old)
const timestamp = parseInt(webhookTimestamp);
const now = Math.floor(Date.now() / 1000);
if (Math.abs(now - timestamp) > 300) {
throw new Error('Webhook timestamp too old or in future');
}
// Parse signatures
const signatures = webhookSignature.split(',').map(sig => {
const parts = sig.split('=');
const version = parts[0];
const signature = parts.slice(1).join('='); // Rejoin in case signature contains '='
return { version, signature };
});
// Create signed payload
const signedPayload = `${webhookTimestamp}.${payload}`;
// Compute expected signature
const expectedSignature = crypto
.createHmac('sha256', this.secret)
.update(signedPayload)
.digest('base64');
// Check if any signature matches
const isValid = signatures.some(sig => {
return sig.version === 'v1' && sig.signature === expectedSignature;
});
if (!isValid) {
throw new Error('Invalid webhook signature');
}
return true;
}
/**
* Process webhook event
*/
process(payload, headers) {
try {
// Verify signature
this.verifySignature(payload, headers);
// Parse payload
const event = typeof payload === 'string' ? JSON.parse(payload) : payload;
// Validate event structure
if (!event.type || !event.data) {
throw new Error('Invalid event structure');
}
return {
success: true,
event: {
type: event.type,
data: event.data
}
};
} catch (error) {
return {
success: false,
error: error.message
};
}
}
/**
* Get event category
*/
static getEventCategory(eventType) {
const categories = {
'checkout.': 'checkout',
'order.': 'order',
'subscription.': 'subscription',
'customer.': 'customer',
'benefit_grant.': 'benefit',
'refund.': 'refund',
'product.': 'product'
};
for (const [prefix, category] of Object.entries(categories)) {
if (eventType.startsWith(prefix)) {
return category;
}
}
return 'unknown';
}
/**
* Check if event is a payment
*/
static isPaymentEvent(eventType) {
return ['order.paid', 'order.created'].includes(eventType);
}
/**
* Check if event is a subscription change
*/
static isSubscriptionEvent(eventType) {
return eventType.startsWith('subscription.');
}
}
// CLI Usage
if (require.main === module) {
const args = process.argv.slice(2);
if (args.length < 1) {
console.log('Usage: node polar-webhook-verify.js <webhook-payload-json> [webhook-secret]');
console.log('\nWebhook secret can also be provided via POLAR_WEBHOOK_SECRET environment variable');
console.log('\nExample:');
console.log(' node polar-webhook-verify.js \'{"type":"order.paid","data":{...}}\' base64secret');
process.exit(1);
}
try {
const payload = args[0];
const secret = args[1] || process.env.POLAR_WEBHOOK_SECRET;
if (!secret) {
console.error('✗ Error: Webhook secret is required');
console.error('Provide it as second argument or set POLAR_WEBHOOK_SECRET environment variable');
process.exit(1);
}
// Mock headers for CLI testing
const timestamp = Math.floor(Date.now() / 1000);
const signedPayload = `${timestamp}.${payload}`;
const signature = crypto
.createHmac('sha256', Buffer.from(secret, 'base64'))
.update(signedPayload)
.digest('base64');
const headers = {
'webhook-id': 'msg_test_' + Date.now(),
'webhook-timestamp': timestamp.toString(),
'webhook-signature': `v1=${signature}`
};
const verifier = new PolarWebhookVerifier(secret);
const result = verifier.process(payload, headers);
if (result.success) {
console.log('✓ Webhook verified successfully\n');
console.log('Event Details:');
console.log(` Type: ${result.event.type}`);
console.log(` Category: ${PolarWebhookVerifier.getEventCategory(result.event.type)}`);
console.log(` Is Payment: ${PolarWebhookVerifier.isPaymentEvent(result.event.type) ? 'Yes' : 'No'}`);
console.log(` Is Subscription: ${PolarWebhookVerifier.isSubscriptionEvent(result.event.type) ? 'Yes' : 'No'}`);
console.log('\nEvent Data:');
console.log(JSON.stringify(result.event.data, null, 2));
} else {
console.error('✗ Verification failed:', result.error);
process.exit(1);
}
} catch (error) {
console.error('✗ Error:', error.message);
process.exit(1);
}
}
module.exports = PolarWebhookVerifier;

View File

@@ -0,0 +1,193 @@
#!/usr/bin/env node
/**
* SePay Webhook Verification Script
*
* Verifies SePay webhook authenticity and processes transaction data.
* Supports API Key and OAuth2 authentication.
*
* Usage:
* node sepay-webhook-verify.js <webhook-payload-json>
*
* Environment Variables:
* SEPAY_WEBHOOK_AUTH_TYPE - Authentication type (api_key or oauth2 or none)
* SEPAY_WEBHOOK_API_KEY - API key for verification (if using api_key)
*/
const crypto = require('crypto');
class SePayWebhookVerifier {
constructor(authType = 'none', apiKey = null) {
this.authType = authType;
this.apiKey = apiKey;
}
/**
* Verify webhook authenticity
*/
verifyAuthentication(headers) {
if (this.authType === 'none') {
console.log('⚠️ Warning: No authentication configured');
return true;
}
if (this.authType === 'api_key') {
const authHeader = headers['authorization'] || headers['Authorization'];
if (!authHeader) {
throw new Error('Missing Authorization header');
}
const expectedAuth = `Apikey ${this.apiKey}`;
if (authHeader !== expectedAuth) {
throw new Error('Invalid API key');
}
return true;
}
if (this.authType === 'oauth2') {
const authHeader = headers['authorization'] || headers['Authorization'];
if (!authHeader || !authHeader.startsWith('Bearer ')) {
throw new Error('Missing or invalid OAuth2 Bearer token');
}
// In production, verify token with OAuth2 provider
console.log('✓ OAuth2 token present (full verification needed in production)');
return true;
}
throw new Error(`Unknown auth type: ${this.authType}`);
}
/**
* Check for duplicate transactions
*/
isDuplicate(transactionId, processedIds = new Set()) {
return processedIds.has(transactionId);
}
/**
* Validate webhook payload structure
*/
validatePayload(payload) {
const required = [
'id',
'gateway',
'transactionDate',
'accountNumber',
'transferType',
'transferAmount',
'referenceCode'
];
for (const field of required) {
if (!(field in payload)) {
throw new Error(`Missing required field: ${field}`);
}
}
// Validate transfer type
if (!['in', 'out'].includes(payload.transferType)) {
throw new Error(`Invalid transferType: ${payload.transferType}`);
}
// Validate amount
if (typeof payload.transferAmount !== 'number' || payload.transferAmount <= 0) {
throw new Error('Invalid transferAmount');
}
return true;
}
/**
* Process webhook payload
*/
process(payload, headers = {}) {
try {
// 1. Verify authentication
this.verifyAuthentication(headers);
// 2. Validate payload structure
this.validatePayload(payload);
// 3. Extract transaction data
const transaction = {
id: payload.id,
gateway: payload.gateway,
transactionDate: new Date(payload.transactionDate),
accountNumber: payload.accountNumber,
code: payload.code || null,
content: payload.content || '',
transferType: payload.transferType,
transferAmount: payload.transferAmount,
accumulated: payload.accumulated || 0,
subAccount: payload.subAccount || null,
referenceCode: payload.referenceCode
};
return {
success: true,
transaction,
isIncoming: transaction.transferType === 'in',
isOutgoing: transaction.transferType === 'out'
};
} catch (error) {
return {
success: false,
error: error.message
};
}
}
}
// CLI Usage
if (require.main === module) {
const args = process.argv.slice(2);
if (args.length === 0) {
console.log('Usage: node sepay-webhook-verify.js <webhook-payload-json>');
console.log('\nEnvironment Variables:');
console.log(' SEPAY_WEBHOOK_AUTH_TYPE - Authentication type (api_key, oauth2, none)');
console.log(' SEPAY_WEBHOOK_API_KEY - API key for verification');
process.exit(1);
}
try {
const payload = JSON.parse(args[0]);
const authType = process.env.SEPAY_WEBHOOK_AUTH_TYPE || 'none';
const apiKey = process.env.SEPAY_WEBHOOK_API_KEY || null;
const verifier = new SePayWebhookVerifier(authType, apiKey);
// Mock headers for CLI testing
const headers = {};
if (authType === 'api_key' && apiKey) {
headers['Authorization'] = `Apikey ${apiKey}`;
}
const result = verifier.process(payload, headers);
if (result.success) {
console.log('✓ Webhook verified successfully\n');
console.log('Transaction Details:');
console.log(` ID: ${result.transaction.id}`);
console.log(` Gateway: ${result.transaction.gateway}`);
console.log(` Type: ${result.transaction.transferType}`);
console.log(` Amount: ${result.transaction.transferAmount.toLocaleString('vi-VN')} VND`);
console.log(` Reference: ${result.transaction.referenceCode}`);
console.log(` Content: ${result.transaction.content || 'N/A'}`);
console.log(`\n Incoming: ${result.isIncoming ? 'Yes' : 'No'}`);
console.log(` Outgoing: ${result.isOutgoing ? 'Yes' : 'No'}`);
} else {
console.error('✗ Verification failed:', result.error);
process.exit(1);
}
} catch (error) {
console.error('✗ Error:', error.message);
process.exit(1);
}
}
module.exports = SePayWebhookVerifier;

View File

@@ -0,0 +1,237 @@
#!/usr/bin/env node
/**
* Test suite for payment integration scripts
*/
const SePayWebhookVerifier = require('./sepay-webhook-verify');
const PolarWebhookVerifier = require('./polar-webhook-verify');
const CheckoutHelper = require('./checkout-helper');
class TestRunner {
constructor() {
this.passed = 0;
this.failed = 0;
}
test(name, fn) {
try {
fn();
console.log(`${name}`);
this.passed++;
} catch (error) {
console.error(`${name}`);
console.error(` Error: ${error.message}`);
this.failed++;
}
}
assert(condition, message) {
if (!condition) {
throw new Error(message || 'Assertion failed');
}
}
assertEqual(actual, expected, message) {
if (actual !== expected) {
throw new Error(message || `Expected ${expected}, got ${actual}`);
}
}
summary() {
console.log(`\nTest Summary: ${this.passed} passed, ${this.failed} failed`);
return this.failed === 0;
}
}
// Run tests
console.log('Running Payment Integration Script Tests\n');
const runner = new TestRunner();
// SePay Webhook Verifier Tests
console.log('SePay Webhook Verifier Tests:');
runner.test('should verify valid SePay webhook', () => {
const verifier = new SePayWebhookVerifier('none');
const payload = {
id: 12345,
gateway: 'Vietcombank',
transactionDate: '2025-01-13 10:00:00',
accountNumber: '0123456789',
transferType: 'in',
transferAmount: 100000,
referenceCode: 'REF123',
content: 'Order payment'
};
const result = verifier.process(payload);
runner.assert(result.success === true, 'Should verify successfully');
runner.assert(result.transaction.id === 12345, 'Should parse transaction ID');
runner.assert(result.isIncoming === true, 'Should detect incoming transfer');
});
runner.test('should reject invalid SePay transfer type', () => {
const verifier = new SePayWebhookVerifier('none');
const payload = {
id: 12345,
gateway: 'Vietcombank',
transactionDate: '2025-01-13 10:00:00',
accountNumber: '0123456789',
transferType: 'invalid',
transferAmount: 100000,
referenceCode: 'REF123'
};
const result = verifier.process(payload);
runner.assert(result.success === false, 'Should fail validation');
runner.assert(result.error.includes('Invalid transferType'), 'Should report invalid transfer type');
});
runner.test('should verify SePay webhook with API key', () => {
const verifier = new SePayWebhookVerifier('api_key', 'test_key_123');
const payload = {
id: 12345,
gateway: 'Vietcombank',
transactionDate: '2025-01-13 10:00:00',
accountNumber: '0123456789',
transferType: 'in',
transferAmount: 100000,
referenceCode: 'REF123'
};
const headers = { Authorization: 'Apikey test_key_123' };
const result = verifier.process(payload, headers);
runner.assert(result.success === true, 'Should verify with valid API key');
});
runner.test('should reject SePay webhook with invalid API key', () => {
const verifier = new SePayWebhookVerifier('api_key', 'test_key_123');
const payload = {
id: 12345,
gateway: 'Vietcombank',
transactionDate: '2025-01-13 10:00:00',
accountNumber: '0123456789',
transferType: 'in',
transferAmount: 100000,
referenceCode: 'REF123'
};
const headers = { Authorization: 'Apikey wrong_key' };
const result = verifier.process(payload, headers);
runner.assert(result.success === false, 'Should reject invalid API key');
});
// Polar Webhook Verifier Tests
console.log('\nPolar Webhook Verifier Tests:');
runner.test('should verify valid Polar webhook', () => {
const crypto = require('crypto');
const secret = Buffer.from('test_secret_key').toString('base64');
const verifier = new PolarWebhookVerifier(secret);
const payload = JSON.stringify({
type: 'order.paid',
data: { id: 'order_123', amount: 2000 }
});
const timestamp = Math.floor(Date.now() / 1000);
const signedPayload = `${timestamp}.${payload}`;
const signature = crypto
.createHmac('sha256', Buffer.from(secret, 'base64'))
.update(signedPayload)
.digest('base64');
const headers = {
'webhook-id': 'msg_123',
'webhook-timestamp': timestamp.toString(),
'webhook-signature': `v1=${signature}`
};
const result = verifier.process(payload, headers);
if (!result.success) {
throw new Error(`Verification failed: ${result.error}`);
}
runner.assert(result.success === true, 'Should verify successfully');
runner.assertEqual(result.event.type, 'order.paid', 'Should parse event type');
});
runner.test('should reject Polar webhook with invalid signature', () => {
const secret = Buffer.from('test_secret_key').toString('base64');
const verifier = new PolarWebhookVerifier(secret);
const payload = JSON.stringify({
type: 'order.paid',
data: { id: 'order_123' }
});
const headers = {
'webhook-id': 'msg_123',
'webhook-timestamp': Math.floor(Date.now() / 1000).toString(),
'webhook-signature': 'v1=invalid_signature'
};
const result = verifier.process(payload, headers);
runner.assert(result.success === false, 'Should reject invalid signature');
});
runner.test('should categorize Polar event types', () => {
runner.assertEqual(PolarWebhookVerifier.getEventCategory('order.paid'), 'order');
runner.assertEqual(PolarWebhookVerifier.getEventCategory('subscription.active'), 'subscription');
runner.assertEqual(PolarWebhookVerifier.getEventCategory('customer.created'), 'customer');
runner.assert(PolarWebhookVerifier.isPaymentEvent('order.paid') === true);
runner.assert(PolarWebhookVerifier.isSubscriptionEvent('subscription.active') === true);
});
// Checkout Helper Tests
console.log('\nCheckout Helper Tests:');
runner.test('should generate SePay checkout fields', () => {
const config = {
merchantId: 'SP-TEST-123',
secretKey: 'test_secret',
orderInvoiceNumber: 'ORD001',
orderAmount: 100000,
successUrl: 'https://example.com/success',
errorUrl: 'https://example.com/error',
cancelUrl: 'https://example.com/cancel',
env: 'sandbox'
};
const result = CheckoutHelper.generateSePayCheckout(config);
runner.assert(result.fields !== undefined, 'Should generate fields');
runner.assert(result.fields.signature !== undefined, 'Should generate signature');
runner.assertEqual(result.fields.merchant_id, 'SP-TEST-123', 'Should include merchant ID');
runner.assert(result.formUrl.includes('sandbox'), 'Should use sandbox URL');
});
runner.test('should generate Polar checkout config', () => {
const config = {
productPriceId: 'price_123',
successUrl: 'https://example.com/success',
externalCustomerId: 'user_123',
accessToken: 'test_token',
server: 'sandbox'
};
const result = CheckoutHelper.generatePolarCheckout(config);
runner.assert(result.config !== undefined, 'Should generate config');
runner.assertEqual(result.config.product_price_id, 'price_123', 'Should include price ID');
runner.assertEqual(result.config.external_customer_id, 'user_123', 'Should include customer ID');
runner.assert(result.apiEndpoint.includes('sandbox'), 'Should use sandbox endpoint');
});
runner.test('should reject Polar config with relative URL', () => {
try {
CheckoutHelper.generatePolarCheckout({
productPriceId: 'price_123',
successUrl: '/success' // Relative URL
});
runner.assert(false, 'Should throw error for relative URL');
} catch (error) {
runner.assert(error.message.includes('absolute URL'), 'Should require absolute URL');
}
});
// Run summary
const success = runner.summary();
process.exit(success ? 0 : 1);