refs https://github.com/TryGhost/Team/issues/616
Working with ProductRepository as a separate package was more trouble
than it was worth, so it's been moved into members-api. We expose the
product repository so that Ghost Admin API can access it.
refs https://github.com/TryGhost/Team/issues/586
On Ghost Boot, as part of configuring Stripe, this populates stripe products and prices for existing stripe customers in the newly created `stripe_prices` and `stripe_products` table, which allows us to map existing customers to default Ghost product and on current prices. The population script on boot is only run if we find -
- A Ghost Product
- No rows in `stripe_products`
- No rows in `stripe_prices`
- One or more rows in `members_stripe_customers_subscriptions`
refs https://github.com/TryGhost/Team/issues/593
- Bumps `stripe` node library major version to v8 - 8.142
- Bumps default Stripe version to latest - '2020-08-27'
- Updated webhook Stripe version to latest - '2020-08-27'
- Removes `@types/stripe` in favor of first-class types support in `stripe` lib directly
- Updates types across files
refs https://github.com/TryGhost/Team/issues/595
For a canceled subscription, the desired MRR delta is to reduce by negative of original amount, but our logic was incorrectly reducing it by double which led to big gap between real MRR and one shown on Dashboard.
- Fixes calculation for MRR change for canceled subscriptions
no-issue
The Admin API uses a Member id rather than email to update
subscriptions, this ensures that we provide an interface that will
continue to work with the Admin API
https://github.com/TryGhost/Team/issues/530
This option will check to see if a subscription is in an unpaid or past
due state, and if so, will cancel the subscription immediately, rather
than cancelling at the period end.
refs https://github.com/TryGhost/Team/issues/530
The RouterController was a grab bag of all controller methods, making it
difficult to mock & test. This adds a MemberController with a smaller
API - making it easier to test.
no-issue
When seeding the database with fake members & stripe data, it's possible
to create stripe plans without a nickname. Similarly some other services
do not have a nickname on their plans. This ensures that we do not error
when working with these plans.
no-issue
If we are to perform the `linkSubscription` method inside of a
transaction, the addition of the paid subscription events would happen
outside of the transaction, and cause errors. This ensures that we pass
the options object (containing the transaction) to the models calls to
add paid subscription events
no-issue
1. We do not want to store payment events for payments of 0 value
2. Stripe webhooks can arrive and be processed "out of order", which can
result in us attempting to add a payment event for a member which
does not yet exist. The change here will 404 in such (edge) cases, so
that Stripe will retry the webhook at a later point, when the Member
has been created, allowing us to store the payment event.
refs https://github.com/TryGhost/Team/issues/469
In order to reduce noise, we want to only display newsletter
subscription events which are not likely to be the result of a member
signup. The approach we've taken is to remove any newsletter
subscription (not unsubscription) event, if when sorted in chronological
order, it is to reside next to a signup event for the same member.
An improvement to this approach might be to add some kind of transaction
id to events which would allow us to group together events which should
be considered to have happened simultaneously.
refs https://github.com/TryGhost/Team/issues/469
Signup events are captured by status changes with no `from_status`, this
means that the member did not have a status (did not exist) before this
change.
refs https://github.com/TryGhost/Team/issues/469
We order the set of all events by created_at, but were not fetching the
individual events with the same order applies, this resulted in
incorrect results.
refs https://github.com/TryGhost/Ghost/issues/12711
We must wait for the stripeSubscriptions relation to be loaded before
attempting to loop through them. As well as this we should use `upsert`
so that we can edit a subscription record by `subscription_id`, rather
than the (internal) `id`
no refs
The member id assigned when creating a new status event on member creation was incorrectly using `data.id` instead of `member.id`, which was undefined causing a validation error.
refs https://github.com/TryGhost/Ghost/issues/12602
* Added Event Repository
** Added method for MRR over time
** Added method for newsletter subscriptions over time
** Added method for gross volume over time
** Added method for status segment size over time
* Captured login events
* Captured newsletter subscription/unsubscription
* Captured email address change events
* Captured paid subscription events
* Captured payment events
* Captured status events
refs https://github.com/TryGhost/Team/issues/479
* Fixed updating payment method for canceled subscriptions
Stripe considers canceled subscriptions as non-existent, so any attempts
to update them will fail with a 404 not found. Prior to this change we
were attempting to update *all* subscriptions for a customer, including
those which were canceled. This would cause an error and the loop to
break.
* 🐛 Fixed errors for members with multiple active customers
Members with multiple active customers would have their first active
customer found updated with the new payment method. We would then
iterate through *all* active subscription, and attempt to update their
payment method. If a subscription was not owned by the customer that was
just updated, it would error and cause the loop to break out.
* Added ability to update a specific subscription's payment method
In order to remove ambiguity we add the ability to update the payment
method for a specific subscription. This will remove room for errors as
we will not have to worry about if a subscription belong to the customer
or not.
refs https://github.com/TryGhost/Team/issues/475
The subscription object here is a database model rather than an
ISubscription from the stripe library, and we need to 1) use the `get`
method to read attributes and 2) read the `subscription_id` attribute,
as the `id` is our internal one.
no-issue
Fetching relations via the model returns a promise, and this was missing
the `await` keyword. We also need to `get` the subscription_id attribute
as we're working with models rather than subscription objects.
no-issue
If we receive webhooks out of order, e.g. a
`customer.subscription.updated` with a status of 'active', followed by a
`customer.subscription.created` with a status of 'incomplete'. We would
overwrite the correct value with data from the "older" webhook. This
ensures that we always fetch the latest data from the Stripe API before
storing in the database.
no-issue
related to ef9cb0862c
In the last patch which fixes the bug for not passing custom redirect urls when a checkout session is created, we missed updating the case where a logged in member tries to update the subscription.
no issue
The logic to fetch member for a checkout session was incorrectly creating a new customer instead of finding an existing one, so the member's billing details was not getting updated.
no-issue
This refactors the members-api module so that it is easier to test going forward,
as well as easier to understand & navigate. The Stripe API no longer contains
storage code, this is all handled via the member repository. And we have dedicated
services for webhooks, and stripe plans initialisation.
refs https://github.com/TryGhost/Ghost/issues/12256 , https://github.com/TryGhost/Ghost/issues/12255
Currently when listing subscriptions for Members, we were only showing the subscriptions which have a status of trialing or active.
Based on discussion, the `unpaid` and `past_due` states on Stripe also represent owner's intention of considering a subscription as active instead of `cancelled`, so we allow any subscriptions under these 2 states to be also listed for a member and consider them as `paid`.
- Subscriptions will go into a past_due state if the payment is missed, this should be considered a grace period where the member still has access.
- After this the subscriptions will either go to the unpaid or the cancelled state - this can be configured on an account by account basis in the Stripe dashboard. `unpaid` is considered as an intention to keep the subscription to allow for re-activation later.
no-issue
Subscription created events are required for migrating Stripe subscriptions from
alternative platforms, which involves creating a new subscription for a customer
(outside of Ghost) before cancelling the original subscription.
refs https://github.com/TryGhost/Ghost/issues/12150
- `destroy` method was using incorrect cancel subscriptions method - stripe.cancelStripeSubscriptions - which doesn't exist
- Fixes call with intended method - `stripe.cancelAllSubscriptions` - to cancel all subscriptions
refs TryGhost/Ghost#12127
- Adds new `updateSubscription` method to members-api which allows updating individual subscription for a member
- New method only allows toggling of cancellation at period end for a subscription at the moment
no-issue
Using models internally and in the exported API means that we avoid expensive
`toJSON` calls, which affects performance when looping through large lists of
members. It also allows us to take advantage of the new relations used in the
models.
The addition of "ByID" methods for linking stripe customers and setting
complimentary subscriptions allows bulk imports to avoid the overhead of creating
a model for each members, instead passing an id string. n.b. currently the impl
_does_ still create models, but it makes it easier to optimise and refactor in the
future.
no-issue
Previously we would blindly put subscriptions into the database when we
received a webhook, which could result in orphaned rows that were not
linked to a customer (and by extension a member)
This updates the logic so that we will only add subscriptions if we have
a record of their customer.
Customers are only added during a checkout.session.completed webhook, at
which point a member is guarunteed, but for formailty and safety against
changes in the flow, the logic has been applied to inserting customers
too.
refs https://github.com/TryGhost/Ghost/issues/11557
If a subscription failed to delete, we would error and bailout of the
process, this updates it to log the error so that site owners have a
record of the error in the logs, but also to continue through the rest
of the subscriptions.
no-issue
Destroy is terminology we usually use for the model layer and was a
little confusing without context, this method is used in one place so
it's a low effort cleanup with minimal repercussions
refs https://github.com/TryGhost/Ghost/issues/12074
Some sites may have had duplicate webhooks created due to a race
condition. This updates the members-api to cleanup _all_ webhooks before
starting, allowing it to create webhooks on a fresh slate, and removing
possible causes of 401 errors due to incorrect webhook secrets.
refs https://github.com/TryGhost/Ghost/issues/12065
This protects us against multiple instances of the members-api being
started simultaneously and race conditions where inbetween the initial
"GET" of a plan which returns empty, and the "POST" of a plan to create
it, another instance has already created it.
refs https://github.com/TryGhost/Ghost/issues/12061
Due to a bug in Ghost webhooks are now created with a trailing "/" which
meant that the previous webhooks to that (without a slash) was never
removed.
This results in users receiving emails from stripe about failed webhook
delivery, which is not good at all.
This fix lists out the webhooks and finds (if present) the webhook which
matches the current URL, minus the trailing slash. If found it will then
attempt to delete that webhook thus stopping the emails from Stripe.
I've added a note to remove this code as it should only ever need to run
once, and can be removed for the Ghost release after these changes.
no-issue
* Refactored model dependencies
This groups all of the model depenencies into a single models object,
and renames the models with more concise identifiers
* Fixed spacing
* Added webhook support to metadata
* Refactored stripe configure to have better logging
* Refactored webhook creation to reuse existing webhook
* Installed @types/stripe
- We have many customers asking for INR as there are special rules in Stripe for this currency
- As well as a desire for local-selling
- Meaning it's not valid to use e.g. USD instead
no issue
- This method is needed to be able to validate if customer exist in configured Stripe account before attempting to link one with local member.
no-issue
Without this flag the checkout session will ignore any default trial periods
attached to the plan. Now we are able to give basic support for trials, by
attaching a trial period in Stripe Dashboard
no issue
- Makes passing `name` and `note` field in member update data as optional instead of making them undefined
- Allows email to be updated
- Adds stripe subscriptions list to updated member's response data to make update consistent with get method
refs TryGhost/members.js#29
- Allows passing metadata to checkout session API
- Metadata is passed to stripe's checkout session on creation and read back from webhook event
- Allows clients like members.js to pass custom info like member name to Stripe flow
no issue
- Current update stripe subscription API calls only allowed cancelling a plan
- This change adds option to pass plan's nickname as `planName` in request to update subscription to new plan
- Checks if plan name is valid and updates stripe subscription to new plan at default prorate behavior
refs https://github.com/TryGhost/members.js/issues/10
- Allows passing an additional `customerEmail` value to our checkout creation API
- This value is used to pass `customer_email` option to stripe's checkout session - https://stripe.com/docs/api/checkout/sessions/create#create_checkout_session-customer_email.
The `customer_email` allows pre-filling the customer's email field in case of an anonymous checkout as customer doesn't exist already, and also ensures the stripe subscription is created with same email address as given by user during signup flow.
no issue
- Adding these properties allows specifying which currency is currently used on member's plan.
- Supported currencies list: USD, AUD, CAD, GBP, EUR
- They were chosen based on the most used/requested currencies within Ghost
- With adding multiple available currencies that can be setup also had to add handling of Stripes limitation of having single currency per paying customer
requires f38d490886
- adds `lib/geolocation.js` with `getGeolocationFromIP()` function which uses https://geojs.io to lookup geolocation data from an IPv4 or IPv6 address
- updates `create/updateMember()` functions to work with a `geolocation` property in the passed in object
- if `geolocation` is `undefined` when updating a member do not reset any existing property
- updates `sendMagicLink` middleware to extract the IP address from the request and stores it as part of the token payload
- updates `getMemberDataFromMagicLinkToken()` method to extract the IP address from the token payload and perform a geolocation lookup if we have an IP address and a matching member does not already have geolocation data
This reverts commit 5f0d2168f3.
After discussing the best approach to multipe currency problem would be
to allow creating multiple "Complimentary" plans. All security related
checks should stay strictly based on name and would not cause issues.
refs https://github.com/TryGhost/Ghost-Admin/pull/1430
- When the client creates a complimentary plan with other currency than USD we should not allow for it to avoid creating a mess in the Stripe plans
refs https://github.com/TryGhost/Ghost/pull/11571
- Allows updating members billing information through Stripe's setup intent (stripe.com/docs/payments/checkout/subscriptions/updating#set)
- Accepts 2 new parameter to handle redirects specific to billing update.
no issue
- On model layer in Ghost empty string is always converted to `null` for not nullable fields, which wasn't letting the value through to the database
- Current solution is a stopgap to fix imports of cyclic plans without nicknames. Ideally nickname field should become nullable in the future so this logic can be simplified
no issue
- This solves a problem when connected Stripe plan doesn't have plan `nickname` filled out (possible with older versions of Stripe API)
- Defaulting to empty string instead of creating a migration because SQLite doesn't support `ALTER ... MODIFY` syntax and thus knex can't altter the table that easy
- "Marks the column as an alter / modify, instead of the default add. Note: This only works in .alterTable() and is not supported by SQlite or Amazon Redshift. Alter is not done incrementally over older column type so if you like to add notNull and keep the old default value, the alter statement must contain both .notNull().defaultTo(1).alter(). If one just tries to add .notNull().alter() the old default value will be dropped." (ref. https://knexjs.org/#Chainable)
refs https://github.com/TryGhost/Ghost/pull/11537
- Adds ability to assign and cancel "complimentary" type of subscriptions to the member
- The functionality is needed to be able to provide free premium plans for members (e.g. family members, trials, gifts)
- When member already has an active paid subscription and complimentary one is applied the old one is upgraded. Proration is not given
- When deleting a subscription we need to update localy stored records right away to be albe to reflect the change in the UI. This behavior will also be in line with how subscriptions updates/creates are handled
- Blocked any client update for complimentary subscription. We should prevent non authenticated clients from upgrading/subscribing themselves to "complimentary" plan.
no issue
- `customers` property contains an array of customer for which 'for..of' syntax is more appropriate
- Bug was causing creation of multiple customers in Stripe when new checkout session was initiated for existing customer
- Discussed in https://github.com/TryGhost/Members/pull/90/files#r368889289
refs https://github.com/TryGhost/Members/pull/105
- It's a follow up to a series of refactorings in the module mostly discussed in refed PR
- The sendEmailWithMagicLink and destroyStripeSubscriptions were exposed through members API so that Ghost could call it from the controller level
refs #https://github.com/TryGhost/Ghost/pull/11434
- Added method to allow updating single subscription. Only `cancel_at_period_end` field can be updated.
- Middleware is needed to allow Ghost Core to cancel/uncancel member's subscription.
- Relies on the request containing identity information to be able to verify if subscription belongs to the user
- When member could not be identified by the identity information present in the request we should throw instead of continuing processing
- Handling and messaging inspired by https://github.com/TryGhost/Ghost/blob/3.1.1/core/server/services/mega/mega.js#L132
- When the user initiates subscription cancellation we can safely mark the subscription as canceled so that it's not shown in the interface on subsequent request. Otherwise, we end up in a situation where we still return the subscription in the period until Stripe triggers the webhook.
- Added boolean coercion for cancel_at_period_end parameter. If anything but boolean is passed to Stripe API it throws an error. Coercing the value on our side is a gives a better dev experience
no-issue
When using localhost urls the call to `create` will error and end in teh
catch block - so we need to use the environment variable there, too.
Introduced in 0149dd8f
no issue
- When debugging Stripe with using: `stripe listen \
--forward-to http://ghost.local/members/webhooks/stripe/` this priority is nice to have so that Ghost process can be initialized using WEBHOOK_SECRET env variable
- It was not working in current form because Stripe recognized `ghost.local` as a valid domain and didn't throw any errors
- Removed unneeded secret assignment in a catch statement. It is redundant with the new implementation
no-issue
* customer.subscription.deleted - when a subscription is cancelled
* customer.subscription.updated - when a subscription status/plan changes
* invoice.payment_succeeded - when a subscription has successfully renew
* invoice.payment.failed - when a subscription has failed to renew
no-issue
This updates the initialisation logic to fetch all webhooks (we use
limit: 100, and there are currently a max of 16 webhooks in stripe) and
find one with the corrct url. Once found, delete that webhook. We then
attempt to create a new one, and log out any errors (this is to allow
for local development, creating a webhook with a local url is expected
to fail)
no-issue
This removes the subscription api as we are using stripe checkout to
generate those
This removes the customers api as we no longer need the deterministic
api for it
* Installed stripe@7.4.0
refs #38
We were relying on stripe being installed in Ghost, this moves the dep
to the correct package.
* Created exponentialBackoff wrapper for stripe api
refs #38https://stripe.com/docs/testing#rate-limits The stripe docs suggest to
use exponential backoff when recieving a rate limit error. This wrapper
will wrap stripe api calls, and retry them after 1s,2s,4s,8s,16s until
eventually failing. This gives a total of 5 retries over 31s.
* Added wrappers around the stripe api calls
refs #38
* Ensured all calls to stripe api go via exp backoff
refs #38
* Scaffolding out the error handling for stripe api
* Forwarding all errors
* Refactored stripe api into modules
* Ensured the ready promise object is not replaced
* Added logging setup
- Sets up common logger structure with custom logger passed through
* Ensure logger is kept in module state
* Renamed updateLogger to setLogger
* Removed `logger` param and exposed setLogger method
* Ensured different ids used for test mode
* Ensure setLogger works for prototype methods
* Removed reconfigureSettings method
* Updated payment processer service to keep static ready promise
* Added eventemitter to member api instance to handle errors
* Moved logging of errors to http level