Skip to content

Commit

Permalink
Merge pull request #4621 from FlowFuse/4577-enable-combined-device-in…
Browse files Browse the repository at this point in the history
…stance-billing

Enable combined device/instance free allocation
  • Loading branch information
knolleary authored Oct 10, 2024
2 parents 2ca9a02 + d2861c0 commit 8cf9231
Show file tree
Hide file tree
Showing 11 changed files with 530 additions and 381 deletions.
2 changes: 1 addition & 1 deletion forge/db/controllers/Device.js
Original file line number Diff line number Diff line change
Expand Up @@ -265,7 +265,7 @@ module.exports = {
// delete all devices
await app.db.models.Device.destroy({ where: { id: ids } })
if (app.license.active() && app.billing) {
await app.billing.updateTeamDeviceCount(team)
await app.billing.updateTeamBillingCounts(team)
}

// Log the deletion
Expand Down
3 changes: 1 addition & 2 deletions forge/ee/db/controllers/Subscription.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,7 @@ module.exports = {

// This *could* be a trial team that has devices in it. In which case,
// we need to reconcile the subscription device and instance count
await app.billing.updateTeamInstanceCount(team)
await app.billing.updateTeamDeviceCount(team)
await app.billing.updateTeamBillingCounts(team)
return existingSub
} else {
// Create the subscription
Expand Down
3 changes: 1 addition & 2 deletions forge/ee/lib/billing/Team.js
Original file line number Diff line number Diff line change
Expand Up @@ -192,8 +192,7 @@ module.exports = function (app) {
await this._updateTeamType(teamType)
// Update the device/instance count items on stripe with the new billing
// details
await app.billing.updateTeamDeviceCount(this)
await app.billing.updateTeamInstanceCount(this)
await app.billing.updateTeamBillingCounts(this)
}

/**
Expand Down
384 changes: 201 additions & 183 deletions forge/ee/lib/billing/index.js

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions forge/routes/api/device.js
Original file line number Diff line number Diff line change
Expand Up @@ -333,7 +333,7 @@ module.exports = async function (app) {
reply.send(response)
} finally {
if (app.license.active() && app.billing) {
await app.billing.updateTeamDeviceCount(team)
await app.billing.updateTeamBillingCounts(team)
}
}
} catch (err) {
Expand Down Expand Up @@ -378,7 +378,7 @@ module.exports = async function (app) {
await request.device.destroy()
await app.auditLog.Team.team.device.deleted(request.session.User, null, team, request.device)
if (app.license.active() && app.billing) {
await app.billing.updateTeamDeviceCount(team)
await app.billing.updateTeamBillingCounts(team)
}
reply.send({ status: 'okay' })
} catch (err) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,12 +79,15 @@
<div class="grid gap-3 grid-cols-4">
<div class="grid gap-3 grid-cols-2">
<FormRow v-model="input.properties.devices.limit"># Limit</FormRow>
<FormRow v-if="billingEnabled" v-model="input.properties.devices.free"># Free</FormRow>
<FormRow v-if="billingEnabled" v-model="input.properties.devices.free" :disabled="input.properties.devices.combinedFreeType !== '_'"># Free</FormRow>
</div>
<FormRow v-if="billingEnabled" v-model="input.properties.devices.productId" :type="editDisabled?'uneditable':''">Product Id</FormRow>
<FormRow v-if="billingEnabled" v-model="input.properties.devices.priceId" :type="editDisabled?'uneditable':''">Price Id</FormRow>
<FormRow v-if="billingEnabled" v-model="input.properties.devices.description" placeholder="eg. $10/month" :type="editDisabled?'uneditable':''">Description</FormRow>
</div>
<div v-if="billingEnabled" class="grid gap-3 grid-cols-1">
<FormRow v-model="input.properties.devices.combinedFreeType" :options="deviceFreeOptions" class="mb-4">Share free allocation with instance type:</FormRow>
</div>

<FormHeading>Features</FormHeading>
<div class="grid gap-3 grid-cols-2">
Expand Down Expand Up @@ -145,7 +148,14 @@ export default {
const instanceTypes = await instanceTypesApi.getInstanceTypes()
instanceTypes.types.sort((A, B) => A.order - B.order)
this.instanceTypes = instanceTypes.types
this.deviceFreeOptions = [
{ label: 'None - use own free limit', value: '_' }
]
this.trialInstanceTypes = this.instanceTypes.map(it => {
this.deviceFreeOptions.push({
value: it.id,
label: it.name
})
return {
value: it.id,
label: `Single ${it.name} instance`
Expand Down Expand Up @@ -255,6 +265,7 @@ export default {
{ label: 'Generate invoice for each change', value: 'always_invoice' },
{ label: 'Add proration items to monthly invoice', value: 'create_prorations' }
],
deviceFreeOptions: [],
input: {
name: '',
active: true,
Expand Down Expand Up @@ -331,6 +342,11 @@ export default {
for (const instanceProperties of Object.values(opts.properties.instances)) {
formatNumber(instanceProperties, 'free')
}
if (opts.properties.devices.combinedFreeType === '_') {
delete opts.properties.devices.combinedFreeType
} else if (opts.properties.devices.combinedFreeType) {
delete opts.properties.devices.free
}
opts.properties.billing = { ...this.input.properties.billing }
if (this.input.properties.trial.active) {
opts.properties.trial = { ...this.input.properties.trial }
Expand Down
6 changes: 5 additions & 1 deletion frontend/src/pages/instance/components/InstanceForm.vue
Original file line number Diff line number Diff line change
Expand Up @@ -562,7 +562,11 @@ export default {
// Need to combine the projectType billing info with any overrides
// from the current teamType
const teamTypeInstanceProperties = this.team.type.properties.instances[pt.id]
const existingInstanceCount = this.team.instanceCountByType?.[pt.id] || 0
let existingInstanceCount = this.team.instanceCountByType?.[pt.id] || 0
if (this.team.type.properties.devices?.combinedFreeType === pt.id) {
// Need to include device count as they use a combined free allocation
existingInstanceCount += this.team.deviceCount
}
pt.price = ''
pt.priceInterval = ''
pt.currency = ''
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,10 +111,16 @@ export default {
computed: {
...mapState('account', ['features', 'team']),
deviceIsBillable () {
let freeAllocation = this.team.type.properties.devices.free || 0
let deviceCount = this.teamDeviceCount
if (this.team.type.properties.devices?.combinedFreeType) {
deviceCount += this.team.instanceCountByType?.[this.team.type.properties.devices.combinedFreeType] || 0
freeAllocation = this.team.type.properties.instances[this.team.type.properties.devices.combinedFreeType]?.free || 0
}
return this.features.billing && // billing enabled
!this.team.billing?.unmanaged &&
this.team.type.properties.devices?.description && // >0 per device cost
(this.team.type.properties.devices.free || 0) <= this.teamDeviceCount // no remaining free allocation
freeAllocation <= deviceCount // no remaining free allocation
},
deviceBillingInformation () {
if (this.deviceIsBillable && this.team.type.properties.devices?.description) {
Expand Down
34 changes: 20 additions & 14 deletions test/lib/stripeMock.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,24 +42,30 @@ module.exports = (testSpecificMock = {}) => {
if (update.items) {
// Do not mutate the passed-in object as we have tests
// that check these functions were called with the expected object
const items = update.items.map(originalItem => {
const item = { ...originalItem }
item.id = `item-${stripeItemCounter++}`
item.plan = {
product: (item.price || 'price').replace('price', 'product')
}
item._price = item.price
item.price = {
unit_amount: 123,
product: {
name: (item.price || 'price').replace('price', 'product')

const existingItems = {}
stripeData[subId].items.data.forEach(item => {
existingItems[item.id] = item
})

update.items.forEach(item => {
if (item.deleted) {
delete existingItems[item.id]
delete stripeItems[item.id]
} else if (item.id) {
existingItems[item.id].quantity = item.quantity
} else {
const id = `item-${stripeItemCounter++}`
existingItems[id] = {
id,
quantity: item.quantity,
price: { id: item.price, product: item.price.replace('price', 'product') }
}
stripeItems[id] = existingItems[id]
}
stripeItems[item.id] = item
return item
})
stripeData[subId].items = {
data: items
data: Object.values(existingItems)
}
}
}),
Expand Down
Loading

0 comments on commit 8cf9231

Please sign in to comment.