239 lines
7.5 KiB
JavaScript
239 lines
7.5 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* Test script to verify payment failure handling with partial discount coupons
|
|
*
|
|
* This script tests the critical billing bug fix where subscriptions with partial
|
|
* discount coupons were incorrectly marked as 'active' even when payment failed.
|
|
*
|
|
* Usage:
|
|
* node tests/test_payment_failure_handling.js
|
|
*
|
|
* Requirements:
|
|
* - Stripe test mode enabled
|
|
* - Test customer created
|
|
* - Discount coupon created (e.g., 50% off)
|
|
*/
|
|
|
|
const path = require('path');
|
|
require('dotenv').config({ path: path.resolve(__dirname, '../environment.env') });
|
|
|
|
const stripe = require('stripe')(process.env.STRIPE_SEC_KEY);
|
|
const assert = require('assert');
|
|
|
|
// Test cards
|
|
const TEST_CARDS = {
|
|
SUCCESS: '4242424242424242',
|
|
DECLINE: '4000000000000341',
|
|
REQUIRES_AUTH: '4000002500003155'
|
|
};
|
|
|
|
async function cleanup(customerId, subscriptionIds = []) {
|
|
console.log('\nCleaning up test data...');
|
|
|
|
// Cancel subscriptions
|
|
for (const subId of subscriptionIds) {
|
|
try {
|
|
await stripe.subscriptions.del(subId);
|
|
console.log(` ✓ Deleted subscription ${subId}`);
|
|
} catch (err) {
|
|
console.log(` ⚠ Could not delete subscription ${subId}: ${err.message}`);
|
|
}
|
|
}
|
|
|
|
// Delete customer
|
|
if (customerId) {
|
|
try {
|
|
await stripe.customers.del(customerId);
|
|
console.log(` ✓ Deleted customer ${customerId}`);
|
|
} catch (err) {
|
|
console.log(` ⚠ Could not delete customer ${customerId}: ${err.message}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
async function createTestPaymentMethod(cardNumber) {
|
|
const paymentMethod = await stripe.paymentMethods.create({
|
|
type: 'card',
|
|
card: {
|
|
number: cardNumber,
|
|
exp_month: 12,
|
|
exp_year: 2030,
|
|
cvc: '123'
|
|
}
|
|
});
|
|
return paymentMethod.id;
|
|
}
|
|
|
|
async function testScenario(name, cardNumber, couponId, expectSuccess) {
|
|
console.log(`\n${'='.repeat(60)}`);
|
|
console.log(`TEST: ${name}`);
|
|
console.log('='.repeat(60));
|
|
|
|
let customer, subscription;
|
|
const subscriptionIds = [];
|
|
|
|
try {
|
|
// Create test customer
|
|
customer = await stripe.customers.create({
|
|
email: `test-${Date.now()}@example.com`,
|
|
metadata: { test: 'payment_failure_handling' }
|
|
});
|
|
console.log(`✓ Created test customer: ${customer.id}`);
|
|
|
|
// Create payment method
|
|
const pmId = await createTestPaymentMethod(cardNumber);
|
|
console.log(`✓ Created payment method: ${pmId}`);
|
|
|
|
// Attach payment method to customer
|
|
await stripe.paymentMethods.attach(pmId, { customer: customer.id });
|
|
console.log(`✓ Attached payment method to customer`);
|
|
|
|
// Set as default
|
|
await stripe.customers.update(customer.id, {
|
|
invoice_settings: { default_payment_method: pmId }
|
|
});
|
|
|
|
// Create subscription with coupon
|
|
const subscriptionParams = {
|
|
customer: customer.id,
|
|
items: [{ price: process.env.ESS_1 }], // Use your test price ID
|
|
expand: ['latest_invoice.payment_intent'],
|
|
payment_behavior: 'error_if_incomplete', // CRITICAL FIX
|
|
metadata: { test: 'payment_failure_test' }
|
|
};
|
|
|
|
if (couponId) {
|
|
subscriptionParams.coupon = couponId;
|
|
console.log(`✓ Applying coupon: ${couponId}`);
|
|
}
|
|
|
|
try {
|
|
subscription = await stripe.subscriptions.create(subscriptionParams);
|
|
subscriptionIds.push(subscription.id);
|
|
|
|
console.log(`\nSubscription created: ${subscription.id}`);
|
|
console.log(` Status: ${subscription.status}`);
|
|
console.log(` Latest Invoice Status: ${subscription.latest_invoice?.status || 'N/A'}`);
|
|
|
|
if (subscription.latest_invoice?.payment_intent) {
|
|
console.log(` Payment Intent Status: ${subscription.latest_invoice.payment_intent.status}`);
|
|
}
|
|
|
|
// Verify expectations
|
|
if (expectSuccess) {
|
|
assert.strictEqual(subscription.status, 'active', 'Subscription should be active');
|
|
console.log('\n✅ PASS: Subscription activated successfully');
|
|
} else {
|
|
assert.strictEqual(subscription.status, 'incomplete', 'Subscription should be incomplete');
|
|
assert.strictEqual(subscription.latest_invoice.status, 'open', 'Invoice should be open');
|
|
console.log('\n✅ PASS: Subscription correctly marked as incomplete');
|
|
}
|
|
|
|
} catch (err) {
|
|
if (!expectSuccess && err.code === 'resource_already_exists') {
|
|
console.log('\n✅ PASS: Payment failed as expected');
|
|
} else {
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
} catch (error) {
|
|
console.error(`\n❌ FAIL: ${error.message}`);
|
|
console.error(error);
|
|
return false;
|
|
} finally {
|
|
await cleanup(customer?.id, subscriptionIds);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
async function main() {
|
|
console.log('Payment Failure Handling Test Suite');
|
|
console.log('=====================================\n');
|
|
console.log('Testing critical billing bug fix:');
|
|
console.log('- Partial discount coupons with failed payments');
|
|
console.log('- Should result in INCOMPLETE status, not ACTIVE\n');
|
|
|
|
// Check if required env vars are present
|
|
if (!process.env.STRIPE_SEC_KEY || !process.env.ESS_1) {
|
|
console.error('❌ Missing required environment variables:');
|
|
console.error(' - STRIPE_SEC_KEY (Stripe secret key)');
|
|
console.error(' - ESS_1 (Price ID for testing)');
|
|
process.exit(1);
|
|
}
|
|
|
|
// Check if we're in test mode
|
|
if (!process.env.STRIPE_SEC_KEY.startsWith('sk_test_')) {
|
|
console.error('❌ ERROR: Not in Stripe test mode!');
|
|
console.error(' This script should only run with test keys.');
|
|
process.exit(1);
|
|
}
|
|
|
|
let allPassed = true;
|
|
|
|
// Test 1: Successful payment with coupon
|
|
console.log('\nTest 1: Successful payment with partial discount coupon');
|
|
console.log('Expected: Status = ACTIVE (payment succeeds)');
|
|
const test1 = await testScenario(
|
|
'Success with 50% coupon',
|
|
TEST_CARDS.SUCCESS,
|
|
null, // Set your test coupon ID here
|
|
true
|
|
);
|
|
allPassed = allPassed && test1;
|
|
|
|
// Test 2: Failed payment with coupon (THE CRITICAL BUG)
|
|
console.log('\nTest 2: Failed payment with partial discount coupon');
|
|
console.log('Expected: Status = INCOMPLETE (payment fails, subscription awaits payment)');
|
|
const test2 = await testScenario(
|
|
'Failed payment with 50% coupon',
|
|
TEST_CARDS.DECLINE,
|
|
null, // Set your test coupon ID here
|
|
false
|
|
);
|
|
allPassed = allPassed && test2;
|
|
|
|
// Test 3: Failed payment without coupon
|
|
console.log('\nTest 3: Failed payment without coupon');
|
|
console.log('Expected: Status = INCOMPLETE');
|
|
const test3 = await testScenario(
|
|
'Failed payment no coupon',
|
|
TEST_CARDS.DECLINE,
|
|
null,
|
|
false
|
|
);
|
|
allPassed = allPassed && test3;
|
|
|
|
// Summary
|
|
console.log('\n' + '='.repeat(60));
|
|
console.log('TEST SUMMARY');
|
|
console.log('='.repeat(60));
|
|
|
|
if (allPassed) {
|
|
console.log('\n✅ All tests PASSED!');
|
|
console.log('\nThe fix is working correctly:');
|
|
console.log(' - Failed payments result in INCOMPLETE status');
|
|
console.log(' - Partial discount coupons do not bypass payment checks');
|
|
console.log(' - Billing security is enforced\n');
|
|
process.exit(0);
|
|
} else {
|
|
console.log('\n❌ Some tests FAILED!');
|
|
console.log('\nThe bug may still exist:');
|
|
console.log(' - Check that payment_behavior is set correctly');
|
|
console.log(' - Verify Stripe webhooks are configured');
|
|
console.log(' - Review error logs above\n');
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
// Run tests
|
|
if (require.main === module) {
|
|
main().catch(err => {
|
|
console.error('\n❌ Fatal error:', err);
|
|
process.exit(1);
|
|
});
|
|
}
|
|
|
|
module.exports = { testScenario, TEST_CARDS };
|