Skip to content

Commit dbe6360

Browse files
authored
Merge pull request #5940 from FlowFuse/update-to-yearly-billing-on-same-team-type
Update to yearly billing on same team type
2 parents 0b2024a + 763f415 commit dbe6360

3 files changed

Lines changed: 36 additions & 3 deletions

File tree

forge/ee/lib/billing/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -537,6 +537,7 @@ module.exports.init = async function (app) {
537537
deleted: true
538538
})
539539
}
540+
540541
await stripe.subscriptions.update(subscription.subscription, {
541542
proration_behavior: prorationBehavior,
542543
items: newItems

forge/routes/api/team.js

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -714,16 +714,30 @@ module.exports = async function (app) {
714714
try {
715715
if (request.body.type) {
716716
auditLogFunc = app.auditLog.Team.team.type.changed
717+
let billingIntervalUpgrade = false
717718
const bodyOptions = { ...request.body }
718719
const targetTypeId = bodyOptions.type
719720
delete bodyOptions.type
720721
const billingInterval = bodyOptions.billing?.interval || null
721722
delete bodyOptions.billing
723+
722724
if (Object.keys(bodyOptions).length > 0) {
723725
reply.code(400).send({ code: 'invalid_request', error: 'Cannot modify other properties whilst changing type' })
724726
return
725727
}
726-
if (targetTypeId !== request.team.TeamType.hashid) {
728+
729+
if (app.billing) {
730+
// allow team updates if upgrading to yearly billing
731+
const subscription = await request.team.getSubscription()
732+
733+
const currentlyOnMonthlySubscription = subscription.interval === 'month'
734+
const upgradingToYearlySubscription = billingInterval === 'year'
735+
const sameTeamType = targetTypeId === request.team.TeamType.hashid
736+
737+
billingIntervalUpgrade = sameTeamType && upgradingToYearlySubscription && currentlyOnMonthlySubscription
738+
}
739+
740+
if (targetTypeId !== request.team.TeamType.hashid || billingIntervalUpgrade) {
727741
const targetTeamType = await app.db.models.TeamType.byId(targetTypeId)
728742
if (!targetTeamType) {
729743
reply.code(400).send({ code: 'invalid_team_type', error: 'Invalid team type' })

frontend/src/pages/team/changeType.vue

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
<p v-if="isContactRequired">To learn more about our {{ input.teamType?.name }} plan, click below to contact our sales team.</p>
5353
<p v-if="trialMode && !trialHasEnded">Setting up billing will bring your free trial to an end</p>
5454
<p v-if="!isContactRequired && team.suspended">Setting up billing will unsuspend your team</p>
55+
<p v-if="isUpgradingFromMonthlyToYearly">Any additional Hosted or Remote Instances will also switch to yearly billing.</p>
5556
<p v-if="!isContactRequired"> Your billing subscription will be updated to reflect the new costs</p>
5657
</div>
5758
</template>
@@ -66,7 +67,8 @@
6667
:disabled="!formValid" data-action="change-team-type"
6768
@click="updateTeam()"
6869
>
69-
Change team type
70+
<span v-if="isUpgradingFromMonthlyToYearly">Switch to Yearly Billing</span>
71+
<span v-else>Change team type</span>
7072
</ff-button>
7173
<ff-button
7274
v-else :disabled="!formValid"
@@ -138,12 +140,22 @@ export default {
138140
computed: {
139141
...mapState('account', ['user', 'team', 'features']),
140142
formValid () {
143+
const isChangingTeamType = this.input.teamTypeId !== this.team.type.id
144+
141145
return !this.isUnmanaged &&
142146
this.input.teamTypeId &&
143147
this.isSelectionAvailable &&
144-
(this.billingMissing || this.input.teamTypeId !== this.team.type.id) &&
148+
(this.billingMissing || isChangingTeamType || this.isUpgradingFromMonthlyToYearly) &&
145149
this.upgradeErrors.length === 0
146150
},
151+
isUpgradingFromMonthlyToYearly () {
152+
const inputTeamHasAnnual = Object.prototype.hasOwnProperty.call(this.input, 'teamType') &&
153+
Object.prototype.hasOwnProperty.call(this.input.teamType, 'properties') &&
154+
Object.prototype.hasOwnProperty.call(this.input.teamType.properties, 'billing') &&
155+
Object.prototype.hasOwnProperty.call(this.input.teamType.properties.billing, 'yrPriceId')
156+
157+
return inputTeamHasAnnual && this.team.billing?.interval === 'month' && this.isAnnualBilling
158+
},
147159
billingEnabled () {
148160
return this.features.billing
149161
},
@@ -298,6 +310,12 @@ export default {
298310
instanceTypes.forEach(instanceType => {
299311
this.instanceTypes[instanceType.id] = instanceType
300312
})
313+
314+
if (this.team.billing?.interval === 'month') {
315+
this.isAnnualBilling = false
316+
} else if (this.team.billing?.interval === 'year') {
317+
this.isAnnualBilling = true
318+
}
301319
},
302320
async mounted () {
303321
this.mounted = true

0 commit comments

Comments
 (0)