🐛 Fixed importing offers when importing members from Stripe (#17739)

fixes https://github.com/TryGhost/Product/issues/3728

- When importing members from Stripe with an existing offer, that didn't
exist in Ghost, the offer never got linked with the imported
subscription because of a missing return statement.
- Fixes importing offers with duplicate names
- Added E2E tests for creating members from a Stripe Customer ID
This commit is contained in:
Simon Backx 2023-08-16 15:25:57 +02:00 committed by GitHub
parent a6776301e3
commit b587429008
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 517 additions and 144 deletions

View File

@ -2723,6 +2723,228 @@ Object {
}
`;
exports[`Members API Can create an offer in Ghost when Stripe subscription has an unknown offer attached 1: [body] 1`] = `
Object {
"members": Array [
Object {
"attribution": Object {
"id": null,
"referrer_medium": "Ghost Admin",
"referrer_source": "Created manually",
"referrer_url": null,
"title": null,
"type": null,
"url": null,
},
"avatar_image": null,
"comped": false,
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"email": "create-member-offer-test@email.com",
"email_count": 0,
"email_open_rate": null,
"email_opened_count": 0,
"email_suppression": Object {
"info": null,
"suppressed": false,
},
"geolocation": null,
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"labels": Any<Array>,
"last_seen_at": null,
"name": "Test Member",
"newsletters": Array [
Object {
"background_color": "light",
"body_font_category": "sans_serif",
"border_color": null,
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"description": null,
"feedback_enabled": false,
"footer_content": null,
"header_image": null,
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"name": "Default Newsletter",
"sender_email": null,
"sender_name": null,
"sender_reply_to": "newsletter",
"show_badge": true,
"show_comment_cta": true,
"show_feature_image": true,
"show_header_icon": true,
"show_header_name": true,
"show_header_title": true,
"show_latest_posts": false,
"show_post_title_section": true,
"show_subscription_details": false,
"slug": "default-newsletter",
"sort_order": 0,
"status": "active",
"subscribe_on_signup": true,
"title_alignment": "center",
"title_color": null,
"title_font_category": "sans_serif",
"updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/,
"visibility": "members",
},
],
"note": null,
"status": "paid",
"subscribed": true,
"subscriptions": Any<Array>,
"tiers": Array [
Object {
"active": true,
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"currency": "usd",
"description": null,
"expiry_at": null,
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"monthly_price": 500,
"monthly_price_id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"name": "Default Product",
"slug": "default-product",
"trial_days": 0,
"type": "paid",
"updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"visibility": "public",
"welcome_page_url": "/welcome-paid",
"yearly_price": 5000,
"yearly_price_id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
},
],
"updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/,
},
],
}
`;
exports[`Members API Can create an offer in Ghost when Stripe subscription has an unknown offer attached 2: [headers] 1`] = `
Object {
"access-control-allow-origin": "http://127.0.0.1:2369",
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
"content-length": "3700",
"content-type": "application/json; charset=utf-8",
"content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/,
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
"location": StringMatching /https\\?:\\\\/\\\\/\\.\\*\\?\\\\/members\\\\/\\[a-f0-9\\]\\{24\\}\\\\//,
"vary": "Accept-Version, Origin, Accept-Encoding",
"x-powered-by": "Express",
}
`;
exports[`Members API Can create an offer in Ghost when Stripe subscription has an unknown offer attached with a duplicate name 1: [body] 1`] = `
Object {
"members": Array [
Object {
"attribution": Object {
"id": null,
"referrer_medium": "Ghost Admin",
"referrer_source": "Created manually",
"referrer_url": null,
"title": null,
"type": null,
"url": null,
},
"avatar_image": null,
"comped": false,
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"email": "create-member-offer-test2@email.com",
"email_count": 0,
"email_open_rate": null,
"email_opened_count": 0,
"email_suppression": Object {
"info": null,
"suppressed": false,
},
"geolocation": null,
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"labels": Any<Array>,
"last_seen_at": null,
"name": "Test Member",
"newsletters": Array [
Object {
"background_color": "light",
"body_font_category": "sans_serif",
"border_color": null,
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"description": null,
"feedback_enabled": false,
"footer_content": null,
"header_image": null,
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"name": "Default Newsletter",
"sender_email": null,
"sender_name": null,
"sender_reply_to": "newsletter",
"show_badge": true,
"show_comment_cta": true,
"show_feature_image": true,
"show_header_icon": true,
"show_header_name": true,
"show_header_title": true,
"show_latest_posts": false,
"show_post_title_section": true,
"show_subscription_details": false,
"slug": "default-newsletter",
"sort_order": 0,
"status": "active",
"subscribe_on_signup": true,
"title_alignment": "center",
"title_color": null,
"title_font_category": "sans_serif",
"updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/,
"visibility": "members",
},
],
"note": null,
"status": "paid",
"subscribed": true,
"subscriptions": Any<Array>,
"tiers": Array [
Object {
"active": true,
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"currency": "usd",
"description": null,
"expiry_at": null,
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"monthly_price": 500,
"monthly_price_id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"name": "Default Product",
"slug": "default-product",
"trial_days": 0,
"type": "paid",
"updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"visibility": "public",
"welcome_page_url": "/welcome-paid",
"yearly_price": 5000,
"yearly_price_id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
},
],
"updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/,
},
],
}
`;
exports[`Members API Can create an offer in Ghost when Stripe subscription has an unknown offer attached with a duplicate name 2: [headers] 1`] = `
Object {
"access-control-allow-origin": "http://127.0.0.1:2369",
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
"content-length": "3727",
"content-type": "application/json; charset=utf-8",
"content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/,
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
"location": StringMatching /https\\?:\\\\/\\\\/\\.\\*\\?\\\\/members\\\\/\\[a-f0-9\\]\\{24\\}\\\\//,
"vary": "Accept-Version, Origin, Accept-Encoding",
"x-powered-by": "Express",
}
`;
exports[`Members API Can delete a member while cancelling Stripe Subscription 1: [headers] 1`] = `
Object {
"access-control-allow-origin": "http://127.0.0.1:2369",
@ -3234,11 +3456,11 @@ Object {
"comped": 4,
"date": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}/,
"free": 4,
"paid": 2,
"paid": 4,
},
],
"resource": "members",
"total": 10,
"total": 12,
}
`;
@ -3834,6 +4056,58 @@ Object {
"updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/,
},
Object {
"avatar_image": null,
"comped": false,
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"email": "create-member-offer-test2@email.com",
"email_count": 0,
"email_open_rate": null,
"email_opened_count": 0,
"email_suppression": Object {
"info": null,
"suppressed": false,
},
"geolocation": null,
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"labels": Any<Array>,
"last_seen_at": null,
"name": "Test Member",
"newsletters": Any<Array>,
"note": null,
"status": "paid",
"subscribed": true,
"subscriptions": Any<Array>,
"tiers": Any<Array>,
"updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/,
},
Object {
"avatar_image": null,
"comped": false,
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"email": "create-member-offer-test@email.com",
"email_count": 0,
"email_open_rate": null,
"email_opened_count": 0,
"email_suppression": Object {
"info": null,
"suppressed": false,
},
"geolocation": null,
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"labels": Any<Array>,
"last_seen_at": null,
"name": "Test Member",
"newsletters": Any<Array>,
"note": null,
"status": "paid",
"subscribed": true,
"subscriptions": Any<Array>,
"tiers": Any<Array>,
"updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/,
},
Object {
"avatar_image": null,
"comped": true,
@ -4024,7 +4298,7 @@ Object {
"page": 1,
"pages": 1,
"prev": null,
"total": 8,
"total": 10,
},
},
}
@ -4034,7 +4308,7 @@ exports[`Members API Can filter on tier slug 2: [headers] 1`] = `
Object {
"access-control-allow-origin": "http://127.0.0.1:2369",
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
"content-length": "23956",
"content-length": "30800",
"content-type": "application/json; charset=utf-8",
"content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/,
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,

View File

@ -1395,6 +1395,229 @@ describe('Members API', function () {
});
});
it('Can create an offer in Ghost when Stripe subscription has an unknown offer attached', async function () {
const fakePrice = {
id: 'price_test_offer_123',
product: 'product_test_offer_123',
active: true,
nickname: 'Monthly',
unit_amount: 5000,
currency: 'usd',
type: 'recurring',
recurring: {
interval: 'month'
}
};
const fakeSubscription = {
id: 'sub_offer_test_123',
customer: 'cus_offer_test_123',
status: 'active',
cancel_at_period_end: false,
metadata: {},
current_period_end: Date.now() / 1000 + 50000,
start_date: Date.now() / 1000,
plan: fakePrice,
items: {
data: [{
price: fakePrice
}]
},
discount: {
coupon: {
id: 'coupon_1',
name: 'Stripe Special',
duration: 'repeating',
duration_in_months: 5,
amount_off: 1000
}
}
};
const fakeCustomer = {
id: 'cus_offer_test_123',
name: 'Test Member',
email: 'create-member-offer-test@email.com',
subscriptions: {
type: 'list',
data: [fakeSubscription]
}
};
stripeMocker.customers.push(fakeCustomer);
stripeMocker.subscriptions.push(fakeSubscription);
stripeMocker.prices.push(fakePrice);
const initialMember = {
name: fakeCustomer.name,
email: fakeCustomer.email,
subscribed: true,
newsletters: [newsletters[0]],
stripe_customer_id: fakeCustomer.id
};
const {body} = await agent
.post(`/members/`)
.body({members: [initialMember]})
.expectStatus(201)
.matchBodySnapshot({
members: new Array(1).fill({
id: anyObjectId,
uuid: anyUuid,
created_at: anyISODateTime,
updated_at: anyISODateTime,
labels: anyArray,
subscriptions: anyArray,
tiers: new Array(1).fill(tierMatcher),
newsletters: new Array(1).fill(newsletterSnapshot)
})
})
.matchHeaderSnapshot({
'content-version': anyContentVersion,
etag: anyEtag,
location: anyLocationFor('members')
});
const newMember = body.members[0];
assert.equal(newMember.status, 'paid', 'The created member should have the paid status');
// Check offer is created in the database
const offers = (await models.Offer.findAll({filter: 'name:\'Stripe Special\''})).models;
assert.equal(offers.length, 1);
assert.equal(offers[0].get('name'), 'Stripe Special');
assert.equal(offers[0].get('code'), 'stripe-special');
assert.equal(offers[0].get('stripe_coupon_id'), 'coupon_1');
assert.equal(offers[0].get('discount_type'), 'amount');
assert.equal(offers[0].get('discount_amount'), 1000);
assert.equal(offers[0].get('duration'), 'repeating');
assert.equal(offers[0].get('duration_in_months'), 5);
// Check subscription is linked to offer
assert.equal(newMember.subscriptions[0].offer.id, offers[0].id);
await assertSubscription('sub_offer_test_123', {
subscription_id: 'sub_offer_test_123',
status: 'active',
cancel_at_period_end: false,
plan_amount: 5000,
plan_interval: 'month',
plan_currency: 'usd',
mrr: 5000,
offer_id: offers[0].get('id')
});
});
it('Can create an offer in Ghost when Stripe subscription has an unknown offer attached with a duplicate name', async function () {
const existingOffers = (await models.Offer.findAll({filter: 'name:\'Stripe Special\''})).models;
assert.equal(existingOffers.length, 1, 'This test expects an offer with the name Stripe Special to already exist');
const fakePrice = {
id: 'price_test_offer_1234',
product: 'product_test_offer_1234',
active: true,
nickname: 'Monthly',
unit_amount: 5000,
currency: 'usd',
type: 'recurring',
recurring: {
interval: 'month'
}
};
const fakeSubscription = {
id: 'sub_offer_test_1234',
customer: 'cus_offer_test_1234',
status: 'active',
cancel_at_period_end: false,
metadata: {},
current_period_end: Date.now() / 1000 + 50000,
start_date: Date.now() / 1000,
plan: fakePrice,
items: {
data: [{
price: fakePrice
}]
},
discount: {
coupon: {
id: 'coupon_2',
name: 'Stripe Special', // Duplicate name
duration: 'repeating',
duration_in_months: 5,
amount_off: 1000
}
}
};
const fakeCustomer = {
id: 'cus_offer_test_1234',
name: 'Test Member',
email: 'create-member-offer-test2@email.com',
subscriptions: {
type: 'list',
data: [fakeSubscription]
}
};
stripeMocker.customers.push(fakeCustomer);
stripeMocker.subscriptions.push(fakeSubscription);
stripeMocker.prices.push(fakePrice);
const initialMember = {
name: fakeCustomer.name,
email: fakeCustomer.email,
subscribed: true,
newsletters: [newsletters[0]],
stripe_customer_id: fakeCustomer.id
};
const {body} = await agent
.post(`/members/`)
.body({members: [initialMember]})
.expectStatus(201)
.matchBodySnapshot({
members: new Array(1).fill({
id: anyObjectId,
uuid: anyUuid,
created_at: anyISODateTime,
updated_at: anyISODateTime,
labels: anyArray,
subscriptions: anyArray,
tiers: new Array(1).fill(tierMatcher),
newsletters: new Array(1).fill(newsletterSnapshot)
})
})
.matchHeaderSnapshot({
'content-version': anyContentVersion,
etag: anyEtag,
location: anyLocationFor('members')
});
const newMember = body.members[0];
assert.equal(newMember.status, 'paid', 'The created member should have the paid status');
// Check offer is created in the database
const offers = (await models.Offer.findAll({filter: 'name:\'Stripe Special (coupon_2)\''})).models;
assert.equal(offers.length, 1);
assert.equal(offers[0].get('name'), 'Stripe Special (coupon_2)');
assert.equal(offers[0].get('code'), 'stripe-special-coupon_2');
assert.equal(offers[0].get('stripe_coupon_id'), 'coupon_2');
assert.equal(offers[0].get('discount_type'), 'amount');
assert.equal(offers[0].get('discount_amount'), 1000);
assert.equal(offers[0].get('duration'), 'repeating');
assert.equal(offers[0].get('duration_in_months'), 5);
// Check subscription is linked to offer
assert.equal(newMember.subscriptions[0].offer.id, offers[0].id);
await assertSubscription('sub_offer_test_1234', {
subscription_id: 'sub_offer_test_1234',
status: 'active',
cancel_at_period_end: false,
plan_amount: 5000,
plan_interval: 'month',
plan_currency: 'usd',
mrr: 5000,
offer_id: offers[0].get('id')
});
});
let memberWithPaidSubscription;
it('Can create a member with an existing paid subscription', async function () {
@ -2328,7 +2551,7 @@ describe('Members API', function () {
.get('/members/?include=tiers&filter=tier:default-product')
.expectStatus(200)
.matchBodySnapshot({
members: new Array(8).fill(buildMemberMatcherShallowIncludesWithTiers())
members: new Array(10).fill(buildMemberMatcherShallowIncludesWithTiers())
})
.matchHeaderSnapshot({
'content-version': anyContentVersion,

View File

@ -1496,141 +1496,6 @@ describe('Members API', function () {
]
});
});
it('Silently ignores an invalid offer id in metadata', async function () {
const interval = 'month';
const unit_amount = 500;
const mrr_with = 400;
const discount = {
id: 'di_1Knkn7HUEDadPGIBPOQgmzIX',
object: 'discount',
checkout_session: null,
coupon: {
id: 'unknownCoupon', // this one is unknown in Ghost
object: 'coupon',
amount_off: null,
created: 1649774041,
currency: 'eur',
duration: 'forever',
duration_in_months: null,
livemode: false,
max_redemptions: null,
metadata: {},
name: '20% off',
percent_off: 20,
redeem_by: null,
times_redeemed: 0,
valid: true
},
end: null,
invoice: null,
invoice_item: null,
promotion_code: null,
start: beforeNow / 1000,
subscription: null
};
const customer_id = createStripeID('cust');
const subscription_id = createStripeID('sub');
discount.customer = customer_id;
set(subscription, {
id: subscription_id,
customer: customer_id,
status: 'active',
discount,
items: {
type: 'list',
data: [{
id: 'item_123',
price: {
id: 'price_123',
product: 'product_123',
active: true,
nickname: interval,
currency: 'usd',
recurring: {
interval
},
unit_amount,
type: 'recurring'
}
}]
},
start_date: beforeNow / 1000,
current_period_end: Math.floor(beforeNow / 1000) + (60 * 60 * 24 * 31),
cancel_at_period_end: false
});
set(customer, {
id: customer_id,
name: 'Test Member',
email: `${customer_id}@email.com`,
subscriptions: {
type: 'list',
data: [subscription]
}
});
let webhookPayload = JSON.stringify({
type: 'checkout.session.completed',
data: {
object: {
mode: 'subscription',
customer: customer.id,
subscription: subscription.id,
metadata: {}
}
}
});
let webhookSignature = stripe.webhooks.generateTestHeaderString({
payload: webhookPayload,
secret: process.env.WEBHOOK_SECRET
});
await membersAgent.post('/webhooks/stripe/')
.body(webhookPayload)
.header('content-type', 'application/json')
.header('stripe-signature', webhookSignature);
const {body} = await adminAgent.get(`/members/?search=${customer_id}@email.com`);
assert.equal(body.members.length, 1, 'The member was not created');
const member = body.members[0];
assert.equal(member.status, 'paid', 'The member should be "paid"');
assert.equal(member.subscriptions.length, 1, 'The member should have a single subscription');
// Check whether MRR and status has been set
await assertSubscription(member.subscriptions[0].id, {
subscription_id: subscription.id,
status: 'active',
cancel_at_period_end: false,
plan_amount: unit_amount,
plan_interval: interval,
plan_currency: 'usd',
current_period_end: new Date(Math.floor(beforeNow / 1000) * 1000 + (60 * 60 * 24 * 31 * 1000)),
mrr: mrr_with,
offer_id: null
});
// Check whether the offer attribute is passed correctly in the response when fetching a single member
member.subscriptions[0].should.match({
offer: null
});
await assertMemberEvents({
eventType: 'MemberPaidSubscriptionEvent',
memberId: member.id,
asserts: [
{
mrr_delta: mrr_with
}
]
});
});
});
// Test if the session metadata is processed correctly

View File

@ -194,11 +194,21 @@ class OfferRepository {
*/
async createFromCoupon(coupon, params, options) {
const {productId, currency, interval, active} = params;
const code = coupon.name && coupon.name.split(' ').map(word => word.toLowerCase()).join('-');
let code = coupon.name && coupon.name.split(' ').map(word => word.toLowerCase()).join('-');
let name = coupon.name;
// If name or coupon already exists, we'll append the Stripe id to the name and code
if (await this.existsByName(name, options)) {
name = `${name} (${coupon.id})`;
}
if (await this.existsByCode(code, options)) {
code = `${code}-${coupon.id}`;
}
const data = {
active,
name: coupon.name,
name,
code,
product_id: productId,
stripe_coupon_id: coupon.id,
@ -217,7 +227,7 @@ class OfferRepository {
data.discount_amount = coupon.amount_off;
}
await this.OfferModel.add(data, options);
return await this.OfferModel.add(data, options);
}
/**

View File

@ -2,7 +2,8 @@ const sinon = require('sinon');
const OfferRepository = require('../../../lib/application/OfferRepository');
const Offer = {
add: sinon.stub()
add: sinon.stub(),
findOne: sinon.stub()
};
describe('OfferRepository', function () {