mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-04-15 03:01:37 -05:00
🐛 Fixed missing subscription attribution on free to paid upgrade (#21846)
closes https://linear.app/ghost/issue/ENG-1561 - problem: When a free member upgraded to paid, we sometimes did not capture the subscription attribution data - cause: - after checkout, Stripe sends `customer.subscription.created`, `customer.subscription.updated` and `checkout.session.completed` webhook events - we want to create a subscription in our database based on the `checkout.session.completed` event, as it contains additional data (e.g. subscription attribution data) - but, we were sometimes creating a subscription based on a `customer.subscription.*` event during free → paid upgrades, which did not contain subscription attribution data - solution: we now ignore `customer.subscription.*` events until a member and its related subscription have been created in the database first, by the `checkout.session.completed` event
This commit is contained in:
parent
dd9d3a6f2e
commit
bd20ad3adb
4 changed files with 733 additions and 73 deletions
|
@ -48,6 +48,54 @@ Object {
|
|||
}
|
||||
`;
|
||||
|
||||
exports[`Members API Member attribution Creates a SubscriptionCreatedEvent with author attribution 3: [body] 1`] = `
|
||||
Object {
|
||||
"members": Array [
|
||||
Object {
|
||||
"attribution": Any<Object>,
|
||||
"avatar_image": null,
|
||||
"comped": false,
|
||||
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
|
||||
"email": Any<String>,
|
||||
"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": null,
|
||||
"newsletters": Any<Array>,
|
||||
"note": null,
|
||||
"status": "paid",
|
||||
"subscribed": false,
|
||||
"subscriptions": Any<Array>,
|
||||
"tiers": Any<Array>,
|
||||
"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 [
|
||||
|
@ -96,6 +144,54 @@ Object {
|
|||
}
|
||||
`;
|
||||
|
||||
exports[`Members API Member attribution Creates a SubscriptionCreatedEvent with deleted post attribution 3: [body] 1`] = `
|
||||
Object {
|
||||
"members": Array [
|
||||
Object {
|
||||
"attribution": Any<Object>,
|
||||
"avatar_image": null,
|
||||
"comped": false,
|
||||
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
|
||||
"email": Any<String>,
|
||||
"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": null,
|
||||
"newsletters": Any<Array>,
|
||||
"note": null,
|
||||
"status": "paid",
|
||||
"subscribed": false,
|
||||
"subscriptions": Any<Array>,
|
||||
"tiers": Any<Array>,
|
||||
"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 [
|
||||
|
@ -144,6 +240,54 @@ Object {
|
|||
}
|
||||
`;
|
||||
|
||||
exports[`Members API Member attribution Creates a SubscriptionCreatedEvent with empty attribution object 3: [body] 1`] = `
|
||||
Object {
|
||||
"members": Array [
|
||||
Object {
|
||||
"attribution": Any<Object>,
|
||||
"avatar_image": null,
|
||||
"comped": false,
|
||||
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
|
||||
"email": Any<String>,
|
||||
"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": null,
|
||||
"newsletters": Any<Array>,
|
||||
"note": null,
|
||||
"status": "paid",
|
||||
"subscribed": false,
|
||||
"subscriptions": Any<Array>,
|
||||
"tiers": Any<Array>,
|
||||
"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 [
|
||||
|
@ -192,6 +336,54 @@ Object {
|
|||
}
|
||||
`;
|
||||
|
||||
exports[`Members API Member attribution Creates a SubscriptionCreatedEvent with page attribution 3: [body] 1`] = `
|
||||
Object {
|
||||
"members": Array [
|
||||
Object {
|
||||
"attribution": Any<Object>,
|
||||
"avatar_image": null,
|
||||
"comped": false,
|
||||
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
|
||||
"email": Any<String>,
|
||||
"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": null,
|
||||
"newsletters": Any<Array>,
|
||||
"note": null,
|
||||
"status": "paid",
|
||||
"subscribed": false,
|
||||
"subscriptions": Any<Array>,
|
||||
"tiers": Any<Array>,
|
||||
"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 [
|
||||
|
@ -240,6 +432,54 @@ Object {
|
|||
}
|
||||
`;
|
||||
|
||||
exports[`Members API Member attribution Creates a SubscriptionCreatedEvent with post attribution 3: [body] 1`] = `
|
||||
Object {
|
||||
"members": Array [
|
||||
Object {
|
||||
"attribution": Any<Object>,
|
||||
"avatar_image": null,
|
||||
"comped": false,
|
||||
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
|
||||
"email": Any<String>,
|
||||
"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": null,
|
||||
"newsletters": Any<Array>,
|
||||
"note": null,
|
||||
"status": "paid",
|
||||
"subscribed": false,
|
||||
"subscriptions": Any<Array>,
|
||||
"tiers": Any<Array>,
|
||||
"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 [
|
||||
|
@ -288,6 +528,54 @@ Object {
|
|||
}
|
||||
`;
|
||||
|
||||
exports[`Members API Member attribution Creates a SubscriptionCreatedEvent with tag attribution 3: [body] 1`] = `
|
||||
Object {
|
||||
"members": Array [
|
||||
Object {
|
||||
"attribution": Any<Object>,
|
||||
"avatar_image": null,
|
||||
"comped": false,
|
||||
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
|
||||
"email": Any<String>,
|
||||
"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": null,
|
||||
"newsletters": Any<Array>,
|
||||
"note": null,
|
||||
"status": "paid",
|
||||
"subscribed": false,
|
||||
"subscriptions": Any<Array>,
|
||||
"tiers": Any<Array>,
|
||||
"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 [
|
||||
|
@ -336,6 +624,54 @@ Object {
|
|||
}
|
||||
`;
|
||||
|
||||
exports[`Members API Member attribution Creates a SubscriptionCreatedEvent with url attribution 3: [body] 1`] = `
|
||||
Object {
|
||||
"members": Array [
|
||||
Object {
|
||||
"attribution": Any<Object>,
|
||||
"avatar_image": null,
|
||||
"comped": false,
|
||||
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
|
||||
"email": Any<String>,
|
||||
"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": null,
|
||||
"newsletters": Any<Array>,
|
||||
"note": null,
|
||||
"status": "paid",
|
||||
"subscribed": false,
|
||||
"subscriptions": Any<Array>,
|
||||
"tiers": Any<Array>,
|
||||
"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 [
|
||||
|
@ -384,6 +720,54 @@ Object {
|
|||
}
|
||||
`;
|
||||
|
||||
exports[`Members API Member attribution Creates a SubscriptionCreatedEvent without attribution 3: [body] 1`] = `
|
||||
Object {
|
||||
"members": Array [
|
||||
Object {
|
||||
"attribution": Any<Object>,
|
||||
"avatar_image": null,
|
||||
"comped": false,
|
||||
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
|
||||
"email": Any<String>,
|
||||
"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": null,
|
||||
"newsletters": Any<Array>,
|
||||
"note": null,
|
||||
"status": "paid",
|
||||
"subscribed": false,
|
||||
"subscriptions": Any<Array>,
|
||||
"tiers": Any<Array>,
|
||||
"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 [
|
||||
|
@ -419,25 +803,70 @@ Object {
|
|||
"data": Any<Object>,
|
||||
"type": "subscription_event",
|
||||
},
|
||||
Object {
|
||||
"data": Any<Object>,
|
||||
"type": "subscription_event",
|
||||
},
|
||||
Object {
|
||||
"data": Any<Object>,
|
||||
"type": "subscription_event",
|
||||
},
|
||||
Object {
|
||||
"data": Any<Object>,
|
||||
"type": "subscription_event",
|
||||
},
|
||||
Object {
|
||||
"data": Any<Object>,
|
||||
"type": "subscription_event",
|
||||
},
|
||||
Object {
|
||||
"data": Any<Object>,
|
||||
"type": "subscription_event",
|
||||
},
|
||||
Object {
|
||||
"data": Any<Object>,
|
||||
"type": "subscription_event",
|
||||
},
|
||||
Object {
|
||||
"data": Any<Object>,
|
||||
"type": "subscription_event",
|
||||
},
|
||||
Object {
|
||||
"data": Any<Object>,
|
||||
"type": "subscription_event",
|
||||
},
|
||||
],
|
||||
"meta": Object {
|
||||
"pagination": Object {
|
||||
"limit": 10,
|
||||
"limit": "100",
|
||||
"next": null,
|
||||
"page": null,
|
||||
"pages": 1,
|
||||
"prev": null,
|
||||
"total": 8,
|
||||
"total": 16,
|
||||
},
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
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": "5664",
|
||||
"content-length": "11237",
|
||||
"content-type": "application/json; charset=utf-8",
|
||||
"content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/,
|
||||
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
|
||||
|
|
|
@ -1855,7 +1855,7 @@ describe('Members API', function () {
|
|||
});
|
||||
});
|
||||
|
||||
async function testWithAttribution(attribution, attributionResource) {
|
||||
async function testAttributionOnSignup(attribution, attributionResource) {
|
||||
const customer_id = createStripeID('cust');
|
||||
const subscription_id = createStripeID('sub');
|
||||
|
||||
|
@ -1900,7 +1900,31 @@ describe('Members API', function () {
|
|||
}
|
||||
});
|
||||
|
||||
let webhookPayload = JSON.stringify({
|
||||
// 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({
|
||||
type: 'checkout.session.completed',
|
||||
data: {
|
||||
object: {
|
||||
|
@ -1916,15 +1940,15 @@ describe('Members API', function () {
|
|||
}
|
||||
});
|
||||
|
||||
let webhookSignature = stripe.webhooks.generateTestHeaderString({
|
||||
payload: webhookPayload,
|
||||
const checkoutWebhookSignature = stripe.webhooks.generateTestHeaderString({
|
||||
payload: checkoutWebhookPayload,
|
||||
secret: process.env.WEBHOOK_SECRET
|
||||
});
|
||||
|
||||
await membersAgent.post('/webhooks/stripe/')
|
||||
.body(webhookPayload)
|
||||
.body(checkoutWebhookPayload)
|
||||
.header('content-type', 'application/json')
|
||||
.header('stripe-signature', webhookSignature)
|
||||
.header('stripe-signature', checkoutWebhookSignature)
|
||||
.expectStatus(200);
|
||||
|
||||
const {body} = await adminAgent.get(`/members/?search=${customer_id}@email.com`);
|
||||
|
@ -1993,6 +2017,161 @@ 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 () {
|
||||
|
@ -2005,7 +2184,7 @@ describe('Members API', function () {
|
|||
|
||||
const absoluteUrl = urlUtils.createUrl('/', true);
|
||||
|
||||
await testWithAttribution(attribution, {
|
||||
const attributionResource = {
|
||||
id: null,
|
||||
url: absoluteUrl,
|
||||
type: 'url',
|
||||
|
@ -2013,7 +2192,10 @@ 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 () {
|
||||
|
@ -2028,7 +2210,7 @@ describe('Members API', function () {
|
|||
|
||||
const absoluteUrl = urlService.getUrlByResourceId(post.id, {absolute: true});
|
||||
|
||||
await testWithAttribution(attribution, {
|
||||
const attributionResource = {
|
||||
id: post.id,
|
||||
url: absoluteUrl,
|
||||
type: 'post',
|
||||
|
@ -2036,7 +2218,10 @@ 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 () {
|
||||
|
@ -2048,7 +2233,7 @@ describe('Members API', function () {
|
|||
|
||||
const absoluteUrl = urlUtils.createUrl('/removed-blog-post/', true);
|
||||
|
||||
await testWithAttribution(attribution, {
|
||||
const attributionResource = {
|
||||
id: null,
|
||||
url: absoluteUrl,
|
||||
type: 'url',
|
||||
|
@ -2056,7 +2241,10 @@ 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 () {
|
||||
|
@ -2071,7 +2259,7 @@ describe('Members API', function () {
|
|||
|
||||
const absoluteUrl = urlService.getUrlByResourceId(post.id, {absolute: true});
|
||||
|
||||
await testWithAttribution(attribution, {
|
||||
const attributionResource = {
|
||||
id: post.id,
|
||||
url: absoluteUrl,
|
||||
type: 'page',
|
||||
|
@ -2079,7 +2267,10 @@ 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 () {
|
||||
|
@ -2094,7 +2285,7 @@ describe('Members API', function () {
|
|||
|
||||
const absoluteUrl = urlService.getUrlByResourceId(tag.id, {absolute: true});
|
||||
|
||||
await testWithAttribution(attribution, {
|
||||
const attributionResource = {
|
||||
id: tag.id,
|
||||
url: absoluteUrl,
|
||||
type: 'tag',
|
||||
|
@ -2102,7 +2293,10 @@ 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 () {
|
||||
|
@ -2117,7 +2311,7 @@ describe('Members API', function () {
|
|||
|
||||
const absoluteUrl = urlService.getUrlByResourceId(author.id, {absolute: true});
|
||||
|
||||
await testWithAttribution(attribution, {
|
||||
const attributionResource = {
|
||||
id: author.id,
|
||||
url: absoluteUrl,
|
||||
type: 'author',
|
||||
|
@ -2125,12 +2319,16 @@ 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;
|
||||
await testWithAttribution(attribution, {
|
||||
|
||||
const attributionResource = {
|
||||
id: null,
|
||||
url: null,
|
||||
type: null,
|
||||
|
@ -2138,13 +2336,17 @@ 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 = {};
|
||||
await testWithAttribution(attribution, {
|
||||
|
||||
const attributionResource = {
|
||||
id: null,
|
||||
url: null,
|
||||
type: null,
|
||||
|
@ -2152,7 +2354,10 @@ describe('Members API', function () {
|
|||
referrer_source: null,
|
||||
referrer_medium: null,
|
||||
referrer_url: null
|
||||
});
|
||||
};
|
||||
|
||||
await testAttributionOnSignup(attribution, attributionResource);
|
||||
await testAttributionOnUpgrade(attribution, attributionResource);
|
||||
});
|
||||
|
||||
// Activity feed
|
||||
|
@ -2160,7 +2365,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`)
|
||||
.get(`/members/events/?filter=type:subscription_event&limit=100`)
|
||||
.expectStatus(200)
|
||||
.matchHeaderSnapshot({
|
||||
'content-version': anyContentVersion,
|
||||
|
@ -2168,7 +2373,8 @@ describe('Members API', function () {
|
|||
})
|
||||
.matchBodySnapshot({
|
||||
events: new Array(subscriptionAttributions.length).fill({
|
||||
data: anyObject
|
||||
data: anyObject,
|
||||
type: 'subscription_event'
|
||||
})
|
||||
})
|
||||
.expect(({body}) => {
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
const errors = require('@tryghost/errors');
|
||||
const _ = require('lodash');
|
||||
const logging = require('@tryghost/logging');
|
||||
module.exports = class SubscriptionEventService {
|
||||
constructor(deps) {
|
||||
this.deps = deps;
|
||||
|
@ -18,18 +19,35 @@ module.exports = class SubscriptionEventService {
|
|||
customer_id: subscription.customer
|
||||
});
|
||||
|
||||
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});
|
||||
// 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;
|
||||
}
|
||||
throw new errors.ConflictError({err});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
@ -6,15 +6,55 @@ const SubscriptionEventService = require('../../../../../lib/services/webhook/Su
|
|||
describe('SubscriptionEventService', function () {
|
||||
let service;
|
||||
let memberRepository;
|
||||
let member;
|
||||
let subscription;
|
||||
|
||||
beforeEach(function () {
|
||||
memberRepository = {get: sinon.stub(), linkSubscription: sinon.stub()};
|
||||
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'
|
||||
};
|
||||
|
||||
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 () {
|
||||
const subscription = {
|
||||
subscription = {
|
||||
items: {
|
||||
data: []
|
||||
}
|
||||
|
@ -29,14 +69,6 @@ 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 {
|
||||
|
@ -48,14 +80,6 @@ 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 {
|
||||
|
@ -67,14 +91,6 @@ 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 {
|
||||
|
@ -97,15 +113,6 @@ 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}));
|
||||
|
|
Loading…
Add table
Reference in a new issue