mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-04-08 02:52:39 -05:00
🎨 Added staff notification when a sub is canceled due to failed payments (#20534)
ref https://linear.app/tryghost/issue/ENG-1254 - when a subscription is canceled automatically by Stripe (e.g. due to multiple failed payments), we now send a staff notification - logic before: if a member cancels a sub in Portal, then send a staff notification - logic now: if a subscription was active, but is now set to cancel immediately or at the end of the billing period, then send a staff notification. - with that logic change, we now send a cancellation staff notification when: 1. A member cancels their sub in Portal (existing) 2. A staff member cancels a member sub in Stripe (new) 3. A staff member cancels a member sub in Admin (new) 4. A sub is canceled automatically by Stripe because of multiple failed payments (new) - the copy of the staff notification email has also been updated to take into account 1) manual vs automatic cancellations, and 2) immediate vs end of billing period cancellations
This commit is contained in:
parent
6108df5292
commit
e476eebd2d
17 changed files with 209 additions and 136 deletions
|
@ -207,7 +207,7 @@ exports[`Collections API Automatic Collection Filtering Creates an automatic Col
|
|||
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": "12653",
|
||||
"content-length": "12649",
|
||||
"content-type": "application/json; charset=utf-8",
|
||||
"content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/,
|
||||
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
|
||||
|
@ -728,7 +728,7 @@ exports[`Collections API Automatic Collection Filtering Creates an automatic Col
|
|||
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": "78564",
|
||||
"content-length": "78560",
|
||||
"content-type": "application/json; charset=utf-8",
|
||||
"content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/,
|
||||
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
|
||||
|
@ -987,7 +987,7 @@ exports[`Collections API Automatic Collection Filtering Creates an automatic Col
|
|||
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": "14427",
|
||||
"content-length": "14423",
|
||||
"content-type": "application/json; charset=utf-8",
|
||||
"content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/,
|
||||
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
|
||||
|
@ -1246,7 +1246,7 @@ exports[`Collections API Automatic Collection Filtering Creates an automatic Col
|
|||
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": "14427",
|
||||
"content-length": "14423",
|
||||
"content-type": "application/json; charset=utf-8",
|
||||
"content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/,
|
||||
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
|
||||
|
|
|
@ -381,7 +381,7 @@ exports[`Members API - member attribution Returns sign up attributions of all ty
|
|||
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": "8054",
|
||||
"content-length": "8053",
|
||||
"content-type": "application/json; charset=utf-8",
|
||||
"content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/,
|
||||
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
|
||||
|
|
|
@ -647,7 +647,7 @@ exports[`Pages API Convert can convert a mobiledoc page to lexical 2: [headers]
|
|||
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": "4065",
|
||||
"content-length": "4063",
|
||||
"content-type": "application/json; charset=utf-8",
|
||||
"content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/,
|
||||
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
|
||||
|
@ -753,7 +753,7 @@ exports[`Pages API Convert can convert a mobiledoc page to lexical 4: [headers]
|
|||
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": "4065",
|
||||
"content-length": "4063",
|
||||
"content-type": "application/json; charset=utf-8",
|
||||
"content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/,
|
||||
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
|
||||
|
@ -947,7 +947,7 @@ exports[`Pages API Copy Can copy a page 3: [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": "3859",
|
||||
"content-length": "3857",
|
||||
"content-type": "application/json; charset=utf-8",
|
||||
"content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/,
|
||||
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
|
||||
|
@ -1053,7 +1053,7 @@ exports[`Pages API Create Can create a page with html 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": "4089",
|
||||
"content-length": "4087",
|
||||
"content-type": "application/json; charset=utf-8",
|
||||
"content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/,
|
||||
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
|
||||
|
@ -1333,7 +1333,7 @@ exports[`Pages API Update Access Visibility is set to tiers Saves only paid tier
|
|||
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": "3494",
|
||||
"content-length": "3492",
|
||||
"content-type": "application/json; charset=utf-8",
|
||||
"content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/,
|
||||
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
|
||||
|
@ -1438,7 +1438,7 @@ exports[`Pages API Update Can modify show_title_and_feature_image property 2: [h
|
|||
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": "3860",
|
||||
"content-length": "3858",
|
||||
"content-type": "application/json; charset=utf-8",
|
||||
"content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/,
|
||||
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
|
||||
|
|
|
@ -197,7 +197,7 @@ exports[`Posts API Can browse 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": "10736",
|
||||
"content-length": "10732",
|
||||
"content-type": "application/json; charset=utf-8",
|
||||
"content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/,
|
||||
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
|
||||
|
@ -421,7 +421,7 @@ exports[`Posts API Can browse filtering by a collection 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": "11731",
|
||||
"content-length": "11727",
|
||||
"content-type": "application/json; charset=utf-8",
|
||||
"content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/,
|
||||
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
|
||||
|
@ -766,7 +766,7 @@ exports[`Posts API Can browse with formats 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": "13534",
|
||||
"content-length": "13530",
|
||||
"content-type": "application/json; charset=utf-8",
|
||||
"content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/,
|
||||
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
|
||||
|
@ -874,7 +874,7 @@ exports[`Posts API Convert can convert a mobiledoc post to lexical 2: [headers]
|
|||
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": "4102",
|
||||
"content-length": "4100",
|
||||
"content-type": "application/json; charset=utf-8",
|
||||
"content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/,
|
||||
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
|
||||
|
@ -983,7 +983,7 @@ exports[`Posts API Convert can convert a mobiledoc post to lexical 4: [headers]
|
|||
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": "4102",
|
||||
"content-length": "4100",
|
||||
"content-type": "application/json; charset=utf-8",
|
||||
"content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/,
|
||||
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
|
||||
|
@ -1089,7 +1089,7 @@ exports[`Posts API Copy Can copy a post 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": "3894",
|
||||
"content-length": "3892",
|
||||
"content-type": "application/json; charset=utf-8",
|
||||
"content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/,
|
||||
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
|
||||
|
@ -1198,7 +1198,7 @@ exports[`Posts API Create Can create a post with html 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": "4124",
|
||||
"content-length": "4122",
|
||||
"content-type": "application/json; charset=utf-8",
|
||||
"content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/,
|
||||
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
|
||||
|
@ -1307,7 +1307,7 @@ exports[`Posts API Create Can create a post with lexical 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": "4136",
|
||||
"content-length": "4134",
|
||||
"content-type": "application/json; charset=utf-8",
|
||||
"content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/,
|
||||
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
|
||||
|
@ -1416,7 +1416,7 @@ exports[`Posts API Create Can create a post with mobiledoc 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": "3952",
|
||||
"content-length": "3950",
|
||||
"content-type": "application/json; charset=utf-8",
|
||||
"content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/,
|
||||
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
|
||||
|
@ -1703,7 +1703,7 @@ exports[`Posts API Delete Can delete posts belonging to a collection and returns
|
|||
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": "11731",
|
||||
"content-length": "11727",
|
||||
"content-type": "application/json; charset=utf-8",
|
||||
"content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/,
|
||||
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
|
||||
|
@ -2054,7 +2054,7 @@ exports[`Posts API Update Access Visibility is set to tiers Saves only paid tier
|
|||
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": "3527",
|
||||
"content-length": "3525",
|
||||
"content-type": "application/json; charset=utf-8",
|
||||
"content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/,
|
||||
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
|
||||
|
@ -2161,7 +2161,7 @@ exports[`Posts API Update Can add and remove collections 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": "3908",
|
||||
"content-length": "3906",
|
||||
"content-type": "application/json; charset=utf-8",
|
||||
"content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/,
|
||||
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
|
||||
|
@ -2384,7 +2384,7 @@ exports[`Posts API Update Can add and remove collections 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": "5504",
|
||||
"content-length": "5502",
|
||||
"content-type": "application/json; charset=utf-8",
|
||||
"content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/,
|
||||
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
|
||||
|
@ -2607,7 +2607,7 @@ exports[`Posts API Update Can add and remove collections 6: [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": "5498",
|
||||
"content-length": "5496",
|
||||
"content-type": "application/json; charset=utf-8",
|
||||
"content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/,
|
||||
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
|
||||
|
@ -2716,7 +2716,7 @@ exports[`Posts API Update Can update a post with lexical 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": "4087",
|
||||
"content-length": "4085",
|
||||
"content-type": "application/json; charset=utf-8",
|
||||
"content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/,
|
||||
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
|
||||
|
@ -2825,7 +2825,7 @@ exports[`Posts API Update Can update a post with lexical 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": "4084",
|
||||
"content-length": "4082",
|
||||
"content-type": "application/json; charset=utf-8",
|
||||
"content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/,
|
||||
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
|
||||
|
@ -2934,7 +2934,7 @@ exports[`Posts API Update Can update a post with mobiledoc 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": "3897",
|
||||
"content-length": "3895",
|
||||
"content-type": "application/json; charset=utf-8",
|
||||
"content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/,
|
||||
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
|
||||
|
@ -3043,7 +3043,7 @@ exports[`Posts API Update Can update a post with mobiledoc 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": "3894",
|
||||
"content-length": "3892",
|
||||
"content-type": "application/json; charset=utf-8",
|
||||
"content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/,
|
||||
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
|
||||
|
|
|
@ -48,7 +48,7 @@ Object {
|
|||
"meta_title": null,
|
||||
"milestone_notifications": true,
|
||||
"name": "Joe Bloggs",
|
||||
"paid_subscription_canceled_notification": false,
|
||||
"paid_subscription_canceled_notification": true,
|
||||
"paid_subscription_started_notification": true,
|
||||
"profile_image": "https://example.com/super_photo.jpg",
|
||||
"recommendation_notifications": true,
|
||||
|
@ -67,7 +67,7 @@ exports[`Sessions API can read session now the owner is logged in 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": "803",
|
||||
"content-length": "802",
|
||||
"content-type": "application/json; charset=utf-8",
|
||||
"content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/,
|
||||
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
|
||||
|
|
|
@ -289,7 +289,7 @@ describe('Members API', function () {
|
|||
// Set the subscription to cancel at the end of the period
|
||||
set(subscription, {
|
||||
...subscription,
|
||||
status: 'active',
|
||||
canceled_at: Date.now() / 1000,
|
||||
cancel_at_period_end: true,
|
||||
metadata: {
|
||||
cancellation_reason: 'I want to break free'
|
||||
|
@ -353,7 +353,18 @@ describe('Members API', function () {
|
|||
]
|
||||
});
|
||||
|
||||
canceledPaidMember = updatedMember;
|
||||
// Check that the staff notifications has been sent
|
||||
await DomainEvents.allSettled();
|
||||
|
||||
mockManager.assert.sentEmail({
|
||||
subject: /Paid subscription started: Cancel me at the end of the billing cycle/,
|
||||
to: 'jbloggs@example.com'
|
||||
});
|
||||
|
||||
mockManager.assert.sentEmail({
|
||||
subject: /Cancellation: Cancel me at the end of the billing cycle/,
|
||||
to: 'jbloggs@example.com'
|
||||
});
|
||||
});
|
||||
|
||||
it('Handles immediate cancellation of paid subscriptions', async function () {
|
||||
|
@ -425,6 +436,7 @@ describe('Members API', function () {
|
|||
set(subscription, {
|
||||
...subscription,
|
||||
status: 'canceled',
|
||||
canceled_at: Date.now() / 1000,
|
||||
cancellation_details: {
|
||||
reason: 'payment_failed'
|
||||
}
|
||||
|
@ -507,6 +519,19 @@ describe('Members API', function () {
|
|||
]
|
||||
});
|
||||
|
||||
// Check that the staff notifications has been sent
|
||||
await DomainEvents.allSettled();
|
||||
|
||||
mockManager.assert.sentEmail({
|
||||
subject: /Paid subscription started: Cancel me now/,
|
||||
to: 'jbloggs@example.com'
|
||||
});
|
||||
|
||||
mockManager.assert.sentEmail({
|
||||
subject: /Cancellation: Cancel me now/,
|
||||
to: 'jbloggs@example.com'
|
||||
});
|
||||
|
||||
canceledPaidMember = updatedMember;
|
||||
});
|
||||
|
||||
|
|
|
@ -33,7 +33,7 @@ Object {
|
|||
"meta_title": null,
|
||||
"milestone_notifications": true,
|
||||
"name": "Joe Bloggs",
|
||||
"paid_subscription_canceled_notification": false,
|
||||
"paid_subscription_canceled_notification": true,
|
||||
"paid_subscription_started_notification": true,
|
||||
"profile_image": "https://example.com/super_photo.jpg",
|
||||
"recommendation_notifications": true,
|
||||
|
@ -86,7 +86,7 @@ Object {
|
|||
"meta_title": null,
|
||||
"milestone_notifications": true,
|
||||
"name": "Joe Bloggs",
|
||||
"paid_subscription_canceled_notification": false,
|
||||
"paid_subscription_canceled_notification": true,
|
||||
"paid_subscription_started_notification": true,
|
||||
"profile_image": "https://example.com/super_photo.jpg",
|
||||
"recommendation_notifications": true,
|
||||
|
@ -190,7 +190,7 @@ Object {
|
|||
"meta_title": null,
|
||||
"milestone_notifications": true,
|
||||
"name": "Joe Bloggs",
|
||||
"paid_subscription_canceled_notification": false,
|
||||
"paid_subscription_canceled_notification": true,
|
||||
"paid_subscription_started_notification": true,
|
||||
"profile_image": "https://example.com/super_photo.jpg",
|
||||
"recommendation_notifications": true,
|
||||
|
@ -270,7 +270,7 @@ Object {
|
|||
"meta_title": null,
|
||||
"milestone_notifications": true,
|
||||
"name": "Joe Bloggs",
|
||||
"paid_subscription_canceled_notification": false,
|
||||
"paid_subscription_canceled_notification": true,
|
||||
"paid_subscription_started_notification": true,
|
||||
"profile_image": "https://example.com/super_photo.jpg",
|
||||
"recommendation_notifications": true,
|
||||
|
@ -338,7 +338,7 @@ Object {
|
|||
"meta_title": null,
|
||||
"milestone_notifications": true,
|
||||
"name": "Joe Bloggs",
|
||||
"paid_subscription_canceled_notification": false,
|
||||
"paid_subscription_canceled_notification": true,
|
||||
"paid_subscription_started_notification": true,
|
||||
"profile_image": "https://example.com/super_photo.jpg",
|
||||
"recommendation_notifications": true,
|
||||
|
@ -492,7 +492,7 @@ Object {
|
|||
"meta_title": null,
|
||||
"milestone_notifications": true,
|
||||
"name": "Joe Bloggs",
|
||||
"paid_subscription_canceled_notification": false,
|
||||
"paid_subscription_canceled_notification": true,
|
||||
"paid_subscription_started_notification": true,
|
||||
"profile_image": "https://example.com/super_photo.jpg",
|
||||
"recommendation_notifications": true,
|
||||
|
@ -560,7 +560,7 @@ Object {
|
|||
"meta_title": null,
|
||||
"milestone_notifications": true,
|
||||
"name": "Joe Bloggs",
|
||||
"paid_subscription_canceled_notification": false,
|
||||
"paid_subscription_canceled_notification": true,
|
||||
"paid_subscription_started_notification": true,
|
||||
"profile_image": "https://example.com/super_photo.jpg",
|
||||
"recommendation_notifications": true,
|
||||
|
@ -714,7 +714,7 @@ Object {
|
|||
"meta_title": null,
|
||||
"milestone_notifications": true,
|
||||
"name": "Joe Bloggs",
|
||||
"paid_subscription_canceled_notification": false,
|
||||
"paid_subscription_canceled_notification": true,
|
||||
"paid_subscription_started_notification": true,
|
||||
"profile_image": "https://example.com/super_photo.jpg",
|
||||
"recommendation_notifications": true,
|
||||
|
@ -782,7 +782,7 @@ Object {
|
|||
"meta_title": null,
|
||||
"milestone_notifications": true,
|
||||
"name": "Joe Bloggs",
|
||||
"paid_subscription_canceled_notification": false,
|
||||
"paid_subscription_canceled_notification": true,
|
||||
"paid_subscription_started_notification": true,
|
||||
"profile_image": "https://example.com/super_photo.jpg",
|
||||
"recommendation_notifications": true,
|
||||
|
@ -935,7 +935,7 @@ Object {
|
|||
"meta_title": null,
|
||||
"milestone_notifications": true,
|
||||
"name": "Joe Bloggs",
|
||||
"paid_subscription_canceled_notification": false,
|
||||
"paid_subscription_canceled_notification": true,
|
||||
"paid_subscription_started_notification": true,
|
||||
"profile_image": "https://example.com/super_photo.jpg",
|
||||
"recommendation_notifications": true,
|
||||
|
@ -1003,7 +1003,7 @@ Object {
|
|||
"meta_title": null,
|
||||
"milestone_notifications": true,
|
||||
"name": "Joe Bloggs",
|
||||
"paid_subscription_canceled_notification": false,
|
||||
"paid_subscription_canceled_notification": true,
|
||||
"paid_subscription_started_notification": true,
|
||||
"profile_image": "https://example.com/super_photo.jpg",
|
||||
"recommendation_notifications": true,
|
||||
|
@ -1156,7 +1156,7 @@ Object {
|
|||
"meta_title": null,
|
||||
"milestone_notifications": true,
|
||||
"name": "Joe Bloggs",
|
||||
"paid_subscription_canceled_notification": false,
|
||||
"paid_subscription_canceled_notification": true,
|
||||
"paid_subscription_started_notification": true,
|
||||
"profile_image": "https://example.com/super_photo.jpg",
|
||||
"recommendation_notifications": true,
|
||||
|
@ -1224,7 +1224,7 @@ Object {
|
|||
"meta_title": null,
|
||||
"milestone_notifications": true,
|
||||
"name": "Joe Bloggs",
|
||||
"paid_subscription_canceled_notification": false,
|
||||
"paid_subscription_canceled_notification": true,
|
||||
"paid_subscription_started_notification": true,
|
||||
"profile_image": "https://example.com/super_photo.jpg",
|
||||
"recommendation_notifications": true,
|
||||
|
@ -1378,7 +1378,7 @@ Object {
|
|||
"meta_title": null,
|
||||
"milestone_notifications": true,
|
||||
"name": "Joe Bloggs",
|
||||
"paid_subscription_canceled_notification": false,
|
||||
"paid_subscription_canceled_notification": true,
|
||||
"paid_subscription_started_notification": true,
|
||||
"profile_image": "https://example.com/super_photo.jpg",
|
||||
"recommendation_notifications": true,
|
||||
|
@ -1446,7 +1446,7 @@ Object {
|
|||
"meta_title": null,
|
||||
"milestone_notifications": true,
|
||||
"name": "Joe Bloggs",
|
||||
"paid_subscription_canceled_notification": false,
|
||||
"paid_subscription_canceled_notification": true,
|
||||
"paid_subscription_started_notification": true,
|
||||
"profile_image": "https://example.com/super_photo.jpg",
|
||||
"recommendation_notifications": true,
|
||||
|
@ -1644,7 +1644,7 @@ Object {
|
|||
"meta_title": null,
|
||||
"milestone_notifications": true,
|
||||
"name": "Joe Bloggs",
|
||||
"paid_subscription_canceled_notification": false,
|
||||
"paid_subscription_canceled_notification": true,
|
||||
"paid_subscription_started_notification": true,
|
||||
"profile_image": "https://example.com/super_photo.jpg",
|
||||
"recommendation_notifications": true,
|
||||
|
@ -1712,7 +1712,7 @@ Object {
|
|||
"meta_title": null,
|
||||
"milestone_notifications": true,
|
||||
"name": "Joe Bloggs",
|
||||
"paid_subscription_canceled_notification": false,
|
||||
"paid_subscription_canceled_notification": true,
|
||||
"paid_subscription_started_notification": true,
|
||||
"profile_image": "https://example.com/super_photo.jpg",
|
||||
"recommendation_notifications": true,
|
||||
|
@ -1850,7 +1850,7 @@ Object {
|
|||
"meta_title": null,
|
||||
"milestone_notifications": true,
|
||||
"name": "Joe Bloggs",
|
||||
"paid_subscription_canceled_notification": false,
|
||||
"paid_subscription_canceled_notification": true,
|
||||
"paid_subscription_started_notification": true,
|
||||
"profile_image": "https://example.com/super_photo.jpg",
|
||||
"recommendation_notifications": true,
|
||||
|
@ -1918,7 +1918,7 @@ Object {
|
|||
"meta_title": null,
|
||||
"milestone_notifications": true,
|
||||
"name": "Joe Bloggs",
|
||||
"paid_subscription_canceled_notification": false,
|
||||
"paid_subscription_canceled_notification": true,
|
||||
"paid_subscription_started_notification": true,
|
||||
"profile_image": "https://example.com/super_photo.jpg",
|
||||
"recommendation_notifications": true,
|
||||
|
@ -2071,7 +2071,7 @@ Object {
|
|||
"meta_title": null,
|
||||
"milestone_notifications": true,
|
||||
"name": "Joe Bloggs",
|
||||
"paid_subscription_canceled_notification": false,
|
||||
"paid_subscription_canceled_notification": true,
|
||||
"paid_subscription_started_notification": true,
|
||||
"profile_image": "https://example.com/super_photo.jpg",
|
||||
"recommendation_notifications": true,
|
||||
|
@ -2139,7 +2139,7 @@ Object {
|
|||
"meta_title": null,
|
||||
"milestone_notifications": true,
|
||||
"name": "Joe Bloggs",
|
||||
"paid_subscription_canceled_notification": false,
|
||||
"paid_subscription_canceled_notification": true,
|
||||
"paid_subscription_started_notification": true,
|
||||
"profile_image": "https://example.com/super_photo.jpg",
|
||||
"recommendation_notifications": true,
|
||||
|
|
|
@ -33,7 +33,7 @@ Object {
|
|||
"meta_title": null,
|
||||
"milestone_notifications": true,
|
||||
"name": "Joe Bloggs",
|
||||
"paid_subscription_canceled_notification": false,
|
||||
"paid_subscription_canceled_notification": true,
|
||||
"paid_subscription_started_notification": true,
|
||||
"profile_image": "https://example.com/super_photo.jpg",
|
||||
"recommendation_notifications": true,
|
||||
|
@ -89,7 +89,7 @@ Object {
|
|||
"meta_title": null,
|
||||
"milestone_notifications": true,
|
||||
"name": "Joe Bloggs",
|
||||
"paid_subscription_canceled_notification": false,
|
||||
"paid_subscription_canceled_notification": true,
|
||||
"paid_subscription_started_notification": true,
|
||||
"profile_image": "https://example.com/super_photo.jpg",
|
||||
"recommendation_notifications": true,
|
||||
|
@ -192,7 +192,7 @@ Object {
|
|||
"meta_title": null,
|
||||
"milestone_notifications": true,
|
||||
"name": "Joe Bloggs",
|
||||
"paid_subscription_canceled_notification": false,
|
||||
"paid_subscription_canceled_notification": true,
|
||||
"paid_subscription_started_notification": true,
|
||||
"profile_image": "https://example.com/super_photo.jpg",
|
||||
"recommendation_notifications": true,
|
||||
|
@ -271,7 +271,7 @@ Object {
|
|||
"meta_title": null,
|
||||
"milestone_notifications": true,
|
||||
"name": "Joe Bloggs",
|
||||
"paid_subscription_canceled_notification": false,
|
||||
"paid_subscription_canceled_notification": true,
|
||||
"paid_subscription_started_notification": true,
|
||||
"profile_image": "https://example.com/super_photo.jpg",
|
||||
"recommendation_notifications": true,
|
||||
|
@ -341,7 +341,7 @@ Object {
|
|||
"meta_title": null,
|
||||
"milestone_notifications": true,
|
||||
"name": "Joe Bloggs",
|
||||
"paid_subscription_canceled_notification": false,
|
||||
"paid_subscription_canceled_notification": true,
|
||||
"paid_subscription_started_notification": true,
|
||||
"profile_image": "https://example.com/super_photo.jpg",
|
||||
"recommendation_notifications": true,
|
||||
|
@ -493,7 +493,7 @@ Object {
|
|||
"meta_title": null,
|
||||
"milestone_notifications": true,
|
||||
"name": "Joe Bloggs",
|
||||
"paid_subscription_canceled_notification": false,
|
||||
"paid_subscription_canceled_notification": true,
|
||||
"paid_subscription_started_notification": true,
|
||||
"profile_image": "https://example.com/super_photo.jpg",
|
||||
"recommendation_notifications": true,
|
||||
|
@ -584,7 +584,7 @@ Header Level 3
|
|||
"meta_title": null,
|
||||
"milestone_notifications": true,
|
||||
"name": "Joe Bloggs",
|
||||
"paid_subscription_canceled_notification": false,
|
||||
"paid_subscription_canceled_notification": true,
|
||||
"paid_subscription_started_notification": true,
|
||||
"profile_image": "https://example.com/super_photo.jpg",
|
||||
"recommendation_notifications": true,
|
||||
|
@ -738,7 +738,7 @@ Object {
|
|||
"meta_title": null,
|
||||
"milestone_notifications": true,
|
||||
"name": "Joe Bloggs",
|
||||
"paid_subscription_canceled_notification": false,
|
||||
"paid_subscription_canceled_notification": true,
|
||||
"paid_subscription_started_notification": true,
|
||||
"profile_image": "https://example.com/super_photo.jpg",
|
||||
"recommendation_notifications": true,
|
||||
|
@ -808,7 +808,7 @@ Object {
|
|||
"meta_title": null,
|
||||
"milestone_notifications": true,
|
||||
"name": "Joe Bloggs",
|
||||
"paid_subscription_canceled_notification": false,
|
||||
"paid_subscription_canceled_notification": true,
|
||||
"paid_subscription_started_notification": true,
|
||||
"profile_image": "https://example.com/super_photo.jpg",
|
||||
"recommendation_notifications": true,
|
||||
|
@ -960,7 +960,7 @@ Object {
|
|||
"meta_title": null,
|
||||
"milestone_notifications": true,
|
||||
"name": "Joe Bloggs",
|
||||
"paid_subscription_canceled_notification": false,
|
||||
"paid_subscription_canceled_notification": true,
|
||||
"paid_subscription_started_notification": true,
|
||||
"profile_image": "https://example.com/super_photo.jpg",
|
||||
"recommendation_notifications": true,
|
||||
|
@ -1030,7 +1030,7 @@ Object {
|
|||
"meta_title": null,
|
||||
"milestone_notifications": true,
|
||||
"name": "Joe Bloggs",
|
||||
"paid_subscription_canceled_notification": false,
|
||||
"paid_subscription_canceled_notification": true,
|
||||
"paid_subscription_started_notification": true,
|
||||
"profile_image": "https://example.com/super_photo.jpg",
|
||||
"recommendation_notifications": true,
|
||||
|
@ -1182,7 +1182,7 @@ Object {
|
|||
"meta_title": null,
|
||||
"milestone_notifications": true,
|
||||
"name": "Joe Bloggs",
|
||||
"paid_subscription_canceled_notification": false,
|
||||
"paid_subscription_canceled_notification": true,
|
||||
"paid_subscription_started_notification": true,
|
||||
"profile_image": "https://example.com/super_photo.jpg",
|
||||
"recommendation_notifications": true,
|
||||
|
@ -1252,7 +1252,7 @@ Object {
|
|||
"meta_title": null,
|
||||
"milestone_notifications": true,
|
||||
"name": "Joe Bloggs",
|
||||
"paid_subscription_canceled_notification": false,
|
||||
"paid_subscription_canceled_notification": true,
|
||||
"paid_subscription_started_notification": true,
|
||||
"profile_image": "https://example.com/super_photo.jpg",
|
||||
"recommendation_notifications": true,
|
||||
|
@ -1405,7 +1405,7 @@ Object {
|
|||
"meta_title": null,
|
||||
"milestone_notifications": true,
|
||||
"name": "Joe Bloggs",
|
||||
"paid_subscription_canceled_notification": false,
|
||||
"paid_subscription_canceled_notification": true,
|
||||
"paid_subscription_started_notification": true,
|
||||
"profile_image": "https://example.com/super_photo.jpg",
|
||||
"recommendation_notifications": true,
|
||||
|
@ -1496,7 +1496,7 @@ Header Level 3
|
|||
"meta_title": null,
|
||||
"milestone_notifications": true,
|
||||
"name": "Joe Bloggs",
|
||||
"paid_subscription_canceled_notification": false,
|
||||
"paid_subscription_canceled_notification": true,
|
||||
"paid_subscription_started_notification": true,
|
||||
"profile_image": "https://example.com/super_photo.jpg",
|
||||
"recommendation_notifications": true,
|
||||
|
@ -1694,7 +1694,7 @@ Object {
|
|||
"meta_title": null,
|
||||
"milestone_notifications": true,
|
||||
"name": "Joe Bloggs",
|
||||
"paid_subscription_canceled_notification": false,
|
||||
"paid_subscription_canceled_notification": true,
|
||||
"paid_subscription_started_notification": true,
|
||||
"profile_image": "https://example.com/super_photo.jpg",
|
||||
"recommendation_notifications": true,
|
||||
|
@ -1785,7 +1785,7 @@ Header Level 3
|
|||
"meta_title": null,
|
||||
"milestone_notifications": true,
|
||||
"name": "Joe Bloggs",
|
||||
"paid_subscription_canceled_notification": false,
|
||||
"paid_subscription_canceled_notification": true,
|
||||
"paid_subscription_started_notification": true,
|
||||
"profile_image": "https://example.com/super_photo.jpg",
|
||||
"recommendation_notifications": true,
|
||||
|
@ -1923,7 +1923,7 @@ Object {
|
|||
"meta_title": null,
|
||||
"milestone_notifications": true,
|
||||
"name": "Joe Bloggs",
|
||||
"paid_subscription_canceled_notification": false,
|
||||
"paid_subscription_canceled_notification": true,
|
||||
"paid_subscription_started_notification": true,
|
||||
"profile_image": "https://example.com/super_photo.jpg",
|
||||
"recommendation_notifications": true,
|
||||
|
@ -2014,7 +2014,7 @@ Header Level 3
|
|||
"meta_title": null,
|
||||
"milestone_notifications": true,
|
||||
"name": "Joe Bloggs",
|
||||
"paid_subscription_canceled_notification": false,
|
||||
"paid_subscription_canceled_notification": true,
|
||||
"paid_subscription_started_notification": true,
|
||||
"profile_image": "https://example.com/super_photo.jpg",
|
||||
"recommendation_notifications": true,
|
||||
|
@ -2167,7 +2167,7 @@ Object {
|
|||
"meta_title": null,
|
||||
"milestone_notifications": true,
|
||||
"name": "Joe Bloggs",
|
||||
"paid_subscription_canceled_notification": false,
|
||||
"paid_subscription_canceled_notification": true,
|
||||
"paid_subscription_started_notification": true,
|
||||
"profile_image": "https://example.com/super_photo.jpg",
|
||||
"recommendation_notifications": true,
|
||||
|
@ -2237,7 +2237,7 @@ Object {
|
|||
"meta_title": null,
|
||||
"milestone_notifications": true,
|
||||
"name": "Joe Bloggs",
|
||||
"paid_subscription_canceled_notification": false,
|
||||
"paid_subscription_canceled_notification": true,
|
||||
"paid_subscription_started_notification": true,
|
||||
"profile_image": "https://example.com/super_photo.jpg",
|
||||
"recommendation_notifications": true,
|
||||
|
|
|
@ -594,7 +594,7 @@ Object {
|
|||
"meta_title": null,
|
||||
"milestone_notifications": true,
|
||||
"name": "test user edit",
|
||||
"paid_subscription_canceled_notification": false,
|
||||
"paid_subscription_canceled_notification": true,
|
||||
"paid_subscription_started_notification": true,
|
||||
"profile_image": "https://example.com/super_photo.jpg",
|
||||
"recommendation_notifications": true,
|
||||
|
@ -614,7 +614,7 @@ exports[`Authentication API Blog setup update setup 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": "794",
|
||||
"content-length": "793",
|
||||
"content-type": "application/json; charset=utf-8",
|
||||
"content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/,
|
||||
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
|
||||
|
|
|
@ -133,7 +133,8 @@ DataGenerator.Content = {
|
|||
slug: 'joe-bloggs',
|
||||
email: 'jbloggs@example.com',
|
||||
password: 'Sl1m3rson99',
|
||||
profile_image: 'https://example.com/super_photo.jpg'
|
||||
profile_image: 'https://example.com/super_photo.jpg',
|
||||
paid_subscription_canceled_notification: true
|
||||
},
|
||||
{
|
||||
// admin
|
||||
|
|
|
@ -4,6 +4,9 @@
|
|||
* @prop {string} memberId
|
||||
* @prop {string} tierId
|
||||
* @prop {string} subscriptionId
|
||||
* @prop {boolean} cancelNow
|
||||
* @prop {Date} expiryAt
|
||||
* @prop {Date} canceledAt
|
||||
*/
|
||||
|
||||
module.exports = class SubscriptionCancelledEvent {
|
||||
|
|
|
@ -1089,7 +1089,7 @@ module.exports = class MemberRepository {
|
|||
const originalMrrDelta = model.get('mrr');
|
||||
const updatedMrrDelta = updated.get('mrr');
|
||||
|
||||
const getEventName = (originalStatus, updatedStatus) => {
|
||||
const getEventType = (originalStatus, updatedStatus) => {
|
||||
if (originalStatus === updatedStatus) {
|
||||
return 'updated';
|
||||
}
|
||||
|
@ -1103,12 +1103,14 @@ module.exports = class MemberRepository {
|
|||
|
||||
const originalStatus = getStatus(model);
|
||||
const updatedStatus = getStatus(updated);
|
||||
const eventType = getEventType(originalStatus, updatedStatus);
|
||||
|
||||
const mrrDelta = updatedMrrDelta - originalMrrDelta;
|
||||
|
||||
await this._MemberPaidSubscriptionEvent.add({
|
||||
member_id: member.id,
|
||||
source: 'stripe',
|
||||
type: getEventName(originalStatus, updatedStatus),
|
||||
type: eventType,
|
||||
subscription_id: updated.id,
|
||||
from_plan: model.get('plan_id'),
|
||||
to_plan: updated.get('status') === 'canceled' ? null : updated.get('plan_id'),
|
||||
|
@ -1133,6 +1135,29 @@ module.exports = class MemberRepository {
|
|||
});
|
||||
this.dispatchEvent(event, options);
|
||||
}
|
||||
|
||||
// Dispatch cancellation event, i.e. send paid cancellation staff notification, if:
|
||||
// 1. The subscription has been set to cancel at period end, by the member in Portal, status 'canceled'
|
||||
// 2. The subscription has been immediately canceled (e.g. due to multiple failed payments), status 'expired'
|
||||
if (this.isActiveSubscriptionStatus(originalStatus) && (updatedStatus === 'canceled' || updatedStatus === 'expired')) {
|
||||
const context = options?.context || {};
|
||||
const source = this._resolveContextSource(context);
|
||||
const cancelNow = updatedStatus === 'expired';
|
||||
const canceledAt = new Date(subscription.canceled_at * 1000);
|
||||
const expiryAt = cancelNow ? canceledAt : updated.get('current_period_end');
|
||||
|
||||
const event = SubscriptionCancelledEvent.create({
|
||||
source,
|
||||
tierId: ghostProduct?.get('id'),
|
||||
memberId: member.id,
|
||||
subscriptionId: updated.get('id'),
|
||||
cancelNow,
|
||||
canceledAt,
|
||||
expiryAt
|
||||
});
|
||||
|
||||
this.dispatchEvent(event, options);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
eventData.created_at = new Date(subscription.start_date * 1000);
|
||||
|
@ -1484,34 +1509,6 @@ module.exports = class MemberRepository {
|
|||
id: member.id,
|
||||
subscription: updatedSubscription
|
||||
}, options);
|
||||
|
||||
// Dispatch cancellation event
|
||||
if (data.subscription.cancel_at_period_end) {
|
||||
const stripeProductId = _.get(updatedSubscription, 'items.data[0].price.product');
|
||||
|
||||
let ghostProduct;
|
||||
try {
|
||||
ghostProduct = await this._productRepository.get(
|
||||
{stripe_product_id: stripeProductId},
|
||||
{...sharedOptions, forUpdate: true}
|
||||
);
|
||||
} catch (e) {
|
||||
ghostProduct = null;
|
||||
}
|
||||
|
||||
const context = options?.context || {};
|
||||
const source = this._resolveContextSource(context);
|
||||
const cancellationTimestamp = updatedSubscription.canceled_at
|
||||
? new Date(updatedSubscription.canceled_at * 1000)
|
||||
: new Date();
|
||||
const cancelEventData = {
|
||||
source,
|
||||
memberId: member.id,
|
||||
subscriptionId: subscriptionModel.get('id'),
|
||||
tierId: ghostProduct?.get('id')
|
||||
};
|
||||
this.dispatchEvent(SubscriptionCancelledEvent.create(cancelEventData, cancellationTimestamp), options);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -119,11 +119,11 @@ class StaffService {
|
|||
attribution
|
||||
});
|
||||
} else if (type === SubscriptionCancelledEvent) {
|
||||
subscription.canceledAt = event.timestamp;
|
||||
await this.emails.notifyPaidSubscriptionCanceled({
|
||||
member,
|
||||
tier,
|
||||
subscription
|
||||
subscription,
|
||||
...event.data
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -122,7 +122,7 @@ class StaffServiceEmails {
|
|||
}
|
||||
}
|
||||
|
||||
async notifyPaidSubscriptionCanceled({member, tier, subscription}, options = {}) {
|
||||
async notifyPaidSubscriptionCanceled({member, tier, subscription, cancelNow, expiryAt, canceledAt}, options = {}) {
|
||||
const users = await this.models.User.getEmailAlertUsers('paid-canceled', options);
|
||||
|
||||
for (const user of users) {
|
||||
|
@ -140,8 +140,9 @@ class StaffServiceEmails {
|
|||
};
|
||||
|
||||
const subscriptionData = {
|
||||
expiryAt: this.getFormattedDate(subscription.cancelAt),
|
||||
canceledAt: this.getFormattedDate(subscription.canceledAt),
|
||||
expiryAt: this.getFormattedDate(expiryAt),
|
||||
cancelNow: cancelNow,
|
||||
canceledAt: this.getFormattedDate(canceledAt),
|
||||
cancellationReason: subscription.cancellationReason || ''
|
||||
};
|
||||
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
{{#if subscriptionData.cancellationReason}}
|
||||
Reason: {{subscriptionData.cancellationReason}}
|
||||
{{else}}
|
||||
A paid member has just canceled their subscription.
|
||||
A paid member's subscription has just been canceled.
|
||||
{{/if}}
|
||||
{{/inline}}
|
||||
{{/preview}}
|
||||
|
@ -33,7 +33,7 @@
|
|||
<tr>
|
||||
<td style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 14px; vertical-align: top;">
|
||||
<p style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 20px; color: #15212A; font-weight: bold; line-height: 25px; margin: 0; margin-bottom: 15px;">Hey there,</p>
|
||||
<p style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 16px; color: #3A464C; font-weight: normal; margin: 0; line-height: 25px; margin-bottom: 32px;">A paid member has just <span style="font-weight: bold; color: #15212A;">canceled their subscription</span>.</p>
|
||||
<p style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 16px; color: #3A464C; font-weight: normal; margin: 0; line-height: 25px; margin-bottom: 32px;">A paid member's subscription has just been <span style="font-weight: bold; color: #15212A;">canceled</span>.</p>
|
||||
|
||||
<table width="100" border="0" cellpadding="0" cellspacing="0" class="btn btn-primary" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; table-layout: fixed; width: 100%; min-width: 100%; box-sizing: border-box; background: #F9F9FA; border-radius: 7px;">
|
||||
<tbody>
|
||||
|
@ -51,7 +51,9 @@
|
|||
{{#if memberData.showEmail}}
|
||||
<p class="text-link" style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 13px; padding-right: 8px; padding: 0; margin: 0; color: #394047; font-weight: 400;">{{memberData.email}}</p>
|
||||
{{/if}}
|
||||
<p style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 13px; padding-right: 8px; padding: 0; margin: 0; color: #95A1AD;">Canceled on {{subscriptionData.canceledAt}} </p>
|
||||
{{#unless subscriptionData.cancelNow}}
|
||||
<p style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 13px; padding-right: 8px; padding: 0; margin: 0; color: #95A1AD;">Canceled on {{subscriptionData.canceledAt}} </p>
|
||||
{{/unless}}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
@ -66,7 +68,11 @@
|
|||
</tr>
|
||||
<tr>
|
||||
<td style="vertical-align: top; padding-top: 0; padding-right: 16px; padding-bottom: 16px; padding-left: 16px;">
|
||||
{{#if subscriptionData.cancelNow}}
|
||||
<p style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 13px; padding-right: 8px; padding: 0; margin: 0; color: #95A1AD;">Subscription expired on</p>
|
||||
{{else}}
|
||||
<p style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 13px; padding-right: 8px; padding: 0; margin: 0; color: #95A1AD;">Subscription will expire on</p>
|
||||
{{/if}}
|
||||
<p style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; font-size: 16px; padding-right: 8px; padding: 0; margin: 0; color: #15171A; font-weight: 600;">{{subscriptionData.expiryAt}}</p>
|
||||
</td>
|
||||
</tr>
|
||||
|
|
|
@ -3,7 +3,7 @@ module.exports = function (data) {
|
|||
return `
|
||||
Hey there,
|
||||
|
||||
A paid member has just cancelled their subscription: "${data.memberData.name}"
|
||||
A paid member's subscription has just been canceled: "${data.memberData.name}"
|
||||
|
||||
---
|
||||
|
||||
|
|
|
@ -706,6 +706,9 @@ describe('StaffService', function () {
|
|||
let member;
|
||||
let tier;
|
||||
let subscription;
|
||||
let expiryAt;
|
||||
let canceledAt;
|
||||
let cancelNow;
|
||||
before(function () {
|
||||
member = {
|
||||
name: 'Ghost',
|
||||
|
@ -722,31 +725,34 @@ describe('StaffService', function () {
|
|||
subscription = {
|
||||
amount: 5000,
|
||||
currency: 'USD',
|
||||
interval: 'month',
|
||||
cancelAt: '2024-08-01T07:30:39.882Z',
|
||||
canceledAt: '2022-08-05T07:30:39.882Z'
|
||||
interval: 'month'
|
||||
};
|
||||
|
||||
expiryAt = '2024-09-05T07:30:39.882Z';
|
||||
canceledAt = '2022-08-05T07:30:39.882Z';
|
||||
cancelNow = false;
|
||||
});
|
||||
|
||||
it('sends paid subscription cancel alert', async function () {
|
||||
it('sends paid subscription cancel notification when sub is canceled at the end of billing period', async function () {
|
||||
await service.emails.notifyPaidSubscriptionCanceled({member, tier, subscription: {
|
||||
...subscription,
|
||||
cancellationReason: 'Changed my mind!'
|
||||
}}, options);
|
||||
}, expiryAt, canceledAt, cancelNow}, options);
|
||||
|
||||
mailStub.calledOnce.should.be.true();
|
||||
testCommonPaidSubCancelMailData(stubs);
|
||||
|
||||
mailStub.calledWith(
|
||||
sinon.match.has('html', sinon.match('Canceled on 5 Aug 2022'))
|
||||
).should.be.true();
|
||||
|
||||
// Expiration sentence is in the future tense
|
||||
mailStub.calledWith(
|
||||
sinon.match.has('html', sinon.match('Subscription will expire on'))
|
||||
).should.be.true();
|
||||
|
||||
mailStub.calledWith(
|
||||
sinon.match.has('html', sinon.match('Canceled on 5 Aug 2022'))
|
||||
).should.be.true();
|
||||
|
||||
mailStub.calledWith(
|
||||
sinon.match.has('html', sinon.match('1 Aug 2024'))
|
||||
sinon.match.has('html', sinon.match('5 Sep 2024'))
|
||||
).should.be.true();
|
||||
|
||||
mailStub.calledWith(
|
||||
|
@ -761,34 +767,68 @@ describe('StaffService', function () {
|
|||
).should.be.true();
|
||||
});
|
||||
|
||||
it('sends paid subscription cancel alert without reason', async function () {
|
||||
await service.emails.notifyPaidSubscriptionCanceled({member, tier, subscription}, options);
|
||||
it('sends paid subscription cancel alert when sub is canceled without reason', async function () {
|
||||
await service.emails.notifyPaidSubscriptionCanceled({member, tier, subscription, expiryAt, canceledAt, cancelNow}, options);
|
||||
|
||||
mailStub.calledOnce.should.be.true();
|
||||
testCommonPaidSubCancelMailData(stubs);
|
||||
|
||||
mailStub.calledWith(
|
||||
sinon.match.has('html', sinon.match('Canceled on 5 Aug 2022'))
|
||||
).should.be.true();
|
||||
|
||||
// Expiration sentence is in the future tense
|
||||
mailStub.calledWith(
|
||||
sinon.match.has('html', sinon.match('Subscription will expire on'))
|
||||
).should.be.true();
|
||||
|
||||
mailStub.calledWith(
|
||||
sinon.match.has('html', sinon.match('Canceled on 5 Aug 2022'))
|
||||
).should.be.true();
|
||||
|
||||
mailStub.calledWith(
|
||||
sinon.match.has('html', sinon.match('1 Aug 2024'))
|
||||
sinon.match.has('html', sinon.match('5 Sep 2024'))
|
||||
).should.be.true();
|
||||
|
||||
// Cancellation reason block is hidden
|
||||
mailStub.calledWith(
|
||||
sinon.match.has('html', sinon.match('Reason: '))
|
||||
).should.be.false();
|
||||
mailStub.calledWith(
|
||||
sinon.match.has('html', sinon.match('Cancellation reason'))
|
||||
).should.be.false();
|
||||
});
|
||||
|
||||
// check preview text
|
||||
it('sends paid subscription cancel alert when subscription is canceled immediately', async function () {
|
||||
cancelNow = true;
|
||||
await service.emails.notifyPaidSubscriptionCanceled({member, tier, subscription: {
|
||||
...subscription,
|
||||
cancellationReason: 'Payment failed'
|
||||
}, expiryAt, canceledAt, cancelNow}, options);
|
||||
|
||||
mailStub.calledOnce.should.be.true();
|
||||
testCommonPaidSubCancelMailData(stubs);
|
||||
|
||||
// We don't show "Canceled on" when subscription is canceled immediately
|
||||
mailStub.calledWith(
|
||||
sinon.match.has('html', sinon.match('A paid member has just canceled their subscription.'))
|
||||
sinon.match.has('html', sinon.match('Canceled on'))
|
||||
).should.be.false();
|
||||
|
||||
// Expiration sentence is in the past tense
|
||||
mailStub.calledWith(
|
||||
sinon.match.has('html', sinon.match('Subscription expired on'))
|
||||
).should.be.true();
|
||||
|
||||
mailStub.calledWith(
|
||||
sinon.match.has('html', sinon.match('5 Sep 2024'))
|
||||
).should.be.true();
|
||||
|
||||
mailStub.calledWith(
|
||||
sinon.match.has('html', 'Offer')
|
||||
).should.be.false();
|
||||
|
||||
mailStub.calledWith(
|
||||
sinon.match.has('html', sinon.match('Cancellation reason'))
|
||||
).should.be.true();
|
||||
|
||||
mailStub.calledWith(
|
||||
sinon.match.has('html', sinon.match('Reason: Payment failed'))
|
||||
).should.be.true();
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Add table
Reference in a new issue