From 0b4ea162ca9a461b346ddad08d2271f7080013f3 Mon Sep 17 00:00:00 2001 From: Steve Larson <9larsons@gmail.com> Date: Thu, 13 Feb 2025 17:13:24 -0600 Subject: [PATCH] =?UTF-8?q?Revert=20"=F0=9F=90=9B=20Fixed=20missing=20subs?= =?UTF-8?q?cription=20attribution=20on=20free=20to=20paid=20upgrade=20(#21?= =?UTF-8?q?846)"=20(#22191)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ref https://linear.app/ghost/issue/ONC-729/ - this commit caused issues w/ subscription linking --- .../__snapshots__/webhooks.test.js.snap | 435 +----------------- .../test/e2e-api/members/webhooks.test.js | 254 +--------- .../webhook/SubscriptionEventService.js | 40 +- .../webhooks/SubscriptionEventService.test.js | 77 ++-- 4 files changed, 73 insertions(+), 733 deletions(-) diff --git a/ghost/core/test/e2e-api/members/__snapshots__/webhooks.test.js.snap b/ghost/core/test/e2e-api/members/__snapshots__/webhooks.test.js.snap index e9548f7d9a4..48b12df4198 100644 --- a/ghost/core/test/e2e-api/members/__snapshots__/webhooks.test.js.snap +++ b/ghost/core/test/e2e-api/members/__snapshots__/webhooks.test.js.snap @@ -48,54 +48,6 @@ Object { } `; -exports[`Members API Member attribution Creates a SubscriptionCreatedEvent with author attribution 3: [body] 1`] = ` -Object { - "members": Array [ - Object { - "attribution": Any, - "avatar_image": null, - "comped": false, - "created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, - "email": Any, - "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, - "last_seen_at": null, - "name": null, - "newsletters": Any, - "note": null, - "status": "paid", - "subscribed": false, - "subscriptions": Any, - "tiers": Any, - "unsubscribe_url": "http://domain.com/unsubscribe/?uuid=memberuuid&key=abc123dontstealme", - "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 Member attribution Creates a SubscriptionCreatedEvent with author attribution 4: [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": "2384", - "content-type": "application/json; charset=utf-8", - "content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/, - "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, - "vary": "Accept-Version, Origin, Accept-Encoding", - "x-powered-by": "Express", -} -`; - exports[`Members API Member attribution Creates a SubscriptionCreatedEvent with deleted post attribution 1: [body] 1`] = ` Object { "members": Array [ @@ -144,54 +96,6 @@ Object { } `; -exports[`Members API Member attribution Creates a SubscriptionCreatedEvent with deleted post attribution 3: [body] 1`] = ` -Object { - "members": Array [ - Object { - "attribution": Any, - "avatar_image": null, - "comped": false, - "created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, - "email": Any, - "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, - "last_seen_at": null, - "name": null, - "newsletters": Any, - "note": null, - "status": "paid", - "subscribed": false, - "subscriptions": Any, - "tiers": Any, - "unsubscribe_url": "http://domain.com/unsubscribe/?uuid=memberuuid&key=abc123dontstealme", - "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 Member attribution Creates a SubscriptionCreatedEvent with deleted post attribution 4: [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": "2391", - "content-type": "application/json; charset=utf-8", - "content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/, - "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, - "vary": "Accept-Version, Origin, Accept-Encoding", - "x-powered-by": "Express", -} -`; - exports[`Members API Member attribution Creates a SubscriptionCreatedEvent with empty attribution object 1: [body] 1`] = ` Object { "members": Array [ @@ -240,54 +144,6 @@ Object { } `; -exports[`Members API Member attribution Creates a SubscriptionCreatedEvent with empty attribution object 3: [body] 1`] = ` -Object { - "members": Array [ - Object { - "attribution": Any, - "avatar_image": null, - "comped": false, - "created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, - "email": Any, - "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, - "last_seen_at": null, - "name": null, - "newsletters": Any, - "note": null, - "status": "paid", - "subscribed": false, - "subscriptions": Any, - "tiers": Any, - "unsubscribe_url": "http://domain.com/unsubscribe/?uuid=memberuuid&key=abc123dontstealme", - "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 Member attribution Creates a SubscriptionCreatedEvent with empty attribution object 4: [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": "2335", - "content-type": "application/json; charset=utf-8", - "content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/, - "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, - "vary": "Accept-Version, Origin, Accept-Encoding", - "x-powered-by": "Express", -} -`; - exports[`Members API Member attribution Creates a SubscriptionCreatedEvent with page attribution 1: [body] 1`] = ` Object { "members": Array [ @@ -336,54 +192,6 @@ Object { } `; -exports[`Members API Member attribution Creates a SubscriptionCreatedEvent with page attribution 3: [body] 1`] = ` -Object { - "members": Array [ - Object { - "attribution": Any, - "avatar_image": null, - "comped": false, - "created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, - "email": Any, - "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, - "last_seen_at": null, - "name": null, - "newsletters": Any, - "note": null, - "status": "paid", - "subscribed": false, - "subscriptions": Any, - "tiers": Any, - "unsubscribe_url": "http://domain.com/unsubscribe/?uuid=memberuuid&key=abc123dontstealme", - "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 Member attribution Creates a SubscriptionCreatedEvent with page attribution 4: [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": "2415", - "content-type": "application/json; charset=utf-8", - "content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/, - "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, - "vary": "Accept-Version, Origin, Accept-Encoding", - "x-powered-by": "Express", -} -`; - exports[`Members API Member attribution Creates a SubscriptionCreatedEvent with post attribution 1: [body] 1`] = ` Object { "members": Array [ @@ -432,54 +240,6 @@ Object { } `; -exports[`Members API Member attribution Creates a SubscriptionCreatedEvent with post attribution 3: [body] 1`] = ` -Object { - "members": Array [ - Object { - "attribution": Any, - "avatar_image": null, - "comped": false, - "created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, - "email": Any, - "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, - "last_seen_at": null, - "name": null, - "newsletters": Any, - "note": null, - "status": "paid", - "subscribed": false, - "subscriptions": Any, - "tiers": Any, - "unsubscribe_url": "http://domain.com/unsubscribe/?uuid=memberuuid&key=abc123dontstealme", - "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 Member attribution Creates a SubscriptionCreatedEvent with post attribution 4: [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": "2398", - "content-type": "application/json; charset=utf-8", - "content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/, - "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, - "vary": "Accept-Version, Origin, Accept-Encoding", - "x-powered-by": "Express", -} -`; - exports[`Members API Member attribution Creates a SubscriptionCreatedEvent with tag attribution 1: [body] 1`] = ` Object { "members": Array [ @@ -528,54 +288,6 @@ Object { } `; -exports[`Members API Member attribution Creates a SubscriptionCreatedEvent with tag attribution 3: [body] 1`] = ` -Object { - "members": Array [ - Object { - "attribution": Any, - "avatar_image": null, - "comped": false, - "created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, - "email": Any, - "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, - "last_seen_at": null, - "name": null, - "newsletters": Any, - "note": null, - "status": "paid", - "subscribed": false, - "subscriptions": Any, - "tiers": Any, - "unsubscribe_url": "http://domain.com/unsubscribe/?uuid=memberuuid&key=abc123dontstealme", - "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 Member attribution Creates a SubscriptionCreatedEvent with tag attribution 4: [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": "2405", - "content-type": "application/json; charset=utf-8", - "content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/, - "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, - "vary": "Accept-Version, Origin, Accept-Encoding", - "x-powered-by": "Express", -} -`; - exports[`Members API Member attribution Creates a SubscriptionCreatedEvent with url attribution 1: [body] 1`] = ` Object { "members": Array [ @@ -624,54 +336,6 @@ Object { } `; -exports[`Members API Member attribution Creates a SubscriptionCreatedEvent with url attribution 3: [body] 1`] = ` -Object { - "members": Array [ - Object { - "attribution": Any, - "avatar_image": null, - "comped": false, - "created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, - "email": Any, - "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, - "last_seen_at": null, - "name": null, - "newsletters": Any, - "note": null, - "status": "paid", - "subscribed": false, - "subscriptions": Any, - "tiers": Any, - "unsubscribe_url": "http://domain.com/unsubscribe/?uuid=memberuuid&key=abc123dontstealme", - "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 Member attribution Creates a SubscriptionCreatedEvent with url attribution 4: [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": "2362", - "content-type": "application/json; charset=utf-8", - "content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/, - "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, - "vary": "Accept-Version, Origin, Accept-Encoding", - "x-powered-by": "Express", -} -`; - exports[`Members API Member attribution Creates a SubscriptionCreatedEvent without attribution 1: [body] 1`] = ` Object { "members": Array [ @@ -720,54 +384,6 @@ Object { } `; -exports[`Members API Member attribution Creates a SubscriptionCreatedEvent without attribution 3: [body] 1`] = ` -Object { - "members": Array [ - Object { - "attribution": Any, - "avatar_image": null, - "comped": false, - "created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/, - "email": Any, - "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, - "last_seen_at": null, - "name": null, - "newsletters": Any, - "note": null, - "status": "paid", - "subscribed": false, - "subscriptions": Any, - "tiers": Any, - "unsubscribe_url": "http://domain.com/unsubscribe/?uuid=memberuuid&key=abc123dontstealme", - "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 Member attribution Creates a SubscriptionCreatedEvent without attribution 4: [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": "2335", - "content-type": "application/json; charset=utf-8", - "content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/, - "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, - "vary": "Accept-Version, Origin, Accept-Encoding", - "x-powered-by": "Express", -} -`; - exports[`Members API Member attribution Returns subscription created attributions in activity feed 1: [body] 1`] = ` Object { "events": Array [ @@ -803,70 +419,25 @@ Object { "data": Any, "type": "subscription_event", }, - Object { - "data": Any, - "type": "subscription_event", - }, - Object { - "data": Any, - "type": "subscription_event", - }, - Object { - "data": Any, - "type": "subscription_event", - }, - Object { - "data": Any, - "type": "subscription_event", - }, - Object { - "data": Any, - "type": "subscription_event", - }, - Object { - "data": Any, - "type": "subscription_event", - }, - Object { - "data": Any, - "type": "subscription_event", - }, - Object { - "data": Any, - "type": "subscription_event", - }, ], "meta": Object { "pagination": Object { - "limit": "100", + "limit": 10, "next": null, "page": null, "pages": 1, "prev": null, - "total": 16, + "total": 8, }, }, } `; -exports[`Members API Member attribution Returns subscription created attributions in activity feed 1: [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": "7027", - "content-type": "application/json; charset=utf-8", - "content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/, - "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, - "vary": "Accept-Version, Origin, Accept-Encoding", - "x-powered-by": "Express", -} -`; - exports[`Members API Member attribution Returns subscription created attributions in activity feed 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": "11237", + "content-length": "5664", "content-type": "application/json; charset=utf-8", "content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/, "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, diff --git a/ghost/core/test/e2e-api/members/webhooks.test.js b/ghost/core/test/e2e-api/members/webhooks.test.js index 4f59a80aeb9..ae461318d23 100644 --- a/ghost/core/test/e2e-api/members/webhooks.test.js +++ b/ghost/core/test/e2e-api/members/webhooks.test.js @@ -1855,7 +1855,7 @@ describe('Members API', function () { }); }); - async function testAttributionOnSignup(attribution, attributionResource) { + async function testWithAttribution(attribution, attributionResource) { const customer_id = createStripeID('cust'); const subscription_id = createStripeID('sub'); @@ -1900,31 +1900,7 @@ describe('Members API', function () { } }); - // Stripe first sends a customer.subscription.created webhook - const subscriptionWebhookPayload = JSON.stringify({ - type: 'customer.subscription.created', - data: { - object: subscription - } - }); - - const subscriptionWebhookSignature = stripe.webhooks.generateTestHeaderString({ - payload: subscriptionWebhookPayload, - secret: process.env.WEBHOOK_SECRET - }); - - await membersAgent.post('/webhooks/stripe/') - .body(subscriptionWebhookPayload) - .header('content-type', 'application/json') - .header('stripe-signature', subscriptionWebhookSignature) - .expectStatus(200); - - // This should not create a member in the database yet - const {body: bodyAfterSubscriptionWebhook} = await adminAgent.get(`/members/?search=${customer_id}@email.com`); - assert.equal(bodyAfterSubscriptionWebhook.members.length, 0, 'A member was created before the checkout.session.completed webhook was sent'); - - // Then it sends a checkout.session.completed webhook - const checkoutWebhookPayload = JSON.stringify({ + let webhookPayload = JSON.stringify({ type: 'checkout.session.completed', data: { object: { @@ -1940,15 +1916,15 @@ describe('Members API', function () { } }); - const checkoutWebhookSignature = stripe.webhooks.generateTestHeaderString({ - payload: checkoutWebhookPayload, + let webhookSignature = stripe.webhooks.generateTestHeaderString({ + payload: webhookPayload, secret: process.env.WEBHOOK_SECRET }); await membersAgent.post('/webhooks/stripe/') - .body(checkoutWebhookPayload) + .body(webhookPayload) .header('content-type', 'application/json') - .header('stripe-signature', checkoutWebhookSignature) + .header('stripe-signature', webhookSignature) .expectStatus(200); const {body} = await adminAgent.get(`/members/?search=${customer_id}@email.com`); @@ -2017,161 +1993,6 @@ describe('Members API', function () { return memberModel; } - async function testAttributionOnUpgrade(attribution, attributionResource) { - const customer_id = createStripeID('cust'); - const subscription_id = createStripeID('sub'); - - const interval = 'month'; - const unit_amount = 150; - - // Create initial free member - const initialFreeMember = await models.Member.add({ - email: `${customer_id}@email.com`, - status: 'free', - email_disabled: false - }); - - // Create a Stripe Customer too for the free member, as this is created during Stripe Checkout, i.e. before receiving Stripe webhooks - await models.MemberStripeCustomer.add({ - member_id: initialFreeMember.id, - customer_id: customer_id, - email: initialFreeMember.get('email') - }); - - set(subscription, { - id: subscription_id, - customer: customer_id, - status: 'active', - 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, - metadata: {} - }); - - set(customer, { - id: customer_id, - name: 'Test Member', - email: `${customer_id}@email.com`, - subscriptions: { - type: 'list', - data: [subscription] - } - }); - - // Stripe first sends a customer.subscription.created webhook - const subscriptionWebhookPayload = JSON.stringify({ - type: 'customer.subscription.created', - data: { - object: subscription - } - }); - - const subscriptionWebhookSignature = stripe.webhooks.generateTestHeaderString({ - payload: subscriptionWebhookPayload, - secret: process.env.WEBHOOK_SECRET - }); - - await membersAgent.post('/webhooks/stripe/') - .body(subscriptionWebhookPayload) - .header('content-type', 'application/json') - .header('stripe-signature', subscriptionWebhookSignature) - .expectStatus(200); - - // This should not create a member subscription in the database yet - const {body: bodyAfterSubscriptionWebhook} = await adminAgent.get(`/members/?search=${customer_id}@email.com`); - const memberAfterSubscriptionWebhook = bodyAfterSubscriptionWebhook.members[0]; - - assert.equal(memberAfterSubscriptionWebhook.subscriptions.length, 0, 'The member should not have a subscription after customer.subscription.updated'); - assert.equal(memberAfterSubscriptionWebhook.status, 'free', 'The member status should still be "free" after customer.subscription.updated'); - - // Then it sends a checkout.session.completed webhook - const checkoutWebhookPayload = JSON.stringify({ - type: 'checkout.session.completed', - data: { - object: { - mode: 'subscription', - customer: customer.id, - subscription: subscription.id, - metadata: attribution ? { - attribution_id: attribution.id, - attribution_url: attribution.url, - attribution_type: attribution.type - } : {} - } - } - }); - - const checkoutWebhookSignature = stripe.webhooks.generateTestHeaderString({ - payload: checkoutWebhookPayload, - secret: process.env.WEBHOOK_SECRET - }); - - await membersAgent.post('/webhooks/stripe/') - .body(checkoutWebhookPayload) - .header('content-type', 'application/json') - .header('stripe-signature', checkoutWebhookSignature) - .expectStatus(200); - - 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" after checkout.session.completed'); - assert.equal(member.subscriptions.length, 1, 'The member should have a single subscription after checkout.session.completed'); - - // Convert Stripe ID to internal model ID - const subscriptionModel = await getSubscription(member.subscriptions[0].id); - - await assertMemberEvents({ - eventType: 'SubscriptionCreatedEvent', - memberId: member.id, - asserts: [ - { - member_id: member.id, - subscription_id: subscriptionModel.id, - - // Defaults if attribution is not set - attribution_id: attribution?.id ?? null, - attribution_url: attribution?.url ?? null, - attribution_type: attribution?.type ?? null - } - ] - }); - - await adminAgent - .get(`/members/${member.id}/`) - .expectStatus(200) - .matchBodySnapshot({ - members: new Array(1).fill(memberMatcherShallowIncludes) - }) - .matchHeaderSnapshot({ - 'content-version': anyContentVersion, - etag: anyEtag - }) - .expect(({body: body3}) => { - should(body3.members[0].subscriptions[0].attribution).eql(attributionResource); - subscriptionAttributions.push(body3.members[0].subscriptions[0].attribution); - }); - } - const subscriptionAttributions = []; it('Creates a SubscriptionCreatedEvent with url attribution', async function () { @@ -2184,7 +2005,7 @@ describe('Members API', function () { const absoluteUrl = urlUtils.createUrl('/', true); - const attributionResource = { + await testWithAttribution(attribution, { id: null, url: absoluteUrl, type: 'url', @@ -2192,10 +2013,7 @@ describe('Members API', function () { referrer_source: null, referrer_medium: null, referrer_url: null - }; - - await testAttributionOnSignup(attribution, attributionResource); - await testAttributionOnUpgrade(attribution, attributionResource); + }); }); it('Creates a SubscriptionCreatedEvent with post attribution', async function () { @@ -2210,7 +2028,7 @@ describe('Members API', function () { const absoluteUrl = urlService.getUrlByResourceId(post.id, {absolute: true}); - const attributionResource = { + await testWithAttribution(attribution, { id: post.id, url: absoluteUrl, type: 'post', @@ -2218,10 +2036,7 @@ describe('Members API', function () { referrer_source: null, referrer_medium: null, referrer_url: null - }; - - await testAttributionOnSignup(attribution, attributionResource); - await testAttributionOnUpgrade(attribution, attributionResource); + }); }); it('Creates a SubscriptionCreatedEvent with deleted post attribution', async function () { @@ -2233,7 +2048,7 @@ describe('Members API', function () { const absoluteUrl = urlUtils.createUrl('/removed-blog-post/', true); - const attributionResource = { + await testWithAttribution(attribution, { id: null, url: absoluteUrl, type: 'url', @@ -2241,10 +2056,7 @@ describe('Members API', function () { referrer_source: null, referrer_medium: null, referrer_url: null - }; - - await testAttributionOnSignup(attribution, attributionResource); - await testAttributionOnUpgrade(attribution, attributionResource); + }); }); it('Creates a SubscriptionCreatedEvent with page attribution', async function () { @@ -2259,7 +2071,7 @@ describe('Members API', function () { const absoluteUrl = urlService.getUrlByResourceId(post.id, {absolute: true}); - const attributionResource = { + await testWithAttribution(attribution, { id: post.id, url: absoluteUrl, type: 'page', @@ -2267,10 +2079,7 @@ describe('Members API', function () { referrer_source: null, referrer_medium: null, referrer_url: null - }; - - await testAttributionOnSignup(attribution, attributionResource); - await testAttributionOnUpgrade(attribution, attributionResource); + }); }); it('Creates a SubscriptionCreatedEvent with tag attribution', async function () { @@ -2285,7 +2094,7 @@ describe('Members API', function () { const absoluteUrl = urlService.getUrlByResourceId(tag.id, {absolute: true}); - const attributionResource = { + await testWithAttribution(attribution, { id: tag.id, url: absoluteUrl, type: 'tag', @@ -2293,10 +2102,7 @@ describe('Members API', function () { referrer_source: null, referrer_medium: null, referrer_url: null - }; - - await testAttributionOnSignup(attribution, attributionResource); - await testAttributionOnUpgrade(attribution, attributionResource); + }); }); it('Creates a SubscriptionCreatedEvent with author attribution', async function () { @@ -2311,7 +2117,7 @@ describe('Members API', function () { const absoluteUrl = urlService.getUrlByResourceId(author.id, {absolute: true}); - const attributionResource = { + await testWithAttribution(attribution, { id: author.id, url: absoluteUrl, type: 'author', @@ -2319,16 +2125,12 @@ describe('Members API', function () { referrer_source: null, referrer_medium: null, referrer_url: null - }; - - await testAttributionOnSignup(attribution, attributionResource); - await testAttributionOnUpgrade(attribution, attributionResource); + }); }); it('Creates a SubscriptionCreatedEvent without attribution', async function () { const attribution = undefined; - - const attributionResource = { + await testWithAttribution(attribution, { id: null, url: null, type: null, @@ -2336,17 +2138,13 @@ describe('Members API', function () { referrer_source: null, referrer_medium: null, referrer_url: null - }; - - await testAttributionOnSignup(attribution, attributionResource); - await testAttributionOnUpgrade(attribution, attributionResource); + }); }); it('Creates a SubscriptionCreatedEvent with empty attribution object', async function () { // Shouldn't happen, but to make sure we handle it const attribution = {}; - - const attributionResource = { + await testWithAttribution(attribution, { id: null, url: null, type: null, @@ -2354,10 +2152,7 @@ describe('Members API', function () { referrer_source: null, referrer_medium: null, referrer_url: null - }; - - await testAttributionOnSignup(attribution, attributionResource); - await testAttributionOnUpgrade(attribution, attributionResource); + }); }); // Activity feed @@ -2365,7 +2160,7 @@ describe('Members API', function () { it('Returns subscription created attributions in activity feed', async function () { // Check activity feed await adminAgent - .get(`/members/events/?filter=type:subscription_event&limit=100`) + .get(`/members/events/?filter=type:subscription_event`) .expectStatus(200) .matchHeaderSnapshot({ 'content-version': anyContentVersion, @@ -2373,8 +2168,7 @@ describe('Members API', function () { }) .matchBodySnapshot({ events: new Array(subscriptionAttributions.length).fill({ - data: anyObject, - type: 'subscription_event' + data: anyObject }) }) .expect(({body}) => { diff --git a/ghost/stripe/lib/services/webhook/SubscriptionEventService.js b/ghost/stripe/lib/services/webhook/SubscriptionEventService.js index 26e531a5c07..8432e44b8c7 100644 --- a/ghost/stripe/lib/services/webhook/SubscriptionEventService.js +++ b/ghost/stripe/lib/services/webhook/SubscriptionEventService.js @@ -1,6 +1,5 @@ const errors = require('@tryghost/errors'); const _ = require('lodash'); -const logging = require('@tryghost/logging'); module.exports = class SubscriptionEventService { constructor(deps) { this.deps = deps; @@ -19,35 +18,18 @@ module.exports = class SubscriptionEventService { customer_id: subscription.customer }); - // After checkout, Stripe sends `customer.subscription.created`, `customer.subscription.updated` and `checkout.session.completed` events - // We want to create a member and its related subscription in the database based on the `checkout.session.completed` event as it contains additional information on the subscription (e.g. attribution data) - // Therefore, if the member or the subscription does not exist in the database yet, we ignore `customer.subscription.*` events, to avoid creating subscriptions with missing data - if (!member) { - logging.info(`Ignoring customer.subscription.* event as member does not exist`); - return; - } - - const memberSubscription = await member.related('stripeSubscriptions').query({ - where: { - subscription_id: subscription.id - } - }).fetchOne(); - - if (!memberSubscription) { - logging.info(`Ignoring customer.subscription.* event as member subscription does not exist`); - return; - } - - try { - await memberRepository.linkSubscription({ - id: member.id, - subscription - }); - } catch (err) { - if (err.code !== 'ER_DUP_ENTRY' && err.code !== 'SQLITE_CONSTRAINT') { - throw err; + if (member) { + try { + await memberRepository.linkSubscription({ + id: member.id, + subscription + }); + } catch (err) { + if (err.code !== 'ER_DUP_ENTRY' && err.code !== 'SQLITE_CONSTRAINT') { + throw err; + } + throw new errors.ConflictError({err}); } - throw new errors.ConflictError({err}); } } }; diff --git a/ghost/stripe/test/unit/lib/services/webhooks/SubscriptionEventService.test.js b/ghost/stripe/test/unit/lib/services/webhooks/SubscriptionEventService.test.js index 39a7fe044f2..42beff74a8b 100644 --- a/ghost/stripe/test/unit/lib/services/webhooks/SubscriptionEventService.test.js +++ b/ghost/stripe/test/unit/lib/services/webhooks/SubscriptionEventService.test.js @@ -6,55 +6,15 @@ const SubscriptionEventService = require('../../../../../lib/services/webhook/Su describe('SubscriptionEventService', function () { let service; let memberRepository; - let member; - let subscription; beforeEach(function () { - member = { - id: 'member_123', - related: sinon.stub().returns({ - query: sinon.stub().returns({ - fetchOne: sinon.stub().resolves({subscription_id: 'sub_123'}) - }) - }) - }; - - memberRepository = { - get: sinon.stub().resolves(member), - linkSubscription: sinon.stub() - }; - - subscription = { - id: 'sub_123', - items: { - data: [{price: {id: 'price_123'}}] - }, - customer: 'cust_123' - }; + memberRepository = {get: sinon.stub(), linkSubscription: sinon.stub()}; service = new SubscriptionEventService({memberRepository}); }); - it('should not call linkSubscription if member does not exist', async function () { - memberRepository.get.resolves(null); - - await service.handleSubscriptionEvent(subscription); - assert(memberRepository.linkSubscription.notCalled); - }); - - it('should not call linkSubscription if member subscription does not exist', async function () { - member.related.returns({ - query: sinon.stub().returns({ - fetchOne: sinon.stub().resolves(null) - }) - }); - - await service.handleSubscriptionEvent(subscription); - assert(memberRepository.linkSubscription.notCalled); - }); - it('should throw BadRequestError if subscription has no price item', async function () { - subscription = { + const subscription = { items: { data: [] } @@ -69,6 +29,14 @@ describe('SubscriptionEventService', function () { }); it('should throw ConflictError if linkSubscription fails with ER_DUP_ENTRY', async function () { + const subscription = { + items: { + data: [{price: {id: 'price_123'}}] + }, + customer: 'cust_123' + }; + + memberRepository.get.resolves({id: 'member_123'}); memberRepository.linkSubscription.rejects({code: 'ER_DUP_ENTRY'}); try { @@ -80,6 +48,14 @@ describe('SubscriptionEventService', function () { }); it('should throw ConflictError if linkSubscription fails with SQLITE_CONSTRAINT', async function () { + const subscription = { + items: { + data: [{price: {id: 'price_123'}}] + }, + customer: 'cust_123' + }; + + memberRepository.get.resolves({id: 'member_123'}); memberRepository.linkSubscription.rejects({code: 'SQLITE_CONSTRAINT'}); try { @@ -91,6 +67,14 @@ describe('SubscriptionEventService', function () { }); it('should throw if linkSubscription fails with unexpected error', async function () { + const subscription = { + items: { + data: [{price: {id: 'price_123'}}] + }, + customer: 'cust_123' + }; + + memberRepository.get.resolves({id: 'member_123'}); memberRepository.linkSubscription.rejects(new Error('Unexpected error')); try { @@ -113,6 +97,15 @@ describe('SubscriptionEventService', function () { }); it('should call linkSubscription with correct arguments', async function () { + const subscription = { + items: { + data: [{price: {id: 'price_123'}}] + }, + customer: 'cust_123' + }; + + memberRepository.get.resolves({id: 'member_123'}); + await service.handleSubscriptionEvent(subscription); assert(memberRepository.linkSubscription.calledWith({id: 'member_123', subscription}));