Skip to content

Commit

Permalink
feat: implement get subscription info method
Browse files Browse the repository at this point in the history
  • Loading branch information
stfsy committed Sep 30, 2022
1 parent f769074 commit a200371
Show file tree
Hide file tree
Showing 3 changed files with 177 additions and 4 deletions.
32 changes: 30 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,6 @@ It does **not**
- validate webhook content. Use and register [paddle-webhook-validator](https://github.com/discue/paddle-webhook-validator) in your application to validate webhooks before storing them.
- provide a component or methods to query payment-related information.

The module stores payment-related information in aollection of the target application like e.g. `api-clients`, or `api-users` and expects the application to read this information anyhow for every request. Therefore, no extra costly read is required.

## Installation
```bash
npm install @discue/paddle-integration-firestore
Expand Down Expand Up @@ -97,6 +95,36 @@ module.exports = async (req, res, next) => {
}
```

### Geting subscription infos
Returns all available information about a subscription. Will include the `start` and (optionally) `end` date, the `status_trail`, and the `payments_trail` and a property indicating whether the subscription is currently `active`.

```js
'use strict'

const { SubscriptionInfo } = require('@discue/paddle-firebase-integration')
// pass the path to the collection here
const subscriptions = new SubscriptionInfo('api_clients')

const PREMIUM_SUBSCRIPTION_PLAN_ID = '123'

module.exports = (req,res,next) => {
// requires application to provide the target ids of the
// subscription document. This can be e.g. the api client id
const targetIds = getSubscriptionIds(targetIds)

const info = await subscriptions.getSubscriptionInfo(targetIds)
// {
// '8': {
// start: '2022-08-30T07:59:44.326Z',
// end: '2022-09-30T07:59:44.404Z',
// status_trail: [Array],
// payments_trail: [Array],
// active: false
// }
// }
}
```

### Checking Subscription Status
Will return the status for all subscriptions associated with the given user/api_client.

Expand Down
54 changes: 52 additions & 2 deletions lib/subscription-info.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,59 @@ const { PLACEHOLDER_DESCRIPTION, UPCOMING_PAYMENTS_DESCRIPTION, ACTIVE_SUBSCRIPT
class SubscriptionInfo {

constructor(storagePath) {
this._storage = resource({ documentPath: storagePath, resourceName: 'subscriptions' })
this._storage = resource({ documentPath: storagePath, resourceName: 'wrapper' })
}

/**
* @typedef SubscriptionInfos
* @property {SubscriptionInfo} [any] subscription info per subscription plan id
*/

/**
* @typedef SubscriptionInfo
* @property {Boolean} active indicates whether the subscription is currently active
* @property {String} [start] ISO-formatted start date
* @property {String} [end=undefined] ISO-formatted end date
* @property {Array} [status_trail] a list of subscription status updates
* @property {Array} [payments_trail] a list of payments
*/

/**
* Reads and returns subscription related information
*
* @property {Array<String>} ids ids necessary to lookup possibly nested subscription object
*
* @returns {SubscriptionInfos}
*/
async getSubscriptionInfo(ids) {
const result = await this._storage.get(ids)
const { subscription } = result

const status = await this.getStartAndEndDates(subscription)
const statusTrail = await this.getStatusTrail(subscription)
const paymentsTrail = await this.getPaymentsTrail(subscription)

return Object.keys(status).reduce((context, key) => {
context[key] = Object.assign(status[key], {
status_trail: statusTrail[key],
payments_trail: paymentsTrail[key] || []
})

const hasStarted = new Date(context[key].start).getTime() < Date.now()
// if we have an end date then subscription was cancelled
const hasEnded = context[key].end ? new Date(context[key].end).getTime() <= Date.now() : false

context[key].active = hasStarted && !hasEnded

return context
}, {})
}

/**
* @typedef {Object} StartAndEndDateBySubscription
* @property {StartAndEndDate} [any] start and end date of a subscription plan
*/

/**
* @typedef {Object} StartAndEndDate
* @property {String} start - subscription start date as ISO formatted string
Expand All @@ -19,7 +69,7 @@ class SubscriptionInfo {
* Returns start and end dates for all subscription plans found in the document.
*
* @param {Object} subscription
* @returns {StartAndEndDate} containing the start and end date
* @returns {StartAndEndDateBySubscription} containing the start and end date
*/
async getStartAndEndDates(subscription) {
const statusByPlanId = this._bySubscriptionId(subscription.status, new Date(2099, 1).getTime())
Expand Down
95 changes: 95 additions & 0 deletions test/spec/subscription-info.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,101 @@ describe('SubscriptionInfo', () => {
await subscriptions.addSubscriptionPlaceholder(ids)
})

describe('.getSubscriptionInfo', () => {
beforeEach(async () => {
const subscriptionId = uuid()
const createPayload = Object.assign({}, subscriptionCreated,
{
subscription_id: subscriptionId,
passthrough: JSON.stringify({ ids }),
event_time: new Date().toISOString()
}
)
await subscriptions.addSubscriptionCreatedStatus(createPayload)

const paymentSuccesfulPayload = Object.assign({}, paymentSucceded, {
event_time: '2017-08-08 10:47:47',
subscription_id: subscriptionId,
passthrough: JSON.stringify({ ids }),
})
await subscriptions.addSuccessfulPayment(paymentSuccesfulPayload)

const paymentSuccesfulPayload2 = Object.assign({}, paymentSucceded, {
event_time: '2019-08-08 10:47:47',
subscription_id: subscriptionId,
passthrough: JSON.stringify({ ids }),
})
await subscriptions.addSuccessfulPayment(paymentSuccesfulPayload2)
})
it('indicates the subscription is active', async () => {
await new Promise((resolve) => setTimeout(resolve, 1000))

const status = await subscriptionInfo.getSubscriptionInfo(ids)
console.log({ status }, new Date().toISOString())
expect(status[subscriptionCreated.subscription_plan_id].active).to.be.true
})
it('returns end dates for a subscription if it was cancelled', async () => {
const payload = Object.assign({}, subscriptionCancelled,
{
subscription_id: 'subscriptionId',
passthrough: JSON.stringify({ ids }),
cancellation_effective_date: new Date().toISOString()
}
)

await subscriptions.addSubscriptionCancelledStatus(payload)
await new Promise((resolve) => setTimeout(resolve, 1000))

const status = await subscriptionInfo.getSubscriptionInfo(ids)
expect(status[subscriptionCreated.subscription_plan_id].start).to.match(/[0-9]{2}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}.[0-9]{3}Z/)
expect(status[subscriptionCreated.subscription_plan_id].end).to.match(/[0-9]{2}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}.[0-9]{3}Z/)
})
it('indicates the subscription is not active', async () => {
const payload = Object.assign({}, subscriptionCancelled,
{
subscription_id: 'subscriptionId',
passthrough: JSON.stringify({ ids }),
cancellation_effective_date: new Date().toISOString()
}
)

await subscriptions.addSubscriptionCancelledStatus(payload)
await new Promise((resolve) => setTimeout(resolve, 1000))

const status = await subscriptionInfo.getSubscriptionInfo(ids)
console.log({ status }, new Date().toISOString())
expect(status[subscriptionCreated.subscription_plan_id].active).to.be.false
})
it('returns a sorted list of status events', async () => {
const { [subscriptionCreated.subscription_plan_id]: { status_trail } } = await subscriptionInfo.getSubscriptionInfo(ids)
expect(status_trail).to.have.length(1)

const sorted = status_trail.every((status, index, array) => {
if (index === 0) {
return true
} else {
return new Date(status.event_time).getTime() <= new Date(array[index - 1].event_time).getTime()
}
})

expect(sorted).to.be.true
})
it('returns a sorted list of payment events', async () => {
const { [subscriptionCreated.subscription_plan_id]: { payments_trail } } = await subscriptionInfo.getSubscriptionInfo(ids)
expect(payments_trail).to.have.length(3)

const sorted = payments_trail.every((payment, index, array) => {
if (index === 0) {
return true
} else {
return new Date(payment.event_time).getTime() <= new Date(array[index - 1].event_time).getTime()
}
})

expect(sorted).to.be.true
})
})

describe('.getAllSubscriptionsStatus', () => {
it('takes the most recent status into account', async () => {
const subscriptionId = uuid()
Expand Down

0 comments on commit a200371

Please sign in to comment.