mirror of
https://github.com/screentinker/screentinker.git
synced 2026-08-14 14:23:14 -06:00
Add allow_promotion_codes: true to the checkout.sessions.create call in POST /checkout. This is what renders the "Add promotion code" field on Stripe's hosted checkout page; for API-created sessions there is no Dashboard equivalent (that toggle only exists for Payment Links, which we don't use), so a comment warns against removing it as "redundant". The billingPortal branch is untouched — portal sessions handle discounts separately. Testing: no Stripe-SDK test/mock existed (the billing-*.test.js files cover the #146 usage-metering path, not Stripe). Added stripe-checkout.test.js using the repo's in-process router-mount convention with a minimal `stripe` stub injected via require.cache, asserting the checkout payload carries allow_promotion_codes:true (and still builds a subscription session for the requested price). Full suite 557/557. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
84 lines
3.8 KiB
JavaScript
84 lines
3.8 KiB
JavaScript
'use strict';
|
|
|
|
// Stripe checkout session shape. There was no pre-existing Stripe-SDK test/mock in the repo
|
|
// (the billing-*.test.js files cover the #146 usage-metering path, not Stripe), so this uses
|
|
// the repo's in-process router-mount convention with a minimal `stripe` stub injected via
|
|
// require.cache — capturing the exact params passed to checkout.sessions.create.
|
|
//
|
|
// Guards that the hosted-checkout promo-code field stays enabled: allow_promotion_codes:true
|
|
// is the ONLY way to render it for API-created sessions (no dashboard equivalent), so a
|
|
// silent removal would quietly break promotion codes with no other signal.
|
|
|
|
const os = require('node:os');
|
|
const path = require('node:path');
|
|
const crypto = require('node:crypto');
|
|
process.env.DATA_DIR = path.join(os.tmpdir(), 'st-stripe-' + crypto.randomBytes(4).toString('hex'));
|
|
process.env.SELF_HOSTED = 'true';
|
|
process.env.NODE_ENV = 'test';
|
|
process.env.STRIPE_SECRET_KEY = 'sk_test_dummy'; // makes routes/stripe build the (stubbed) client
|
|
|
|
const { test, before, after } = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const http = require('node:http');
|
|
const express = require('express');
|
|
|
|
// --- stub the Stripe SDK: capture checkout.sessions.create params ---
|
|
let capturedCheckout = null;
|
|
const fakeStripeFactory = () => ({
|
|
customers: { create: async () => ({ id: 'cus_test' }) },
|
|
checkout: { sessions: { create: async (params) => { capturedCheckout = params; return { url: 'https://stripe.test/checkout' }; } } },
|
|
billingPortal: { sessions: { create: async () => ({ url: 'https://stripe.test/portal' }) } },
|
|
});
|
|
const stripePath = require.resolve('stripe');
|
|
require.cache[stripePath] = { id: stripePath, filename: stripePath, loaded: true, exports: fakeStripeFactory };
|
|
|
|
// --- stub requireAuth so the route runs with a fixed user (no JWT plumbing needed) ---
|
|
const authPath = require.resolve('../middleware/auth');
|
|
require.cache[authPath] = {
|
|
id: authPath, filename: authPath, loaded: true,
|
|
exports: {
|
|
requireAuth: (req, _res, next) => {
|
|
req.user = { id: 'u-test', email: 'u@test.local', name: 'Test',
|
|
stripe_customer_id: 'cus_test', stripe_subscription_id: null };
|
|
next();
|
|
},
|
|
},
|
|
};
|
|
|
|
const { db } = require('../db/database');
|
|
// A plan with a Stripe price so the handler reaches checkout.sessions.create.
|
|
db.prepare(`INSERT OR REPLACE INTO plans (id, name, display_name, stripe_price_monthly, stripe_price_yearly)
|
|
VALUES ('promo_test', 'promo_test', 'Promo', 'price_test_m', 'price_test_y')`).run();
|
|
|
|
const stripeRouter = require('../routes/stripe');
|
|
|
|
let server, base;
|
|
before(async () => {
|
|
const app = express();
|
|
app.use(express.json());
|
|
app.use('/api/stripe', stripeRouter);
|
|
server = http.createServer(app);
|
|
await new Promise((r) => server.listen(0, r));
|
|
base = `http://127.0.0.1:${server.address().port}`;
|
|
});
|
|
after(() => new Promise((r) => server.close(r)));
|
|
|
|
test('POST /checkout passes allow_promotion_codes:true to Stripe', async () => {
|
|
capturedCheckout = null;
|
|
const res = await fetch(`${base}/api/stripe/checkout`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ plan_id: 'promo_test', interval: 'monthly' }),
|
|
});
|
|
const body = await res.json();
|
|
assert.equal(res.status, 200);
|
|
assert.equal(body.type, 'checkout', 'took the checkout branch, not the portal branch');
|
|
|
|
assert.ok(capturedCheckout, 'checkout.sessions.create was called');
|
|
assert.equal(capturedCheckout.allow_promotion_codes, true,
|
|
'allow_promotion_codes:true must be present so the promo-code field renders on hosted checkout');
|
|
// sanity: it is still a subscription checkout for the requested price
|
|
assert.equal(capturedCheckout.mode, 'subscription');
|
|
assert.equal(capturedCheckout.line_items[0].price, 'price_test_m');
|
|
});
|