Stripe Subscriptions module API explorer

/api/user/subscriptions/create-usage-record (POST)

Account information like email addresses is generated with faker-js it is not real user information.

await global.api.user.subscriptions.CreateUsageRecord.post(req)

Returns object

{
  "usagerecordid": "mbur_1LEOdvHHqepMFuCXX4mnTjeQ",
  "object": "usagerecord",
  "stripeObject": {
    "id": "mbur_1LEOdvHHqepMFuCXX4mnTjeQ",
    "object": "usage_record",
    "livemode": false,
    "quantity": 400,
    "subscription_item": "si_LwHCaawYvGMBtB",
    "timestamp": 1656123758
  },
  "customerid": "cus_LwHCFzn9o1EEsR",
  "accountid": "acct_bca7369ef4ad8873",
  "subscriptionid": "sub_1LEOdfHHqepMFuCXUSGSS2rj",
  "subscriptionitemid": "si_LwHCaawYvGMBtB",
  "appid": "tests_1656123739",
  "createdAt": "2022-06-25T02:22:39.311Z",
  "updatedAt": "2022-06-25T02:22:39.311Z"
}

Receives

API routes may receive parameters from the URL and POST supporting simple and multipart:

Field Value Required Type
action string required POST
quantity string required POST
subscriptionitemid string required POST

Exceptions

These exceptions are thrown (NodeJS) or returned as JSON (HTTP) if you provide incorrect data or do not meet the requirements:

Exception Circumstances
invalid-account ineligible accessing account
invalid-action missing posted action
invalid posted action
invalid-quantity invalid posted quantity is not integer
invalid posted quantity is negative
invalid-subscription invalid querystring subscription is not "metered"
invalid-subscriptionid missing querystring subscriptionid
invalid querystring subscriptionid
invalid-subscriptionitemid missing posted subscriptionitemid
invalid posted subscriptionitemid

NodeJS source (view on github)

const stripeCache = require('../../../../stripe-cache.js')
const subscriptions = require('../../../../../index.js')

module.exports = {
  post: async (req) => {
    if (!req.query || !req.query.subscriptionid) {
      throw new Error('invalid-subscriptionid')
    }
    const subscription = await global.api.user.subscriptions.Subscription.get(req)
    if (!subscription) {
      throw new Error('invalid-subscriptionid')
    }
    if (!req.body) {
      throw new Error('invalid-quantity')
    }
    try {
      const quantity = parseInt(req.body.quantity, 10)
      if (req.body.quantity !== quantity.toString()) {
        throw new Error('invalid-quantity')
      }
    } catch (s) {
      throw new Error('invalid-quantity')
    }
    if (req.body.quantity < 0) {
      throw new Error('invalid-quantity')
    }
    if (!req.body.action || (req.body.action !== 'increment' && req.body.action !== 'set')) {
      throw new Error('invalid-action')
    }
    if (!req.body.subscriptionitemid || !req.body.subscriptionitemid.length) {
      throw new Error('invalid-subscriptionitemid')
    }
    let found = false
    for (const item of subscription.stripeObject.items.data) {
      found = item.id === req.body.subscriptionitemid
      if (found) {
        break
      }
    }
    if (!found) {
      throw new Error('invalid-subscriptionitemid')
    }
    const usageInfo = {
      action: req.body.action,
      quantity: req.body.quantity
    }
    if (Math.floor(new Date().getTime() / 1000) >= subscription.stripeObject.current_period_start) {
      usageInfo.timestamp = Math.floor(new Date().getTime() / 1000)
    } else {
      usageInfo.timestamp = subscription.stripeObject.current_period_start
    }
    let usageRecord
    try {
      usageRecord = await stripeCache.execute('subscriptionItems', 'createUsageRecord', req.body.subscriptionitemid, usageInfo, req.stripeKey)
    } catch (error) {
      if (error.message === 'invalid-subscriptionitemid' || error.message === 'invalid-subscription') {
        throw new Error('invalid-subscription')
      }
    }
    if (!usageRecord) {
      throw new Error('invalid-usagerecord')
    }
    await subscriptions.Storage.UsageRecord.create({
      appid: req.appid || global.appid,
      usagerecordid: usageRecord.id,
      stripeObject: usageRecord,
      customerid: subscription.stripeObject.customer,
      accountid: req.account.accountid,
      subscriptionid: req.query.subscriptionid,
      subscriptionitemid: req.body.subscriptionitemid
    })
    req.query.usagerecordid = usageRecord.id
    return global.api.user.subscriptions.UsageRecord.get(req)
  }
}

Test source (view on github)

/* eslint-env mocha */
const assert = require('assert')
const TestHelper = require('../../../../../test-helper.js')
const TestStripeAccounts = require('../../../../../test-stripe-accounts.js')
const DashboardTestHelper = require('@layeredapps/dashboard/test-helper.js')

describe('/api/user/subscriptions/create-usage-record', function () {
  before(TestHelper.disableMetrics)
  after(TestHelper.enableMetrics)
  let cachedResponses
  async function bundledData (retryNumber) {
    if (retryNumber > 0) {
      cachedResponses = {}
    }
    if (cachedResponses && cachedResponses.finished) {
      return
    }
    cachedResponses = {}
    await TestHelper.setupBefore()
    await DashboardTestHelper.setupBeforeEach()
    await TestHelper.setupBeforeEach()
    const administrator = await TestStripeAccounts.createOwnerWithPrice({
      active: 'true',
      unit_amount: 3000,
      currency: 'usd',
      tax_behavior: 'inclusive',
      recurring_interval: 'month',
      recurring_interval_count: '1',
      recurring_usage_type: 'metered',
      recurring_aggregate_usage: 'sum'
    })
    const user = await TestStripeAccounts.createUserWithPaidSubscription(administrator.price)
    // invalid / missing subscriptionid
    const req = TestHelper.createRequest('/api/user/subscriptions/create-usage-record')
    req.account = user.account
    req.session = user.session
    req.body = {
      quantity: 'abcde',
      action: 'set',
      subscriptionitemid: 'fake'
    }
    try {
      await req.post()
    } catch (error) {
      cachedResponses.missing = error.message
    }
    const req2 = TestHelper.createRequest('/api/user/subscriptions/create-usage-record?subscriptionid=invalid')
    req2.account = user.account
    req2.session = user.session
    req2.body = {
      quantity: 'abcde',
      action: 'set',
      subscriptionitemid: 'fake'
    }
    try {
      await req2.post()
    } catch (error) {
      cachedResponses.invalid = error.message
    }
    // invalid subscription
    const price2 = await TestHelper.createPrice(administrator, {
      productid: administrator.product.productid,
      unit_amount: 3000,
      currency: 'usd',
      recurring_interval: 'month',
      recurring_interval_count: '1',
      recurring_usage_type: 'licensed',
      recurring_aggregate_usage: 'sum',
      tax_behavior: 'inclusive',
      active: 'true'
    })
    const user2 = await TestStripeAccounts.createUserWithPaidSubscription(price2)
    const req3 = TestHelper.createRequest(`/api/user/subscriptions/create-usage-record?subscriptionid=${user2.subscription.subscriptionid}`)
    req3.account = user2.account
    req3.session = user2.session
    req3.body = {
      quantity: '10',
      action: 'set',
      subscriptionitemid: user2.subscription.stripeObject.items.data[0].id
    }
    try {
      await req3.post()
    } catch (error) {
      cachedResponses.invalidSubscription = error.message
    }
    // invalid account
    const req4 = TestHelper.createRequest(`/api/user/subscriptions/create-usage-record?subscriptionid=${user.subscription.subscriptionid}`)
    req4.account = user2.account
    req4.session = user2.session
    try {
      await req4.post()
    } catch (error) {
      cachedResponses.invalidAccount = error.message
    }
    // invalid quantity
    const req5 = TestHelper.createRequest(`/api/user/subscriptions/create-usage-record?subscriptionid=${user.subscription.subscriptionid}`)
    req5.account = user.account
    req5.session = user.session
    req5.body = {
      quantity: 'abcde',
      action: 'set',
      subscriptionitemid: user.subscription.stripeObject.items.data[0].id
    }
    try {
      await req5.post()
    } catch (error) {
      cachedResponses.invalidQuantity = error.message
    }
    const req6 = TestHelper.createRequest(`/api/user/subscriptions/create-usage-record?subscriptionid=${user.subscription.subscriptionid}`)
    req6.account = user.account
    req6.session = user.session
    req6.body = {
      quantity: '-20',
      action: 'set',
      subscriptionitemid: user.subscription.stripeObject.items.data[0].id
    }
    try {
      await req6.post()
    } catch (error) {
      cachedResponses.negativeQuantity = error.message
    }
    // invalid action
    const req7 = TestHelper.createRequest(`/api/user/subscriptions/create-usage-record?subscriptionid=${user.subscription.subscriptionid}`)
    req7.account = user.account
    req7.session = user.session
    req7.body = {
      quantity: '30',
      action: '',
      subscriptionitemid: user.subscription.stripeObject.items.data[0].id
    }
    try {
      await req7.post()
    } catch (error) {
      cachedResponses.missingAction = error.message
    }
    req7.body = {
      quantity: '40',
      action: 'invalid',
      subscriptionitemid: user.subscription.stripeObject.items.data[0].id
    }
    try {
      await req7.post()
    } catch (error) {
      cachedResponses.invalidAction = error.message
    }
    // missing and invalid subscriptionitemid
    req7.body = {
      quantity: '50',
      action: 'set',
      subscriptionitemid: ''
    }
    try {
      await req7.post()
    } catch (error) {
      cachedResponses.missingItem = error.message
    }
    req7.body = {
      quantity: '60',
      action: 'set',
      subscriptionitemid: 'invalid'
    }
    try {
      await req7.post()
    } catch (error) {
      cachedResponses.invalidItem = error.message
    }
    // quantity
    const req8 = TestHelper.createRequest(`/api/user/subscriptions/create-usage-record?subscriptionid=${user.subscription.subscriptionid}`)
    req8.account = user.account
    req8.session = user.session
    req8.body = {
      quantity: '70',
      action: 'set',
      subscriptionitemid: user.subscription.stripeObject.items.data[0].id
    }
    cachedResponses.quantity = await req8.post()
    await TestHelper.wait(1000)
    // action
    req8.body = {
      quantity: '200',
      action: 'set',
      subscriptionitemid: user.subscription.stripeObject.items.data[0].id
    }
    cachedResponses.action = await req8.post()
    await TestHelper.wait(1000)
    // item
    req8.body = {
      quantity: '300',
      action: 'set',
      subscriptionitemid: user.subscription.stripeObject.items.data[0].id
    }
    cachedResponses.item = await req8.post()
    await TestHelper.wait(1000)
    // response
    const req9 = TestHelper.createRequest(`/api/user/subscriptions/create-usage-record?subscriptionid=${user.subscription.subscriptionid}`)
    req9.account = user.account
    req9.session = user.session
    req9.body = {
      quantity: '400',
      action: 'set',
      subscriptionitemid: user.subscription.stripeObject.items.data[0].id
    }
    req9.filename = __filename
    req9.saveResponse = true
    await TestHelper.wait(1000)
    cachedResponses.returns = await req9.post()
    cachedResponses.finished = true
  }

  describe('exceptions', () => {
    describe('invalid-subscriptionid', () => {
      it('missing querystring subscriptionid', async function () {
        await bundledData(this.test.currentRetry())
        const errorMessage = cachedResponses.missing
        assert.strictEqual(errorMessage, 'invalid-subscriptionid')
      })

      it('invalid querystring subscriptionid', async function () {
        await bundledData(this.test.currentRetry())
        const errorMessage = cachedResponses.invalid
        assert.strictEqual(errorMessage, 'invalid-subscriptionid')
      })
    })

    describe('invalid-subscription', () => {
      it('invalid querystring subscription is not "metered"', async function () {
        await bundledData(this.test.currentRetry())
        const errorMessage = cachedResponses.invalidSubscription
        assert.strictEqual(errorMessage, 'invalid-subscription')
      })
    })

    describe('invalid-account', () => {
      it('ineligible accessing account', async function () {
        await bundledData(this.test.currentRetry())
        const errorMessage = cachedResponses.invalidAccount
        assert.strictEqual(errorMessage, 'invalid-account')
      })
    })

    describe('invalid-quantity', () => {
      it('invalid posted quantity is not integer', async function () {
        await bundledData(this.test.currentRetry())
        const errorMessage = cachedResponses.invalidQuantity
        assert.strictEqual(errorMessage, 'invalid-quantity')
      })

      it('invalid posted quantity is negative', async function () {
        await bundledData(this.test.currentRetry())
        const errorMessage = cachedResponses.negativeQuantity
        assert.strictEqual(errorMessage, 'invalid-quantity')
      })
    })

    describe('invalid-action', () => {
      it('missing posted action', async function () {
        await bundledData(this.test.currentRetry())
        const errorMessage = cachedResponses.missingAction
        assert.strictEqual(errorMessage, 'invalid-action')
      })

      it('invalid posted action', async function () {
        await bundledData(this.test.currentRetry())
        const errorMessage = cachedResponses.invalidAction
        assert.strictEqual(errorMessage, 'invalid-action')
      })
    })

    describe('invalid-subscriptionitemid', () => {
      it('missing posted subscriptionitemid', async function () {
        await bundledData(this.test.currentRetry())
        const errorMessage = cachedResponses.missingItem
        assert.strictEqual(errorMessage, 'invalid-subscriptionitemid')
      })

      it('invalid posted subscriptionitemid', async function () {
        await bundledData(this.test.currentRetry())
        const errorMessage = cachedResponses.invalidItem
        assert.strictEqual(errorMessage, 'invalid-subscriptionitemid')
      })
    })
  })

  describe('receives', () => {
    it('required posted quantity', async () => {
      const usageRecord = cachedResponses.quantity
      assert.strictEqual(usageRecord.stripeObject.quantity, 70)
    })

    it('required posted action', async () => {
      const usageRecord = cachedResponses.action
      assert.strictEqual(usageRecord.stripeObject.quantity, 200)
      // TODO: actions can be verified setting/incrementing
      // when the usage record summaries are  available
    })

    it('required posted subscriptionitemid', async () => {
      const usageRecord = cachedResponses.item
      assert.notStrictEqual(usageRecord.subscriptionitemid, null)
      assert.notStrictEqual(usageRecord.subscriptionitemid, undefined)
    })
  })

  describe('returns', () => {
    it('object', async () => {
      const usageRecord = cachedResponses.returns
      assert.strictEqual(usageRecord.object, 'usagerecord')
    })
  })
})